解决粘包问题

解决粘包问题

在OSI七层模型中,我们可以看到,数据包从应用层产生,会在应用层生成一个头文件+数据的包传递给下一层,在下一层中它会认识这个包就是一个整体,然后会在这上面再重新添加一个包,直到物理层发送电信号,到了服务端一层层的解包,(https://www.processon.com/view/5b124aa3e4b068c2520f1292)

那么如果我们想解决粘包的问题,首先最重要的就是要知道这个数据包到底有多大,那么应该怎么知道呢?

struct模块

struct模块是Python中内置模块,它用来在C语言中的结构体与Python中的字符串之间进行转换,数据一般来自网络或文件

常用方法

struct.pack(fmt,v1)
返回的是一个字符串,是参数按照fmt数据格式组合而成

struct.unpack(fmt.string)
按照给定数据格式解开(通常都是由struct.pack进行打包)数据,返回值是一个tuple

格式符

具体请参考:(https://blog.csdn.net/djstavav/article/details/77950352)

我们目前能用到的就是fmt=i,固定返回的是4个bytes,那么我们就可以和OSI七层模型一样,在还没有发送自己的真实数据之前,先告诉客户端我会发送多大的数据,也就是先发一个头部文件过去,然后客户端就开始进入循环,一直把这个数据接收完才可以,看以下代码

简单版本

# 服务端

# 导入模块
import socket
import subprocess
import struct

# 创建socket对象,监听连接
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8009))
server.listen(5)

while True:
    # 获取客户端连接对象以及IP地址
    conn,client_addr = server.accept()
    print('客户端地址:',client_addr)
    try:
        cmd = conn.recv(1024)  # 接收1024bytes
        obj = subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)

        stdout = obj.stdout.read()
        stderr = obj.stderr.read()
        total_size = len(stdout) + len(stderr)  # 统计总大小
        
        # 制作固定长度的报头
        header = struct.pack('i',total_size)
        # 发送报头给客户端
        conn.send(header)
        # 发送真实的数据
        conn.send(stdout)
        conn.send(stderr)
    except ConnectionResetError as e:  # 如果客户端突然断开连接,则break
        break
    conn.close()  # 断开本次连接
server.close()

# 客户端
import socket
import struct
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8009))  # 连接服务器地址

while True:
    cmd = input('>>>').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))  # 以UTF-8的编码发送

    header = client.recv(4)  # 接收头部固定4个bytes

    total_size = struct.unpack('i',header)[0]  # 获取真实数据的长度

    recv_size = 0  # 接收的大小
    recv_data = b''  # 字符串的拼接
    while recv_size < total_size:  # 如果接收的大小<总长度
        res = client.recv(1024)  # 接收bytes
        recv_data += res  # 拼接
        recv_size += len(res)  # 获取当前已接收真实数据的长度
    print(recv_data.decode('GBK'))  # GBK解码
client.close()  # 关闭本地连接

终极版本

# 服务端
# 导入模块
import socket
import subprocess
import json
import struct

# 创建对象、绑定、监听
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8889))
server.listen(5)
print('starting......')

# 与客户端建立连接
while True:
    conn,addr = server.accept()
    print('客户端地址:',addr)
    while True:
        try:
            # 1.收命令
            cmd = conn.recv(1024)
            # 2.执行命令,得到命令的结果
            obj = subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 3.把命令的结果返回给客户端

            # 3.1制作固定长度的报头
            header_dict = {
                'filename':'a.txt',
                'md5':'dd0a833f9512f1145d9f6869c42ec902',
                'total_size':len(stdout)+len(stderr)
            }
            # print(len(header_dict))
            header_json = json.dumps(header_dict)  # 序列化文件
            header_bytes = header_json.encode('utf-8')  # 转换成为二进制字节码
            # print(header_bytes)
            # 3.2先发送报头的长度
            header = struct.pack('i',len(header_bytes))  # 将转换的字节码通过pack进行打包得到4个bytes
            print(len(header))
            conn.send(header)
            conn.send(header_bytes)
            print('已经发送文件')
            # 3.3再发送真实的数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()
server.close()

# 客户端
# 导入模块
import socket
import struct
import json

# 创建套接字对象、连接服务器端
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8889))

while True:
    # 1.发送命令
    cmd = input('>>>>>').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))
    # 2.拿到命令的结果并打印
    # 2.1先收报头的长度
    obj = client.recv(4)  # 接收4个bytes
    header_size = struct.unpack('i',obj)[0]  # 获取报头的长度,即header_dict的长度
    # 2.2再收报头
    header_bytes = client.recv(header_size)  # 将header_dict收到后进行转码,得到了json文件
    # 2.3从报头中解析出对真实数据的描述信息
    header_json = header_bytes.decode('utf-8')
    # print(len(header_json))
    print('client已经收到文件')
    header_dict = json.loads(header_json)  # 从内存中拿到json文件
    print(header_dict)
    total_size = header_dict['total_size']  # 获取真实数据的大小
    # 3.接收真实的数据
    recv_size = 0  # 已经接收的数据
    recv_data = b''  # 字符拼接
    while recv_size < total_size:
        res = client.recv(1024)
        recv_data += res
        recv_size += len(res)
    print(recv_data.decode('gbk'))
client.close()

FTP文件传输

所谓的FTP文件传输,就是输入一条get 1.txt命令,然后自动从服务器端下载了1.txt文件,代码和上面的比较相似

简单版本

说明一点:本操作在linux系统上操作,windows端的pycharm连接本地的Linux服务器,在服务器内执行操作,so请注意文件路径

# 服务端
import socket
import os
import json
import struct

# 设置路径,以后要写到配置文件中的
SHARE_DIR = r'/py_study/day21-粘包/文件传输test/server/share'

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',10000))
server.listen(5)

while True:
    conn,addr = server.accept()
    print('客户端地址:',addr)
    while True:
        try:
            # 1.收命令
            obj = conn.recv(1024)  # 收到b'get 1.mp4'
            if not obj:break
            # 2.解析命令,提取相应的命令参数
            cmd = obj.decode('utf-8').split()  # ['get','1.mp4']
            filename = cmd[1]  # 文件名
            # 3.以读的方式打开文件,读取内容发送给客户端
            # 3.1制作固定长度的报头
            header_dict = {
                'filename':filename,
                'md5':'xxxxxxxx',
                'file_size':os.path.getsize(r'%s/%s'%(SHARE_DIR,filename))
            }
            header_json = json.dumps(header_dict)
            header_bytes = header_json.encode('utf-8')
            # 3.2发送报头的长度
            conn.send(struct.pack('i',len(header_bytes)))
            # 3.3再发送报头
            conn.send(header_bytes)
            # 4.发送真实的数据
            with open('%s/%s'%(SHARE_DIR,filename),'rb') as f:
                for line in f:
                    conn.send(line)  # 逐行发送
        except ConnectionResetError:
            break
    conn.close()
server.close()

# 客户端
import socket
import struct
import json

DOWNLOAD_DIR = r'/py_study/day21-粘包/文件传输test/client/download'
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',10000))

while True:
    # 1.发送命令
    cmd = input('>>>').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))
    # 2.以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户端的新文件
    # 2.1先接收报头的长度
    obj = client.recv(4)
    header_size = struct.unpack('i',obj)[0]
    # 2.2再接收报头
    header_bytes = client.recv(header_size)
    # 3从报头中解析出对真实数据的描述信息
    header_json = header_bytes.decode('utf-8')
    header_dict = json.loads(header_json)
    print(header_dict)
    total_size = header_dict['file_size']
    filename = header_dict['filename']
    # 4.接收真实的数据
    with open('%s/%s'%(DOWNLOAD_DIR,filename),'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            res = client.recv(1024)
            f.write(res)
            recv_size += len(res)
            print('总大小:%s        已下载大小:%s'%(total_size,recv_size))

client.close()

终极版本

# 服务端
import socket
import os
import json
import struct

# 设置路径,以后要写到配置文件中的
SHARE_DIR = r'/py_study/day21-粘包/文件传输优化test/server/share'

def get(cmd,conn):
    filename = cmd[1]  # 文件名
    # 3.以读的方式打开文件,读取内容发送给客户端
    # 3.1制作固定长度的报头
    header_dict = {
        'filename': filename,
        'md5': 'xxxxxxxx',
        'file_size': os.path.getsize(r'%s/%s' % (SHARE_DIR, filename))
    }
    header_json = json.dumps(header_dict)
    header_bytes = header_json.encode('utf-8')
    # 3.2发送报头的长度
    conn.send(struct.pack('i', len(header_bytes)))
    # 3.3再发送报头
    conn.send(header_bytes)
    # 4.发送真实的数据
    with open('%s/%s' % (SHARE_DIR, filename), 'rb') as f:
        for line in f:
            conn.send(line)

def put():
    pass

def run():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 11115))
    server.listen(5)

    while True:
        conn,addr = server.accept()
        print('客户端地址:',addr)
        while True:
            try:
                # 1.收命令
                obj = conn.recv(1024)  # 收到b'get 1.mp4'
                print('收到数据')
                if not obj:break
                # 2.解析命令,提取相应的命令参数
                cmds = obj.decode('utf-8').split()  # ['get','1.mp4']
                if cmds[0] == 'get':
                    get(cmds,conn)
                elif cmds == 'put':
                    pass

            except ConnectionResetError:
                break
        conn.close()
    server.close()

if __name__ == '__main__':
    run()
    
# 客户端
import socket
import struct
import json

DOWNLOAD_DIR = r'/py_study/day21-粘包/文件传输优化test/client/download'



def get(client,cmds):

        # 2.以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户端的新文件
        # 2.1先接收报头的长度
        print('进入get方法')
        obj = client.recv(4)
        print('发送数据成功')
        header_size = struct.unpack('i',obj)[0]
        print('解包完成')
        # 2.2再接收报头
        header_bytes = client.recv(header_size)
        print('再次接收报头成功')
        # 3从报头中解析出对真实数据的描述信息
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        print(header_dict)
        total_size = header_dict['file_size']
        filename = header_dict['filename']
        # 4.接收真实的数据
        with open('%s/%s'%(DOWNLOAD_DIR,filename),'wb') as f:
            recv_size = 0
            while recv_size < total_size:
                res = client.recv(1024)
                f.write(res)
                recv_size += len(res)
                print('总大小:%s        已下载大小:%s'%(total_size,recv_size))

def put():
    pass

def run():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1',11115))
    while True:
        # 1.发送命令
        cmd = input('>>>').strip()
        if not cmd:continue
        client.send(cmd.encode('utf-8'))

        cmds = cmd.split()
        if cmds[0] == 'get':
            print('get方法')
            get(client,cmds)
            print('get方法执行完成')
        elif cmds[0] == 'put':
            pass
    client.close()
if __name__ == '__main__':
    run()
原文地址:https://www.cnblogs.com/xiaoyafei/p/9149462.html