欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

解决粘包问题

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

解决粘包问题

在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()