网络编程笔记(3)——解决黏包问题

内容目录:

解决黏包问题:

  • 提前发送文件的大小
  • 使用struct模块
  • 定制报文

内容详细:

1.发送之前先发文件大小

优点:

  • 确定了到底要接收多大的数据
  • 要在文件中配置一个配置项:每次recv文件的大小,buffer = 4096(建议使用这个)
  • 当我们要发送大数据时,明确告知接收方要发送多大的数据,以便接收方能准确的接收到所有的数据
  • 多用在文件传输的过程中:
    • 大文件的传输,一定是按照字节读,每次读固定大小的字节
    • 传输的过程中,一边读一边传;接收端:一边收一边写入文件

缺点:

  • 多了一次接收交互

注意:

  • send 或 sendto超过一定范围的时候都会报错(设备默认的范围大小)
  • 写文件传输的时候必须考虑程序的内存管理
# server端:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()

conn,addr = sk.accept()

while True:
    cmd = input('>>>')
    if cmd == 'q':
        conn.send(b'q')
        break
    conn.send(cmd.encode('gbk'))
    num = conn.recv(1024).decode('utf-8')   #接收到了client端发过来的字节总数
    conn.send(b'ok')
    res = conn.recv(int(num)).decode('gbk') #获取了接收的总字节数,设置进要接收的值里面,就可以动态的感知要发送的数据了
    print(res)

conn.close()
sk.close()

#Client端:
import socket
import subprocess

sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
    cmd = sk.recv(1024).decode('gbk')   #因为Windows传输都为GBK编码的
    if cmd == 'q':
        break
    res = subprocess.Popen(cmd,shell=True,          #PIPE为管道/队列,只能取一次,取完就结束了
                           stdout=subprocess.PIPE,  #stdout为命令的输出
                           stderr=subprocess.PIPE)  #stderr为命令错误的提示信息
    std_out = res.stdout.read() #从队列中取出命令
    std_err = res.stderr.read() #从队列中取出错误命令信息
    sk.send(str(len(std_out)+len(std_err)).encode('utf-8'))  #计算要发送的字节总数并发送过去
    sk.recv(4096)       #一般不要超过这个数字,否则会超过设备内存
    sk.send(std_out)
    sk.send(std_err)
sk.close()

2.使用struct模块

  • 什么是固定长度的bytes

  • 为什么要固定长度bytes?

    • 因为发送数据前,先发数据的长度,接收方接收长度。如果固定长度的话就省略了一步收发数据长度的次数
    #发送端需要借助struct模块
    import struct
    ret = struct.pack('i',2048) #'i'代表int,就是即将要把一个数字转换成固定长度的bytes类型
    print(ret)
    
    #接收端:
    num = struct.unpack('i',ret)	#此时num为元组tuple类型
    print(num[0])	#获取数据长度2048
    #这样接收方只需要接收固定的长度就可以获取整个数据的长度了
    
    # 使用struct模块收发信息
    # Server端:
    import socket
    import struct
    sk = socket.socket()
    sk.bind(('127.0.0.1',8080))
    sk.listen()
    
    conn,addr = sk.accept()
    
    while True:
        cmd = input('>>>')
        if cmd == 'q':
            conn.send(b'q')
            break
        conn.send(cmd.encode('gbk'))
        num = conn.recv(4)   #接收到了client端发过来的4个字节数(二进制),不管多大始终是4个字节
        num = struct.unpack('i',num)[0]   #把4个字节数二进制unpack,得到总字节数:num
        res = conn.recv(int(num)).decode('gbk') #获取了接收的总字节数,设置进要接收的值里面,就可以动态的感知要发送的数据了
        print(res)
    
    conn.close()
    sk.close()
    
    # Client端:
    import socket
    import struct
    import subprocess
    
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    while True:
        cmd = sk.recv(1024).decode('gbk')   #因为Windows传输都为GBK编码的
        if cmd == 'q':
            break
        res = subprocess.Popen(cmd,shell=True,          #PIPE为管道/队列,只能取一次,取完就结束了
                               stdout=subprocess.PIPE,  #stdout为命令的输出
                               stderr=subprocess.PIPE)  #stderr为命令错误的提示信息
        std_out = res.stdout.read() #从队列中取出命令
        std_err = res.stderr.read() #从队列中取出错误命令信息
        len_num = len(std_out) + len(std_err)#计算要发送的字节总数并发送过去
        num_by = struct.pack('i',len_num)	#使用模块pack进去总字节数,发送过去的始终是4个字节
        sk.send(num_by)     #发送了4个字节
        sk.send(std_out)
        sk.send(std_err)
    sk.close()
    

3.定制报文报头

  • 我们在网络上传输的所有数据,都叫数据包

  • 数据包里的所有数据,都叫报文

  • 报文里不只有发送的数据,还有ip地址,MAC地址,端口号

  • 所有的报文都有报头

  • 可以自定义报文头

    head = {'filename':'test','filesize':409600,'filetype':'txt','filepath':r'userin'}
    # 先发送报头的长度              #接收4个字节的报头长度
    # send(head)                   #根据这4个字节获取报头
    # send(file)发送数据文件        #从报头中获取filesize,然后设置大小接收文件
    
  • 例子:上传文件

    # Server端:
    # 实现一个大文件的上传或下载
    # 配置文件,IP地址,端口号
    import socket
    import struct
    import json
    sk = socket.socket()
    sk.bind(('127.0.0.1',8090))
    sk.listen()
    buffer = 1024       #注意,如果设置过大会设备缓存会出错,导致文件接收不完整
    
    conn,addr = sk.accept()
    #接收
    head_len = conn.recv(4)
    head_len = struct.unpack('i',head_len)[0]
    json_head = conn.recv(head_len).decode('utf-8')
    head = json.loads(json_head)
    filesize = head['filesize']
    
    with open(head['filename'],'wb')as f:
        while filesize:
            if filesize >= buffer:
                content = conn.recv(buffer)
                f.write(content)
                filesize -= buffer
            else:
                content = conn.recv(filesize)
                f.write(content)
                filesize = 0
            print('====>',len(content))
        print(filesize)
    conn.close()
    sk.close()
    print(head['filesize'])
    
    # Client端:
    # 发送端
    import os
    import json
    import struct
    import socket
    sk = socket.socket()
    sk.connect(('127.0.0.1',8090))
    buffer = 1024
    
    # 发送文件
    head = {'filename':'02 python fullstack s9day47-web框架的本质.mp4',
            'filepath':r'D:硬盘文件老男孩pythonPython全栈9期(第02部分):并发编程+数据库+前端3. 前端开发day47',
            'filesize':None}
    filepath = os.path.join(head['filepath'],head['filename'])
    filesize = os.path.getsize(filepath)
    head['filesize'] = filesize
    json_head = json.dumps(head)    #字典转成了字符串
    bytes_head = json_head.encode('utf-8')  #字符串转bytes
    
    #计算head的长度
    head_len = len(bytes_head)      #报头的长度
    pack_len = struct.pack('i',head_len)
    sk.send(pack_len)           #先发报头的长度
    sk.send(bytes_head)         #再发送bytes类型的报头
    with open(filepath,'rb')as f:
        while filesize:
            if filesize >= buffer:
                content = f.read(buffer)        #每次读出来的内容
                sk.send(content)
                filesize -= buffer
            else:
                content = f.read(filesize)
                sk.send(content)
                filesize = 0
    
    sk.close()
    
原文地址:https://www.cnblogs.com/lynlearnde/p/13471642.html