学习笔记-文件上传粘包问题

实现模拟文件上传文件下载和文件查看功能

模拟出服务端和客户端

客户端:

客户端具有文件查看功能,文件上传功能,以及从服务端下载文件功能

文件查看功能是客户端进行输入指定的字符,将其传送给服务端,服务端解析客户端输入的命令并执行相关的操作,将结果返回给客户端

文件下载功能是客户端输入指定字符以及文件名,传送字符给服务端,服务端解析字符后进行打开文件并传输回客户端,客户端接收到数据后进行文件写入的操作,从而完成文件下载功能

文件上传功能是客户端打开文件,将文件名和文件内容以数据的形式发送出去,服务端接收数据,并在服务端进行文件写入

from socket import *
import pickle


def post_file(cmd):
    # 先对输入的命令进行判断
    post = cmd.split(':')
    # 先把文件名传过去
    # 创建对象进行发送信息
    client = socket(AF_INET, SOCK_STREAM)
    # 创建连接
    client.connect(('172.16.17.154', 8888))
    # 发送信息 先把文件名传过去,为了防止粘包,在文件名后加一个字符区分
    file_name = 'P:' + post[1] + '$@@$'
    client.send(file_name.encode('utf-8'))  # 就直接传过去是二进制
    try:
        with open('./' + post[1], 'rb') as f:
            # 进行文件读取,将信息发送过去
            data = f.read(1024)
            while data:
                client.send(data)
                data = f.read(1024)
        print('客户端文件读取完成')
    except:
        print('语法错误,请重新输入')
    client.close()


def down_load(cmd):
    # 创建对象
    client = socket(AF_INET, SOCK_STREAM)
    # 创建连接
    client.connect(('172.16.17.16', 8888))
    # 发送信息
    client.send(cmd.encode('utf-8'))
    # 接收服务器返回的信息
    data = pickle.loads(client.recv(1024))
    if data == 1:
        print('下载失败')
    else:
        # 如果返回的不是1,则表示服务器没有异常,这里直接写文件
        with open('test.txt', 'wb') as f:
            # 把接收到的数据直接写进去
            while True:
                f.writelines(data)
                try:
                    data = pickle.loads(client.recv(1024))
                except:
                    break
        # 写完后打印成功
        print('下载成功')
    client.close()


def check(cmd):
    # 创建对象
    client = socket(AF_INET, SOCK_STREAM)
    # 创建连接
    client.connect(('172.16.17.16', 8888))
    # 发送信息
    client.send(cmd.encode('utf-8'))
    # 接收服务器返回的信息并打印
    print('查询结果')
    print(pickle.loads(client.recv(1024)))
    client.close()


def main():
    # 自定义输入协议
    cmd = input('请输入内容').strip()
    while True:
        if cmd[0].upper() == 'L':
            check(cmd)
        elif cmd[0].upper() == 'G':
            down_load(cmd)
        elif cmd[0].upper() == 'P':
            post_file(cmd)
        elif cmd == 'quit':
            print('退出')
            break
        cmd = input('请输入内容').strip()


if __name__ == '__main__':
    main()

服务端:

服务端需要解析客户端输入的内容,那么服务端就应该先于客户端拟定协议,当客户端输入的内容满足协议要求的则进行相关的操作,如果不满足则认为是错误请求

from socket import *
import os
import pickle


def main():
    # 创建对象
    server = socket(AF_INET, SOCK_STREAM)
    # 端口立即释放问题
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    # 开启服务器
    server.bind(('172.16.17.154', 8888))
    # 开启监听模式
    server.listen(10)
    # 循环监听
    while True:
        # 创建分器
        new_server, client_address = server.accept()
        # 接收客户端发送的数据
        data = new_server.recv(1024)
        # 拆解data,文件上传的时候可能粘包,将数据传入函数后再处理
        not_pure_data = data
        data = data.decode('utf-8')
        # 进行接收数据判断
        if data[0].upper() == 'L':
            check(new_server)
        elif data[0].upper() == 'P':
            # 表示要上传文件
            # 接收上传过来的文件内容,文件可能很大,需要循环接收
            post_file(new_server, not_pure_data)
        elif data[0].upper() == 'G':
            # 表示要下载文件
            down_load(new_server, data)
        # print('命令解析失败')


def down_load(new_server, data):
    # 表示要下载文件
    print(data)
    # 解析发送过来的信息
    file_name = data.split(":")
    # file_name[1]就是需要查询文件名
    # print(file_name[1])
    # 打开文件进行查询并传回去
    try:
        with open('./' + file_name[1], 'rb') as f:
            # 读取文件并保存
            save_file = f.readlines()
            # 发送回去
            new_server.send(pickle.dumps(save_file))
            # 关闭连接
            new_server.close()
    except:
        # 如果失败或者文件不存在则返回1
        new_server.send(pickle.dumps(1))
        # 关闭连接
        new_server.close()


def post_file(new_server, not_pure_data):
    # print('进入写入')
    # 从对象上获取数据
    file_name = not_pure_data.split(b'$@@$')[0].decode('utf-8').split(':')[1]
    print(file_name)
    # 进行文件写入
    with open('./' + file_name, 'wb') as f:
        # 先把粘包的部分写入
        print('文件已经打开')
        try:
            new_data = not_pure_data.split(b'$@@$')[1]
        except:
            new_data = b''
        # 捕获
        f.write(new_data)
        while True:
            write_data = new_server.recv(1024)
            if write_data:
                f.write(write_data)
            else:
                # 表明已经写完了
                print('文件写入完成')
                break
    # 完成后关闭
    new_server.close()


def check(new_server):
    # 表示需要进行查询
    now_dir = os.listdir()
    # 对数据进行序列化并发送回去
    new_server.send(pickle.dumps(now_dir))
    # 关闭连接
    new_server.close()


if __name__ == '__main__':
    main()

但是在客户端执行文件上传的时候会出现粘包的情况

粘包的原因:

之所以会出现粘包,实际上在客户端一旦连接成功后,服务端就会分配一个缓存区给客户端,这个缓存区就是用来暂时存储客户端传输给服务端的数据,就算服务端还没有执行

new_server, client_address = server.accept()

这个缓存区也已经创建完成,此时客户端已经可以向服务端发送数据,只是服务端还没有从这个缓存区里面取出数据而已,那么客户端发送的数据并不是按照指定的大小一块一块的发送的,数据在传输过程中实际上被分解成为无数的小包,以队列的形式先进先出的方式进行传输到缓存区,那么服务端进行数据的取出也是按照顺序进行取出,

问题的关键来了,数据先传过来的是文件名字,你无法知道每次客户端传入的文件名字是几个字节,这个是服务端不可控制的,那么服务端每次取数据的时候是按照1024个字节进行取出,也就是说取出的数据里面除了包含文件名,还包含了一部分文件内容,因此没法将文件名和文件内容进行区分.

粘包的解决:

这里就需要进行标识,使用一个或者几个特殊的字符进行隔断文件名和文件内容,当客户端发送的文件名后面跟随一个隔断字符的时候,服务端进行第一次取出数据先假定一定发生了粘包,那么数据中就存在隔断的特殊字符,这里只需要将数据从特殊字符处进行隔断拆分成一个列表,那么这个列表的0索引位置就是文件名,1索引位置就是文件数据,从而直接达到了区分的效果

如果第一次取出的时候就没有发生粘包,当然概率很小,那么在数据拆分的步骤就会报异常,只需要捕获这个异常就行了,概率更低的事情是如果正好设定的是几个特殊字符进行分隔,正好取数据的时候在分隔字符中间,那么这个粘包也就没有办法了识别了.

当然特殊字符可能在文件内容中也出现,所以这里的特殊字符要自行限定好,也可以有其他的解决方法,这里就先提供这种最容易实现最简单的解决方法

猜你喜欢

转载自blog.csdn.net/weixin_43959953/article/details/84983272