黏包

黏包现象.

  • socket缓冲区

    • img

    • 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

      write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

      TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

      read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

    • 缓冲区特性

      • I/O缓冲区在每个TCP套接字中单独存在

      • I/O缓冲区在创建套接字时自动生成

      • 即是关闭套接字也会继续传送输出缓冲区中遗留的数据

      • 关闭套接字将丢失输入缓冲区中的数据

      • 缓冲区默认大小8K,可以通过setsocket()函数获取:

      • import socket
        server = socket.socket()
        server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  # 重用ip地址和端口
        server.bind(('127.0.0.1',8010))
        server.listen(3)
        print(server.getsockopt(socket.SOL_SOCKET,socket.SO_SNDBUF))  # 输出缓冲区大小
        print(server.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF))  # 输入缓冲区大小
        
        
  • 只有TCP有黏包现象,DUP永远不会黏包

    • 黏包问题主要还是因为接受方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的
  • 黏包产生情况

    • 接受方没有及时接收缓冲区的包,造成多个包接受(客户端发生了一段数据,服务端只收了一部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生黏包)
    • 发送端需要等缓冲区满才发送出去,造成黏包(发送数据时间间隔很短,数据也很小,会合到一起,产生黏包)
  • 黏包解决方法

    • 利用struct模块.将一个类型转成固定长度的bytes

      • img

      • import struct
        # 将一个数字转化成等长度的bytes类型。
        ret = struct.pack('i', 183346)
        print(ret, type(ret), len(ret))
        
        # 通过unpack反解回来
        ret1 = struct.unpack('i',ret)[0]
        print(ret1, type(ret1), len(ret1))
        
        
        # 但是通过struct 处理不能处理太大
        
        ret = struct.pack('l', 4323241232132324)
        print(ret, type(ret), len(ret))  # 报错
        

    方法1:low版

    • 将要发送的字节流总数按照固定的字节发送给接收端,然后再将数据发送过去,在用死循环将数据读完.

    • 服务端

      • import struct
        phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        
        phone.bind(('127.0.0.1', 8080))
        
        phone.listen(5)
        
        while 1:
            conn, client_addr = phone.accept()
            print(client_addr)
            
            while 1:
                try:
                    cmd = conn.recv(1024)
                    ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    correct_msg = ret.stdout.read()
                    error_msg = ret.stderr.read()
                    
                    # 1 制作固定报头
                    total_size = len(correct_msg) + len(error_msg)
                    header = struct.pack('i', total_size)
                    
                    # 2 发送报头
                    conn.send(header)
                    
                    # 发送真实数据:
                    conn.send(correct_msg)
                    conn.send(error_msg)
                except ConnectionResetError:
                    break
        
        conn.close()
        phone.close()
        
        
        # 但是low版本有问题:
        # 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
        # 2,通过struct模块直接数据处理,不能处理太大。
        
    • 客户端:

      • import socket
        import struct
        phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        
        phone.connect(('127.0.0.1',8080))
        
        
        while 1:
            cmd = input('>>>').strip()
            if not cmd: continue
            phone.send(cmd.encode('utf-8'))
            
            # 1,接收固定报头
            header = phone.recv(4)
            
            # 2,解析报头
            total_size = struct.unpack('i', header)[0]
            
            # 3,根据报头信息,接收真实数据
            recv_size = 0
            res = b''
            
            while recv_size < total_size:
                
                recv_data = phone.recv(1024)
                res += recv_data
                recv_size += len(recv_data)
        
            print(res.decode('gbk'))
        
        phone.close()
        
    • 旗舰版:自定制报头

      • 整体流程

        • 整个流程的大致解释:
          我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小啊之类的),然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。
          我们在网络上传输的所有数据 都叫做数据包,数据包里的所有数据都叫做报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的,看一下
          
          发送时:
          先发报头长度
          再编码报头内容然后发送
          最后发真实内容
          
          接收时:
          先手报头长度,用struct取出来
          根据取出的长度收取报头内容,然后解码,反序列化
          从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
          
      • 服务端

      • import socket
        import subprocess
        import struct
        import json
        phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        
        phone.bind(('127.0.0.1', 8080))
        
        phone.listen(5)
        
        while 1:
            conn, client_addr = phone.accept()
            print(client_addr)
            
            while 1:
                try:
                    cmd = conn.recv(1024)
                    ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    correct_msg = ret.stdout.read()
                    error_msg = ret.stderr.read()
                    
                    # 1 制作固定报头
                    total_size = len(correct_msg) + len(error_msg)
                    
                    header_dict = {
                        'md5': 'fdsaf2143254f',
                        'file_name': 'f1.txt',
                        'total_size':total_size,
                    }
                    
                    header_dict_json = json.dumps(header_dict) # str
                    bytes_headers = header_dict_json.encode('utf-8')
                    
                    header_size = len(bytes_headers)
                    
                    header = struct.pack('i', header_size)
                    
                    # 2 发送报头长度
                    conn.send(header)
                    
                    # 3 发送报头
                    conn.send(bytes_headers)
                    
                    # 4 发送真实数据:
                    conn.send(correct_msg)
                    conn.send(error_msg)
                except ConnectionResetError:
                    break
        
        conn.close()
        phone.close()
        
      • 客户端

        • import socket
          import struct
          import json
          phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
          
          phone.connect(('127.0.0.1',8080))
          
          
          while 1:
              cmd = input('>>>').strip()
              if not cmd: continue
              phone.send(cmd.encode('utf-8'))
              
              # 1,接收固定报头
              header_size = struct.unpack('i', phone.recv(4))[0]
              
              # 2,解析报头长度
              header_bytes = phone.recv(header_size)
              
              header_dict = json.loads(header_bytes.decode('utf-8'))
              
              # 3,收取报头
              total_size = header_dict['total_size']
              
              # 3,根据报头信息,接收真实数据
              recv_size = 0
              res = b''
              
              while recv_size < total_size:
                  
                  recv_data = phone.recv(1024)
                  res += recv_data
                  recv_size += len(recv_data)
          
              print(res.decode('gbk'))
          
          phone.close()
          
原文地址:https://www.cnblogs.com/W-Y-C/p/11201724.html