Python语言系列-09-socket编程





简介

软件开发的架构
          1.C/S架构(client-server)
	  2.B/S架构 (browser-server)


网络基础概念
    网络三要素:
	1、ip

	2、port

        3、通信协议:TCP、UDP
             TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)
             传输效率低全双工通信(发送缓存&接收缓存)、面向字节流
             使用TCP的应用:Web浏览器;电子邮件、文件传输程序

             UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小)
             一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制
             使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)




socket简单使用

server.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socket
from socket import AF_INET, SOCK_STREAM


# 1 创建套接字对象
# AF_INET: 表示ipv4  AF_INET6: 表示ipv6
# SOCK_STREAM:表示tcp协议  SOCK_DGRAM:表示udp协议
sock = socket.socket(family=AF_INET, type=SOCK_STREAM)   # 看源码,括号里参数也可以不写

# 2 绑定IP和端口
sock.bind(("127.0.0.1", 8899))

# 3 创建一个监听数
# 表示可以等待5个请求连接,如果算上已经连接的请求,那么第7个才会报错
# 排队5个,连接1个,所以第7个才会报错
sock.listen(5)

# 4 等待连接,一旦客户端请求过来,会返回两个值:客户端的套接字对象,客户端的地址
print('等待连接...')
conn, addr = sock.accept()  # 阻塞状态,不占CPU资源
print('conn', conn)
print('addr', addr)

# 5 接受客户端信息
data = conn.recv(1024)
print(data.decode())

# 6 给客户端返回一个welcome字符串
conn.send('welcome socket'.encode())

# 7 关闭与客户端连接
conn.close()

# 8 关闭服务器的socket
sock.close()


client.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socket


# 1 创建套接字对象
sock = socket.socket()

# 2 连接服务端
sock.connect(('127.0.0.1', 8899))

# 3 给服务端发送消息
sock.send(b'hello')

# 4 接受服务端的消息
data = sock.recv(1024)
print('data:', data)
print('data:', data.decode())

# 5 关闭链接
sock.close()




聊天室案例

server.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socket


sock = socket.socket()
sock.bind(("127.0.0.1", 8899))
sock.listen(5)

while 1:
    print('等待...')
    conn, addr = sock.accept()
    while 1:
        # 针对windows系统客户端直接关闭的情况处理
        try:
            data = conn.recv(1024)
        except Exception:
            break

        # 针对linux系统客户端直接关闭的情况处理
        if len(data) == 0:
            break

        print('客户消息:', data.decode())

        if data.decode() == 'quit':  # 客户端传入quit,结束会话
            conn.close()
            break

        res = input('响应客户端请求>>>')
        conn.send(res.encode())


client.py

#!/usr/bin/env python3
# author:Alnk(李成果)
import socket

# 1 创建套接字对象
sock = socket.socket()

# 2 连接服务端
sock.connect(('127.0.0.1', 8899))

while 1:
    data = input('>>>')
    if len(data) == 0:  # 防止客户端输入空值,卡死程序
        continue

    if data == 'quit':  # quit退出聊天
        break

    sock.send(data.encode())
    print('等待回复')

    res = sock.recv(1024)
    print('服务器响应:', res.decode())

print('聊天结束')
sock.close()




subprocess模块

#!/usr/bin/env python3
# author: Alnk(李成果)
import subprocess
# subprocess 目的是提供统一的模块来实现对系统命令或脚本的调用
# python3没有了commands模块,subprocess模块就是为了替换commands模块、os.system这些模块
# subprocess模块可以调用操作系统命令,python在linux可以执行shell命令,
# subprocess每执行一条命令,会开启一个子进程(即shell)来执行命令,相当于每执行一条命令,打开一个独立程序窗口,这个就是进程

# subprocess模块 三种执行命令的方法
# subprocess.run(*popenargs, input=None, timeout=None, check=False, **kwargs) 官方推荐
# subprocess.call(*popenargs, timeout=None, **kwargs) 跟上面实现的内容差不多,另一种写法
# subprocess.Popen() 上面各种方法的底层封装


# 1 subprocess.run
# run方法 返回一个对象,执行一条命令返回一个对象,通过对象拿到命令结果
# 1.1 正确的命令
s = subprocess.run(
    ["df", "-h"],
    # PIPE是一个管道,管道相当于建立一个进程执行命令,命令结果通过管道返回python的标准输出,
    # 如果命令执行错误了,管道返回到标准错误
    stderr=subprocess.PIPE,     # stderr是标准错误,输出错误信息
    stdout=subprocess.PIPE,     # stdout是标准输出,输出正确信息
)

print(s.returncode)  # 0 返回命令返回状态
print(s.args)        # ['df', '-h'] 返回命令参数
print("---------------- 0 --------------")

print(s)
print(type(s))  # <class 'subprocess.CompletedProcess'>
print("---------------- 1 --------------")

print(s.stdout)
print(type(s.stdout))  # <class 'bytes'>
print("---------------- 2 --------------")

print(s.stdout.decode())
print(type(s.stdout.decode()))  # <class 'str'>
print("---------------- 3 --------------")

# 1.2 错误的命令或者参数
s2 = subprocess.run(
    ["df", "-123456"],  # 错误的参数命令
    stderr=subprocess.PIPE,
    stdout=subprocess.PIPE,

    # 这里如果没有注释掉,如果命令返回的状态非0,那么会导致程序报错
    # check=True,
)

print(s2.stdout.decode())
print(type(s2.stdout.decode()))  # <class 'str'>
print("---------------- 4 --------------")

print(s2.stderr.decode())
print(type(s2.stderr.decode()))  # <class 'str'>
# df: illegal option -- 1
# usage: df [-b | -H | -h | -k | -m | -g | -P] [-ailn] [-T type] [-t] [filesystem ...]
print("---------------- 5 --------------")

# 1.3 复杂的命令或者参数
s3 = subprocess.run(
    ["ls", "-l", "|", "grep", "client.py"],
    stderr=subprocess.PIPE,
    stdout=subprocess.PIPE,
)

# 报错
print(s3.stderr.decode())
print("---------------- 6 --------------")
# ls: grep: No such file or directory
# ls: |: No such file or directory

s4 = subprocess.run(
    # ["ls", "-l", "|", "grep", "client.py"],
    "ls -l | grep client.py",
    stderr=subprocess.PIPE,
    stdout=subprocess.PIPE,
    shell=True,  # 不需要帮忙拼接参数,直接交给系统去执行shell命令
)
print(s4.stdout.decode())  # -rw-r--r--  1 lichengguo  staff   854 Mar 25 09:55 client.py
print(s4.stderr.decode())
print("---------------- 7 --------------")


# 2 subprocess.call()
# call() 执行命令,返回命令执行状态 , 0 or 非0
s2 = subprocess.call(["ls", "-la"])
print("---------------- 8 --------------")
print(s2)  # 0
print("---------------- 9 --------------")


# 3 subproces.spopen() 方法
# subprocess.Popen(): 用于执行 shell 命令,结果返回三个对象,分别是标准输入,标准输出,标准错误输出
# 常用参数
# args: shell命令,可以是字符串或者序列类型(如:list,元组)
# stdin, stdout, stderr: 分别表示程序的标准输入、输出、标准错误
# preexec_fn: 只在Unix平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用,执行命令之前可以调一个python函数
# cwd: 用于设置子进程的当前目录
# env: 用于指定子进程的环境变量。如果env = None,子进程的环境变量将从父进程中继承。 设置环境变量
# shell=True的意思是这条命令不需要帮忙拼接参数,直接交给系统去执行shell命令

# Popen调用后会返回一个对象,可以通过这个对象拿到命令执行结果或状态等,该对象有以下方法

s3 = subprocess.Popen(
    "ls -l",
    # "ls -123456",
    shell=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,

    # 这里如果没有注释掉,如果命令返回的状态非0,那么会导致程序报错
    # check=True,
)
print(s3)  # <subprocess.Popen object at 0x7fee6f334160>
print("---------------- 10 --------------")
print(s3.stdout)  # <_io.BufferedReader name=3>
print(s3.stdout.read().decode())
print("---------------- 11 --------------")
# 输出结果
# total 48
# -rw-r--r--  1 lichengguo  staff   202 Apr 23 17:18 base.py
# -rw-r--r--  1 lichengguo  staff   854 Mar 25 09:55 client.py
# -rw-r--r--  1 lichengguo  staff  1155 Mar 25 09:55 server.py
# -rw-r--r--  1 lichengguo  staff   493 Apr 23 16:47 struct模块.py
# -rw-r--r--  1 lichengguo  staff  4653 Apr 25 10:25 subprocess模块.py
print(s3.stdout.read().decode())  # 没有输出结果,只能read一次
print("---------------- 12 --------------")

# print(s3.stderr.read().decode())
# ls: illegal option -- 2
# usage: ls [-@ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1%] [file ...]




struct模块

#!/usr/bin/env python3
# author: Alnk(李成果)
import struct  # 打包


# pack 打包
s1 = struct.pack('i', 1000)  # int 类型,打包后的大小为4个字节
print(s1)        # b'\xe8\x03\x00\x00'
print(type(s1))  # <class 'bytes'>
print("----------------------- 1 ------------------------")


# unpack 解包
temp = struct.pack('i', 200)

d1 = struct.unpack('i', temp)
print(d1)        # (200,) 解包出来的数据是一个元组
print(d1[0])     # 200




SSH案例(解决粘包)

server.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socket
import struct
import subprocess

sock = socket.socket()
sock.bind(('127.0.0.1', 8800))
sock.listen(5)

while 1:
    print('等待...')
    conn, addr = sock.accept()

    while 1:
        # noinspection PyBroadException
        try:
            cmd = conn.recv(1024)
        except Exception as e:
            break

        if len(cmd) == 0:
            break

        print('客户端的命令:', cmd.decode())

        if cmd.decode() == 'quit':
            conn.close()
            break

        # 执行客户端传过来的命令
        res = subprocess.Popen(
            cmd.decode(),
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        cmd_data = res.stdout.read()
        print("命令返回结果:", cmd_data.decode())
        print('命令返回字节长度:', len(cmd_data))

        # 解决粘包问题: 根据网络传输的报头报文思路
        pack_length = struct.pack('i', len(cmd_data))  # 打包
        final_data = pack_length + cmd_data            # 拼接命令返回的数据
        conn.send(final_data)


client.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socket
import struct

sock = socket.socket()
sock.connect(('127.0.0.1', 8800))

while 1:
    cmd = input('请输入命令>>>')
    if len(cmd) == 0:
        continue
    if cmd == 'quit':
        break
    sock.send(cmd.encode())

    # 接收服务器数据传过来的数据总长度大小
    temp_data_len = sock.recv(4)
    data_len = struct.unpack('i', temp_data_len)[0]
    print('数据字节总长度:', data_len)

    # 循环接收所有的数据
    recv_data_length = 0  # 已经接收的字节长度
    recv_data = b''       # 已经接收的字节
    while recv_data_length < data_len:
        temp_data = sock.recv(1024)
        recv_data += temp_data  # 拼接字节
        recv_data_length += len(temp_data)  # 加字节长度

    print("服务器命令返回值\n", recv_data.decode())




模拟FTP服务简单示例

server.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socket
import struct
import json
import os


class FTPServer(object):
    """ftp服务端"""
    ip = '127.0.0.1'
    server_port = 8800

    def get_socket(self):
        sock = socket.socket()
        sock.bind((self.ip, self.server_port))
        sock.listen(5)
        return sock

    def run(self):
        sock = self.get_socket()
        print('server is waiting...')
        conn, _ = sock.accept()

        while 1:
            # 获取4个字节,即信息字典的字节长度
            length_park = conn.recv(4)
            length = struct.unpack('i', length_park)[0]

            # 接收信息字典
            info_bites = conn.recv(length)
            info = json.loads(info_bites)
            print('客户端传递的信息字典:', info)

            cmd = info.get('action')
            if hasattr(self, cmd):
                getattr(self, cmd)(info, conn)
            else:
                print('客户端传递过来的信息字典有问题')

    def put(self, info, conn):
        filename = info.get('filename')
        file_size = info.get('file_size')

        # 循环接收客户端传递的文件
        if not os.path.isdir("db"):
            os.mkdir("db")

        with open("db/" + filename, 'wb') as f:
            recv_data_len = 0
            while recv_data_len < file_size:
                line = conn.recv(1024)
                f.write(line)
                recv_data_len += len(line)

    def download(self, info):
        """
        处理用户下载请求
        :return:
        """
        pass


if __name__ == '__main__':
    fs = FTPServer()
    fs.run()


client.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socket
import json
import struct


class FTPClient(object):
    def __init__(self):
        self.ip = '127.0.0.1'
        self.server_port = 8800
        self.run()

    def get_socket(self):
        sock = socket.socket()
        return sock

    def run(self):
        self.sock = self.get_socket()
        self.sock.connect((self.ip, self.server_port))

        msg = '''
            1 : 上传
            2 : 下载
        '''
        action_dict = {'1': 'put', '2': 'download'}
        while 1:
            print(msg)
            choose = input('请输入命令>>>')
            if hasattr(self, action_dict[choose]):
                getattr(self, action_dict[choose])()
            else:
                print('输入有误,重新输入')

    def put(self):
        """
        上传文件
        :return:
        """
        print('欢迎上传文件')
        cmd = input('输入命令>>>')
        action, params = cmd.split(' ')

        if action == 'put':
            # 上传的基本信息
            file_size = 0  # 字节长度
            with open(params, 'rb') as f:
                for line in f:
                    file_size += len(line)
            # 基本信息字典
            info = {'action': action, 'filename': params, 'file_size': file_size}
            # 打包格式
            info_bites = json.dumps(info).encode()
            info_bites_length = len(info_bites)  # 基本信息字典 字节长度
            length_pack = struct.pack('i', info_bites_length)
            # 拼接信息字典字节长度+基本信息字典
            new_data = length_pack + info_bites
            self.sock.send(new_data)
            # 开始上传数据
            with open(params, 'rb') as f:
                for line in f:
                    self.sock.send(line)
            print('上传完成')
        else:
            print('命令错误!')

    def download(self):
        """
        下载文件
        :return:
        """
        pass


if __name__ == '__main__':
    fc = FTPClient()




socketserver并发聊天室

server.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socketserver


class MyServer(socketserver.BaseRequestHandler):
    # 需要重写handle方法
    def handle(self):
        while 1:
            # 针对windows
            # noinspection PyBroadException
            try:
                data = self.request.recv(1024)
            except Exception as e:
                break

            # 针对linux
            if len(data) == 0:
                break

            print('客户端请求:', data.decode())
            if data.decode() == 'quit':
                self.request.close()
                break

            res = input('响应客户端请求>>>')
            self.request.send(res.encode())


server = socketserver.ThreadingTCPServer(('127.0.0.1', 8899), MyServer)
server.serve_forever()


client.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socket

sock = socket.socket()
sock.connect(('127.0.0.1', 8899))

while 1:
    data = input('发给服务端的请求>>>')

    if len(data) == 0:
        continue

    if data == 'quit':
        break

    sock.send(data.encode())

    res = sock.recv(1024)
    print('服务器的回复:', res.decode())

print('聊天结束')
sock.close()




练习-FTP项目

需求

网络编程需求
    1. 多用户同时登陆   (socketserver知识点)
    2. 用户登陆,加密认证  (hashlib)
    3. 上传/下载文件,保证文件一致性 (hashlib)
    4. 传输过程中现实进度条
    5. 不同用户家目录不同,且只能访问自己的家目录
    6. 对用户进行磁盘配额、不同用户配额可不同
    7. 用户登陆server后,可在家目录权限下切换子目录
    8. 查看当前目录下文件,新建文件夹
    9. 删除文件和空文件夹
    10. 充分使用面向对象知识
    11. 支持断点续传


简单分析一下实现方式
    1.字符串操作以及打印         —— 实现上传下载的进度条功能
    2.socketserver            —— 实现ftp server端和client端的交互
    3.struct模块               —— 自定制报头解决文件上传下载过程中的粘包问题
    4.hashlib或者hmac模块       —— 实现文件的一致性校验和用户密文登录
    5.os模块                   —— 实现目录的切换及查看文件文件夹等功能
    6.文件操作                  —— 完成上传下载文件及断点续传等功能


服务端

目录结构

server
├── __init__.py
│
├── bin
│   ├── __init__.py
│   └── start.py
├── conf
│   ├── __init__.py
│   └── settings.py
├── core
│   ├── __init__.py
│   └── main.py
└── db
    ├── __init__.py
    ├── alnk
    │   └── test.txt
    └── user_info
        ├── __init__.py
        └── user_info.json



start.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import os
import sys


sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# print(sys.path)
from server.core.main import run


if __name__ == '__main__':
    run()


settings.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import os


# IP,PORT
IP = '127.0.0.1'
PORT = 8100


# db目录
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
db_base = os.path.join(base, 'db')


# db/info/user_info.json目录
user_info_base = os.path.join(db_base, 'user_info')
user_info_file = os.path.join(user_info_base, 'user_info.json')


main.py

#!/usr/bin/env python3
# author: Alnk(李成果)
import socketserver
import struct
import json
import os
import hashlib
from server.conf.settings import IP, PORT, user_info_file, db_base


class FTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        while 1:
            # noinspection PyBroadException
            try:
                data_length = self.request.recv(4)
                info_length = struct.unpack('i', data_length)[0]   # 解包
                info = json.loads(self.request.recv(info_length))  # {"action":"", ...}
            except Exception:
                break

            if hasattr(self, info['action']):          # 反射
                getattr(self, info['action'])(info)

    def __recv_write_file(self, file_path, file_size, exist_size=0, mode='wb'):
        """接收数据,写入文件"""
        with open(file_path, mode) as f:
            recv_data = 0
            while recv_data < file_size - exist_size:
                data = self.request.recv(1024)
                f.write(data)
                recv_data += len(data)

    @staticmethod
    def __get_md5_and_size(file_path):
        """获取md5和文件大小"""
        md5 = hashlib.md5()
        size = 0
        with open(file_path, 'rb') as f:
            for line in f:
                md5.update(line)
                size += len(line)
        return md5.hexdigest(), size

    @staticmethod
    def __get_info_dict(file_path):
        """获取用户信息"""
        with open(file_path, 'r') as f:  # 读取用户信息
            msg_dict = json.load(f)
        return msg_dict

    @staticmethod
    def __write_info_dict(file_path, info_dict):
        """修改磁盘配额数据,并且写入文件"""
        with open(file_path, 'w') as f:
            json.dump(info_dict, f)
            f.flush()

    def __file_send_to_client(self, file_path, pointer=0):
        """传输文件到服务器"""
        # pointer 指针,默认为0
        already_size = 0
        with open(file_path, 'rb') as f:
            f.seek(pointer)  # 调整指针位置
            for line in f:
                self.request.send(line)  # 循环发送数据到服务器
                already_size += len(line)

    def __info_send_to_client(self, msg):
        """发送给服务端的报头信息"""
        bites_msg = json.dumps(msg).encode()
        bites_len = len(bites_msg)
        msg_pack = struct.pack('i', bites_len)
        last_msg = msg_pack + bites_msg
        self.request.send(last_msg)

    def login(self, info):
        """登录"""
        msg_dict = self.__get_info_dict(user_info_file)
        if msg_dict.get(info['name']) and msg_dict[info['name']]['pwd'] == info['pwd']:  # 判断账号密码
            self.request.send(b'OK')  # 登录成功标志位
        else:
            self.request.send(b'NO')  # 登录失败标志位

    def put(self, info):
        """上传"""
        name = info['name']            # 用户名
        file_name = info['file_name']  # 文件名
        file_size = info['file_size']  # 文件大小
        file_md5 = info['file_md5']    # md5
        file_path = os.path.join(db_base, name, file_name)  # 文件路径

        msg_dict = self.__get_info_dict(user_info_file)     # 服务器用户信息表

        # 判断磁盘配额是否足够
        if file_size > msg_dict[info['name']]['quota']:
            self.request.send(b'NO QUOTA')
            return False

        # 待上传的文件存在了,断点续传或者不需要传输
        if os.path.isfile(file_path):
            # 获取已经存在的文件的大小和md5
            exist_md5, exist_size = self.__get_md5_and_size(file_path)

            # 同一个文件,不在需要上传了
            if file_md5 == exist_md5:
                self.request.send(b'NO')
            else:
                # 断点续传
                self.request.send(str(exist_size).encode())  # 直接发送服务器文件的大小给客户端
                self.__recv_write_file(file_path, file_size, exist_size=exist_size, mode='ab')  # 接收数据,写入文件
                msg_dict[info['name']]['quota'] -= file_size      # 减少磁盘配额
                self.__write_info_dict(user_info_file, msg_dict)  # 写入用户信息文件
                recv_md5 = self.__get_md5_and_size(file_path)[0]  # 获取md5
                if file_md5 == recv_md5:  # 如果 md5 相等,证明没有丢包,上传完成
                    self.request.send(b'OK')
                else:
                    self.request.send(b'NO')

        else:
            # 待上传的文件不存在,全额传输
            self.request.send(b'NO EXIST')  # 发送给客户端的标志位
            self.__recv_write_file(file_path, file_size)  # 接收数据,写入文件
            msg_dict[info['name']]['quota'] -= file_size  # 减少磁盘配额
            self.__write_info_dict(user_info_file, msg_dict)  # 写入用户信息文件
            recv_md5 = self.__get_md5_and_size(file_path)[0]  # 获取md5
            if file_md5 == recv_md5:  # 如果md5 相等,证明没有丢包,上传完成
                self.request.send(b'OK')
            else:
                self.request.send(b'NO')

    def get(self, info):  # 下载
        user_name = info['name']       # 用户名称
        file_name = info['file_name']  # 需要传输给客户端的文件名称
        file_md5 = info['file_md5']    # md5
        pointer = info['pointer']      # 指针,服务端不做断点续传判断,直接 seek 指针的位置就行了
        file_path = os.path.join(db_base, user_name, file_name)  # 客户端请求下载的文件的路径
        if os.path.isfile(file_path):  # 如果文件存在
            md5, file_size = self.__get_md5_and_size(file_path)  # 获取本地文件的md5 和大小
            if file_md5 == md5:  # 需要下载的文件已经在客户端完整的存在了
                info = {'file_size': file_size, 'file_md5': md5, 'stat': 'EXIST'}
                self.__info_send_to_client(info)  # 发送报头给客户端
            elif 0 < pointer < file_size:  # 断点续传
                file_size -= pointer
                info = {'file_size': file_size, 'file_md5': md5, 'stat': 'NO'}  # 断点续传
                self.__info_send_to_client(info)  # 发送报头给客户端
                self.__file_send_to_client(file_path, pointer=pointer)  # 发送文件给客户端
            else:  # 全额传输
                file_size -= pointer  # 传给客户端的文件大小要减去客户端已经存在的文件大小
                info = {'file_size': file_size, 'file_md5': md5, 'stat': 'OK'}
                self.__info_send_to_client(info)  # 发送报头给客户端
                self.__file_send_to_client(file_path, pointer=pointer)  # 发送文件给客户端
        else:
            info = {'file_size': None, 'file_md5': None, 'stat': 'NO EXIST'}
            self.__info_send_to_client(info)  # 发送报头给客户端
            print('需要下载的文件不存在')
            return False

    def cd(self, info):  # 切换目录
        base_path = os.path.join(db_base, info['name'], '%s' % info['parameter'])
        if os.path.isdir(base_path):  # 如果要切换的目录存在
            self.get_bash_path(info, base_path, flag=True)  # 修改目录路径
            self.request.send(('目录切换成功,当前目录为[%s]' % base_path).encode())
        else:
            self.request.send('目录不存在,切换失败!'.encode())

    def get_bash_path(self, info, base_path=None, flag=False):  # 更改目录
        if not flag:
            self.base_path = os.path.join(db_base, info['name'], '%s' % info['parameter'])
            return self.base_path

        self.base_path = base_path
        return self.base_path

    def ls(self, info):  # 显示
        base_path = os.path.join(db_base, info['name'], info['parameter'])
        print(base_path)
        try:
            res = os.listdir(base_path)
        except FileNotFoundError:
            print('没有该目录')
            res = '没有该目录'
        self.request.send(str(res).encode())

    def mk(self, info):  # 新建
        base_path = os.path.join(db_base, info['name'], '%s' % info['parameter'])
        print(base_path)
        try:
            os.makedirs(r'%s' % base_path)
            res = '新建目录成功!'
        except FileExistsError as e:
            res = '当文件已存在时,无法创建该文件'
        self.request.send(str(res).encode())

    def rm(self, info):  # 删除
        base_path = os.path.join(db_base, info['name'], '%s' % info['parameter'])
        try:
            os.removedirs(r'%s' % base_path)
            res = '目录删除成功!'
        except Exception:
            res = '目录删除失败!'
        self.request.send(str(res).encode())


def run():
    fs = socketserver.ThreadingTCPServer((IP, PORT), FTPServer)
    fs.serve_forever()


if __name__ == "__main__":
    run()


user_info.json

{"alnk": {"pwd": "202cb962ac59075b964b07152d234b70", "quota": 9999912}, "tom": {"pwd": "250cf8b51c773f3f8dc8b4be867a9a02", "quota": 1000}}



客户端

README

作者:Alnk(李成果)
版本:v1.0


程序介绍
    功能全部用python知识完成,用到了 os\sys\struct\hashlib\socket\re\json\模块导入\面向对象编程等知识
    常用功能:上传文件、下载文件、显示目录、新建目录、删除目录、切换目录等
    注意:切换目录还存在一个bug,切换目录对其他功能不生效


程序启动方式
    直接运行 client\bin\start.py 文件(需要先启动服务端程序,然后再启动客户端程序)


登录账号密码
    账号:alnk 密码:123
    账号:tom 密码:456


client 目录
    ├─bin              # 执行目录
    │      start.py    # 程序执行文件
    |
    ├─conf             # 配置目录
    │  │  settings.py  # 配置文件
    │  │
    ├─core             # 程序主目录
    │  │  main.py      # 程序主逻辑
    │
    ├─doc              # 文档目录
    │  │  README       # 说明文件
    │  │
    └─db               # 数据目录
        ├─tom          # 用户目录,从服务器下载的文件都默认放在各自的目录下
        └─alnk         # 用户目录


start.py

#!/usr/bin/env python3
# author:Alnk(李成果)
import os
import sys


sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from client.core.main import FTPClient


if __name__ == '__main__':
    name = input('请输入账号>>>:')
    pwd = input('请输入密码>>>:')
    fc = FTPClient(name, pwd)
    fc.run()


settings.py

#!/usr/bin/env python3
# author:Alnk(李成果)
import os

# IP,PORT
IP = '127.0.0.1'
PORT = 8100


# client目录
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# db 目录
db_path = os.path.join(base_path, 'db')


main.py

#!/usr/bin/env python3
# author:Alnk(李成果)
import socket
import struct
import json
import hashlib
import os
import sys
import re
from client.conf.settings import IP, PORT, db_path


class FTPClient(object):
    def __init__(self, name, pwd):
        self.name = name
        self.pwd = pwd
        self.sock = socket.socket()
        self.__connect_server()

    def __connect_server(self):
        """连接服务器"""
        self.sock.connect((IP, PORT))

    def info_send_to_server(self, msg):
        """发送给服务端的报头信息"""
        bites_msg = json.dumps(msg).encode()
        bites_len = len(bites_msg)

        msg_pack = struct.pack('i', bites_len)
        last_msg = msg_pack + bites_msg
        self.sock.send(last_msg)

    def file_send_to_server(self, file_path, file_size, pointer=0):
        """传输文件到服务器"""
        # pointer 指针,默认为0
        already_size = 0  # 已经传递给服务器大小
        with open(file_path, 'rb') as f:
            f.seek(pointer)  # 调整指针位置
            # todo 如果一行文件非常大的话,这种读取方式可能有问题
            for line in f:
                self.sock.send(line)  # 循环发送数据到服务器
                already_size += len(line)
                self.view_bar(already_size, file_size - pointer)  # 进度条

    @staticmethod
    def size_and_md5(file):
        """统计文件字节数和md5"""
        md5 = hashlib.md5()
        size = 0
        with open(file, 'rb') as f:
            for line in f:
                size += len(line)
                md5.update(line)
        return size, md5.hexdigest()

    @staticmethod
    def get_md5(content):  # 密码md5
        md5 = hashlib.md5()
        md5.update(content.encode())
        return md5.hexdigest()

    @staticmethod
    def view_bar(already_size, total_size):
        """
        进度条
        @param already_size:
        @param total_size:
        @return:
        """
        num = int((already_size / total_size) * 100)
        r = '\r%s%s%%' % ('>' * num, num)
        sys.stdout.write(r)
        sys.stdout.flush

    def __recv_info_server(self):  # 接收来自服务端的报头信息
        data_length = self.sock.recv(4)
        info_length = struct.unpack('i', data_length)[0]  # 解包
        info_dict = json.loads(self.sock.recv(info_length))
        return info_dict

    def __recv_server_write_file(self, file_path, file_size, exist_size=0, mode='wb'):  # 接收数据,写入文件
        with open(file_path, mode) as f:
            recv_data = 0
            while recv_data < file_size - exist_size:
                data = self.sock.recv(1024)
                f.write(data)
                recv_data += len(data)
                self.view_bar(recv_data, file_size)

    def login(self):
        """登录"""
        pass_wd = self.get_md5(self.pwd)  # 密码进行md5加密
        msg = {'action': 'login', 'name': self.name, 'pwd': pass_wd}

        self.info_send_to_server(msg)         # 发送给服务端

        flag = self.sock.recv(1024).decode()  # 从服务端接收返回数据
        if flag == 'OK':
            return True
        else:
            return False

    def put(self):
        """
        上传文件
        @return:
        """
        print("上传文件提示命令: put filename")
        cmd = input("输入命令>>>:")
        # noinspection PyBroadException
        try:
            action, parameter = cmd.lstrip(" |\t").rstrip(" |\t").split(" ")
        except Exception:
            print("提示命令: put filename")
            return

        if action == 'put' and os.path.isfile(parameter):         # 判断输入的命令是否合法
            file_size, file_md5 = self.size_and_md5(parameter)    # 获取文件大小和md5
            file_name = os.path.basename(parameter)               # 获取命令参数中的文件名
            info = {
                'action': action,
                'file_name': file_name,
                'file_size': file_size,
                'file_md5': file_md5,
                'name': self.name,
            }

            self.info_send_to_server(info)        # 发送给服务端
            flag = self.sock.recv(1024).decode()  # 接收服务端返回的标志位
        else:
            print("提示命令: put filename")
            print("文件[%s]不存在" % parameter)
            return

        # 看服务器返回的标志位

        if flag == 'NO EXIST':
            # 待上传的文件在服务器上没有,全额传输
            self.file_send_to_server(parameter, file_size)
            end_flag = self.sock.recv(1024).decode()  # 接收服务端返回的标志位
            if end_flag == 'OK':
                print('\n文件上传完成,md5校验通过')
            else:
                print('\n文件上传有问题了')

        elif flag == 'NO':
            # 已经上传过了,不需要在上传了
            print('文件在服务器中已经存在了,不需要在上传了')

        elif flag == 'NO QUOTA':
            print("磁盘配额不够,请联系管理员修改磁盘配额")

        elif flag.isnumeric():
            # 待上传文件在服务器存在,断点续传
            print('该文件之前已经传输过一部分了,启用断点续传...')
            # pointer=int(flag)  直接接收服务器返回已存在的文件的大小
            self.file_send_to_server(parameter, file_size, pointer=int(flag))
            end_flag = self.sock.recv(1024).decode()  # 接收服务端返回的标志位
            if end_flag == 'OK':
                print('\n断点续传完成,md5校验通过')
            else:
                print('断点续传完成有问题了')

    def get(self):  # 下载
        cmd = input('输入命令>>>')
        try:
            action, parameter = cmd.replace(' ', '').split('|')
        except Exception:
            print('提示: put | filename')
            return False
        if action == 'get':  # 判断输入是否合法
            # 判断要下载的文件是否已经存在本地了
            file_name = parameter  # 获取文件名称
            file_path_name = os.path.join(db_path, self.name, file_name)  # db/self.name/file_name 目录下的文件
            if os.path.isfile(file_path_name):  # 不下载或者断点续传
                file_size, md5 = self.size_and_md5(file_path_name)  # 文件已经存在的大小
                info = {'action': action, 'file_name': file_name, 'name': self.name, 'pointer': file_size,
                        'file_md5': md5, }
                self.info_send_to_server(info)  # 发送给服务端的报头信息
                recv_info_dict = self.__recv_info_server()
                if recv_info_dict['stat'] == 'EXIST':  # 需要下载的文件和服务端的文件一致
                    print('文件已经完整的存在客户端了,不需要在下载了')
                elif recv_info_dict['stat'] == 'NO':  # 断点续传
                    print('该文件之前已经下载了一部分了,启用断点续传')
                    self.__recv_server_write_file(file_path_name, recv_info_dict['file_size'], mode='ab')  # 接收数据写入文件
                    if md5 == recv_info_dict['file_md5']:
                        print('\n断点续传完成,md5校验通过')
                    else:
                        print('\n断点续传md5校验未通过')
            else:  # 全额下载
                info = {'action': action, 'file_name': file_name, 'name': self.name, 'pointer': 0,
                        'file_md5': None}  # pointer 指针,文件已经存在的大小
                self.info_send_to_server(info)  # 发送给服务端的报头信息
                recv_info_dict = self.__recv_info_server()
                if recv_info_dict['stat'] == 'NO EXIST':  # 服务器不存在该文件
                    print('该文件在服务器上不存在')
                    return False
                recv_md5 = recv_info_dict['file_md5']  # 服务端传过来的md5
                recv_size = recv_info_dict['file_size']  # 服务端传过来的文件大小
                self.__recv_server_write_file(file_path_name, recv_size)  # 接收数据写入文件
                md5 = self.size_and_md5(file_path_name)[1]
                if md5 == recv_md5:
                    print('\n下载完成,md5校验通过')
                else:
                    print('\nmd5校验未通过')
        else:
            print('命令错误,提示: get | filename')
            return

    def ls(self):  # 显示
        cmd = input('输入命令>>>')
        try:
            action, parameter = cmd.replace(' ', '').split('|')
        except Exception:
            print('提示: ls | dirname1\dirname2\....')
            return False
        if action == 'ls':
            info = {'action': action, 'parameter': parameter, 'name': self.name}
            self.info_send_to_server(info)  # 发送给服务端
        else:
            print('提示: ls | dirname1\dirname2\....')
            return
        recv_data = self.sock.recv(1024)
        print(recv_data.decode())

    def mk(self):  # 新建
        cmd = input('输入命令>>>')
        try:
            action, parameter = cmd.replace(' ', '').split('|')
        except Exception:
            print('提示: mk | dirname1\dirname2\...')
            return False
        if action == 'mk' and not re.search(r'^\\|/', parameter):
            info = {'action': action, 'parameter': parameter, 'name': self.name}
            self.info_send_to_server(info)  # 发送给服务端
        else:
            print('提示: mk | dirname1\dirname2\...')
            return
        recv_data = self.sock.recv(1024)
        print(recv_data.decode())

    def rm(self):  # 删除
        cmd = input('输入命令>>>')
        try:
            action, parameter = cmd.replace(' ', '').split('|')
        except Exception:
            print('提示: rm | dirname1\dirname2\...')
            return False
        if action == 'rm':
            info = {'action': action, 'parameter': parameter, 'name': self.name}
            self.info_send_to_server(info)
        else:
            print('提示: rm | dirname1\dirname2\...')
            return
        data = self.sock.recv(1024)
        print(data.decode())

    def cd(self):  # 切换目录
        cmd = input('输入命令>>>')
        # noinspection PyBroadException
        try:
            action, parameter = cmd.replace(' ', '').split('|')
        except Exception:
            print("提示: cd | dirname1\\dirname2\\...")
            return False
        if action == 'cd':
            info = {'action': action, 'parameter': parameter, 'name': self.name}
            self.info_send_to_server(info)
        else:
            print("提示: cd | dirname1\\dirname2\\...")
            return False
        data = self.sock.recv(1024)
        print(data.decode())

    @staticmethod
    def logout():
        quit()

    def run(self):
        """入口函数、视图函数、反射函数"""
        option_list = [
            ("上传文件", "put"),
            ("下载文件", "get"),
            ("显示目录", "ls"),
            ("新建目录", "mk"),
            ("删除目录", "rm"),
            ("切换目录", "cd"),
            ("退出", "logout"),
        ]

        ret = self.login()  # 登录
        if ret:
            while True:
                print("\n-- 欢迎使用ftp客户端 --")
                for index, value in enumerate(option_list, start=1):
                    print("\t", index, value[0])

                choose = input('输入编号>>>:')
                if choose.isnumeric() and 1 <= int(choose) <= len(option_list):
                    if hasattr(self, option_list[int(choose) - 1][1]):
                        getattr(self, option_list[int(choose) - 1][1])()
                else:
                    print('编号输错了,请重新输入!')
        else:
            print('登录失败!')


if __name__ == "__main__":
    name = input('请输入账号>>>:')
    pwd = input('请输入密码>>>:')
    fc = FTPClient(name, pwd)
    fc.run()


posted @ 2021-04-26 16:17  李成果  阅读(48)  评论(0编辑  收藏  举报