zoukankan      html  css  js  c++  java
  • 粘包

    粘包

    • 粘包现象:

      • TCP属于长连接,当服务端与一个客户端进行了连接以后,其他客户端需要(排队)等待.若服务端想要连接另一个客户端,必须首先断开与第一个客户端的连接。

      • 缓冲区:它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区

      • 为什么引入缓冲区:高速设备与低速设备的不匹配,势必会让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区,也就是一个台阶,怕低速的跟不上

      • 缓冲区(buffer)的作用:

        • 1.可以解除两者的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率。如:我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印。
        • 可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。
      • 区别于缓存区(cache):CPU的Cache,它中文名称是高速缓冲存储器,读写速度很快,几乎与CPU一样。由于CPU的运算速度太快,内存的数据存取速度无法跟上CPU的速度,所以在cpu与内存间设置了cache为cpu的数据快取区。当计算机执行程序时,数据与地址管理部件会预测可能要用到的数据和指令,并将这些数据和指令预先从内存中读出送到Cache。一旦需要时,先检查Cache,若有就从Cache中读取,若无再访问内存,现在的CPU还有一级cache,二级cache。

        img

      • 总结:也就是说缓冲区是内存中的对应的输入输出,而缓存区是cpu中的。

      • 每个socket(套接字)被创建后,都会分配两个缓冲区: 输入缓冲区和输出缓冲区。

      • 在windows复制路径时,从后往前复制路径会自动添加看不见的符号,从前往后就不会有问题

        •  
           
           
           
           
           
           
           
          1、write()/send()并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器. 一旦将数据写入到缓冲区,函数就已经完成任务可以成功返回了,而不用去考虑数据何时被发送到网络,也不用去考虑数据是否已经到达目标机器,因为这些后续操作都是TCP协议负责的事情.
          2、TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况,当前线程是否空闲等诸多因素,不由程序员控制.
          3、read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取
          4、这些I/O缓冲区特性可整理如下:
              1). I/O缓冲区在每个TCP套接字中单独存在
              2). I/O缓冲区在创建套接字时自动生成
              3). 即使关闭套接字也会继续传送输出缓冲区中遗留的数据
              4). 关闭套接字将丢失输入缓冲区中的数据
              5). 输入/输出缓冲区的默认大小一般是8K(了解:可以通过getsockopt()函数获取)
           
    • 粘包现象的原因:(UDP不存在粘包)

      • TCP采用优化方法:拆包机制和合包机制
      • 接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
      • 发送数据时间间隔很短,数据也很小,会合到一起,产生粘包
    • 粘包现象的模拟:

      • 发送方连续发送较小的数据,并且每次发送之间的时间间隔很短,此时,两个消息在输出缓冲区黏在一起了.原因是TCP为了传输效率,做了一个优化算法(Nagle),减少连续的小包发送(因为每个消息被包裹以后,都会有两个过程:组包和拆包,这两个过程是极其消耗时间的,优化算法Magle的目的就是为了减少传输时间)。

         
         
         
        x
         
         
         
         
        #服务端
        import socket
        server = socket.socket()
        ip_port = ("192.168.15.28", 8001)
        server.bind(ip_port)
        server.listen()
        conn, addr = server.accept()
        # 连续接收两次消息
        from_client_msg1 = conn.recv(1024).decode("utf-8")
        print("第一次接收到的消息>>>", from_client_msg1)
        from_client_msg2 = conn.recv(1024).decode("utf-8")
        print("第二次接收到的消息>>>", from_client_msg2)
        conn.close()
        server.close()
        #客户端
        import socket
        client = socket.socket()
        server_ip_port = ("192.168.15.28", 8001)
        client.connect(server_ip_port)
        # 连续发送两次消息
        client.send('Hello'.encode('utf-8'))
        client.send('World'.encode('utf-8'))
        client.close()
        #结果
        #第一次接收到的消息>>> HelloWorld
        #第二次接收到的消息>>>
         

    粘包的解决方案:

    • 方案一、粘包问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕"如何让发送端在发送数据前,把自己将要发送的字节流总长度让接收端知晓"

      •  
         
         
        xxxxxxxxxx
         
         
         
         
        #解决步骤:
        #a. 发送端把"数据长度"传输给接收端
        #b. 接收端把"确认信息"传输给发送端
        #c. 发送端把"全部数据"传输给接收端
        #d. 接收端使用一个死循环接收完所有数据.
        #服务端代码
        import socket
        import subprocess
        server = socket.socket()
        ip_port = ('192.168.15.28',8001)
        server.bind(ip_port)
        server.listen()
        conn,addr = server.accept()
        while 1:
            from_client_cmd = conn.recv(1024).decode('utf-8')   # a.接收来自客户端的cmd指令
            sub_obj = subprocess.Popen(
                from_client_cmd,    # 客户端的指令
                shell=True,    # 使用shell,就相当于使用cmd窗口
                stdout=subprocess.PIPE,  # 标准错误输出,凡是输入错误指令,错误指令输出的报错信息就会被它拿到
                stderr=subprocess.PIPE,
            )
            server_cmd_msg = sub_obj.stdout.read()              # b.拿到cmd指令返回值 --> stdout接受到的返回值是bytes类型的,并且windows系统的默认编码为gbk
            cmd_msg_len = str(len(server_cmd_msg))              # c.拿到返回值的长度
            print("cmd返回的正确信息的长度>>>",cmd_msg_len)
            conn.send(cmd_msg_len.encode('gbk'))                # c.把"长度"传输给客户端
            from_client_ack = conn.recv(1024).decode('utf-8')   # d.拿到"确认信息"
            if from_client_ack == "确认":
                conn.send(server_cmd_msg)                   # e.把"cmd指令返回值"传输给客户端
            else:
                continue
                
                
        #客户端
        import socket
        client = socket.socket()
        server_ip_port = ('192.168.15.28',8001)
        client.connect(server_ip_port)
        while 1:
            cmd = input('请输入要执行的指令>>>')                           # a.用户输入cmd指令
            client.send(cmd.encode('utf-8'))                      # b.把"cmd指令"传输给服务端
            from_server_msglen = int(client.recv(1024).decode('gbk'))      # c.接收cmd指令返回值的"字节流长度"
            print('接收到的信息长度是>>>', from_server_msglen)
            client.send('确认'.encode('utf-8'))                   # d.把"确认信息"传输给服务端
            from_server_stdout = client.recv(from_server_msglen).decode('gbk')   # e.设置最大可接收数据量,同时接收"cmd指令返回值"
            print('接收到的指令返回值是>>>', from_server_stdout)
         
    • 方案二、通过struct模块将数据实体(要传输的数据)的长度打包成一个"4bytes字符串",并将其传输给接收端.接收端取出这个"4bytes字符串",对其进行解包,解包后的内容就是"数据实体的长度",接收端再通过这个长度来继续接收数据实体.(注意粘包的两个send要写一起,否则缓存区会溢出,关闭连接)

      •  
         
         
        xxxxxxxxxx
         
         
         
         
        #struct模块中最重要的两个函数是:
         #pack() --   具有"打包"功能,struct.pack(format, values)  将value打包成bytes的4个
         #unpack() -- 具有"解包"功能  struct.unpack(format, bytes) 通过bytes反解处具体的value
        
        
         #解决流程
            #a.拿到数据实体的长度
            #b.将长度打包成"4bytes字符串"
            #c.将"4bytes字符串"发送给客户端
            #d.发送数据实体
            
        #服务端
        import socket
        import subprocess
        import struct
        server  = socket.socket()
        ip_port = ("127.0.0.1", 8001)
        server.bind(ip_port)
        server.listen()
        conn, addr = server.accept()
        while 1:
            from_client_cmd = conn.recv(1024).decode("utf-8")
            print("来自客户端的指令是>>>")
            # 通过subprocess模块拿到指令的返回值
            sub_obj = subprocess.Popen(
               from_client_cmd,         # 客户端的指令
               shell=True,
               stdout=subprocess.PIPE,  # 标准输出:接收正确指令的执行结果
               stderr=subprocess.PIPE,  # 标准错误输出:接收错误指令的执行结果
            )
            # 通过stdout拿到正确指令的执行结果,即需要发送的"数据实体"
            server_cmd_msg = sub_obj.stdout.read()
            # a.拿到数据实体的长度
            cmd_msg_len = len(server_cmd_msg)
            # b.将长度打包成"4bytes字符串"
            msg_len_stru = struct.pack('i',cmd_msg_len)
            # c.将"4bytes字符串"发送给客户端
            conn.send(msg_len_stru)
            # d.发送数据实体 --> sendall() 循环发送数据,直到数据全部发送成功
            conn.sendall(server_cmd_msg)
         #客户端
        import socket
        import struct
        client = socket.socket()
        server_ip_port = ("127.0.0.1", 8001)
        client.connect(server_ip_port)
        while 1:
            cmd = input("请输入要执行的指令>>>")
            client.send(cmd.encode("utf-8"))
            # a.接收打包后的"4bytes字符串"
            from_server_msglen = client.recv(4)
            # b.解包,拿到"数据实体的长度",即unpack_msglen
            unpack_msglen = struct.unpack('i', from_server_msglen)[0]
            # c.循环接收数据实体,通过"数据实体的长度"来确定跳出循环的条件
            recv_msg_len = 0    # 统计"数据长度"
            all_msg = b''       # 统计"数据实体"
            while recv_msg_len < unpack_msglen:
                every_recv_data = client.recv(1024)
                # 将每次接收到的"数据实体"进行拼接
                all_msg += every_recv_data
                # 将每次接收到的"数据实体的长度"进行累加
                recv_msg_len += len(every_recv_data)
            print(all_msg.decode("gbk"))
         

    二、粘包时的问题:

    1、在每次文件传送中,如果每次发送的数据长度是大于1460bytes的就会出现最后的数据总量缺少一部分,原因是涉及到网络带宽,一般是1500bytes,但是由于系统一些报头文件也需要一些,所以只有1460bytes可以,解决方法是每次文件只减少len(content)

    粘包的应用

    • 大文件的传输

       
       
       
      xxxxxxxxxx
       
       
       
       
      #服务端
      import json
      import struct
      import socket
      sk = socket.socket()
      sk.bind(('127.0.0.1',9001))
      sk.listen()
      conn,addr = sk.accept()
      len_bytes = conn.recv(4)
      num = struct.unpack('i',len_bytes)[0]
      str_dic = conn.recv(num).decode('utf-8')
      dic = json.loads(str_dic)
      with open(dic['filename'],'wb') as f:
          while dic['filesize']:
              content = conn.recv(2048)
              f.write(content)
              dic['filesize'] -= len(content)
              
      #客户端
      import os
      import json
      import struct
      import socket
      sk = socket.socket()
      sk.connect(('127.0.0.1',9001))
      file_path = input('>>>')
      filename = os.path.basename(file_path)
      filesize = os.path.getsize(file_path)
      dic = {'filename':filename,'filesize':filesize}
      bytes_dic = json.dumps(dic).encode('utf-8')
      len_bytes = struct.pack('i',len(bytes_dic))
      sk.send(len_bytes)
      sk.send(bytes_dic)
      with open(file_path,'rb') as f:
          while filesize > 2048:
              content = f.read(2048)
              sk.send(content)
              filesize -= 2048
          else:
              content = f.read()
              sk.send(content)
      sk.close()
       
    • 文件的上传加认证

       
       
       
      xxxxxxxxxx
       
       
       
       
      #服务端
      import json
      import socket
      import struct
      import hashlib
      def get_md5(usr,pwd):
          md5 = hashlib.md5(usr.encode('utf-8'))
          md5.update(pwd.encode('utf-8'))
          return md5.hexdigest()
      def login(conn):
          msg = conn.recv(1024).decode('utf-8')
          dic = json.loads(msg)
          with open('userinfo', encoding='utf-8') as f:
              for line in f:
                  username, password = line.strip().split('|')
                  if username == dic['user'] and password == get_md5(dic['user'], dic['passwd']):
                      res = json.dumps({'flag': True}).encode('utf-8')
                      conn.send(res)
                      return True
              else:
                  res = json.dumps({'flag': False}).encode('utf-8')
                  conn.send(res)
                  return False
      def upload(conn):
          len_bytes = conn.recv(4)
          num = struct.unpack('i', len_bytes)[0]
          str_dic = conn.recv(num).decode('utf-8')
          dic = json.loads(str_dic)
          with open(dic['filename'], 'wb') as f:
              while dic['filesize']:
                  content = conn.recv(2048)
                  f.write(content)
                  dic['filesize'] -= len(content)
      sk = socket.socket()
      sk.bind(('127.0.0.1',9001))
      sk.listen()
      while True:
          try:
              conn,addr = sk.accept()
              ret = login(conn)
              if ret:
                  upload(conn)
          except Exception as e:
              print(e)
          finally:
              conn.close()
      sk.close()
      #客户端
      import os
      import json
      import socket
      import struct
      def upload(sk):
          # 上传文件
          file_path = input('>>>')
          filename = os.path.basename(file_path)
          filesize = os.path.getsize(file_path)
          dic = {'filename': filename, 'filesize': filesize}
          bytes_dic = json.dumps(dic).encode('utf-8')
          len_bytes = struct.pack('i', len(bytes_dic))
          sk.send(len_bytes)
          sk.send(bytes_dic)
          with open(file_path, 'rb') as f:
              while filesize > 2048:
                  content = f.read(2048)
                  sk.send(content)
                  filesize -= 2048
              else:
                  content = f.read()
                  sk.send(content)
      usr = input('username :')
      pwd = input('password :')
      dic = {'operate':'login','user':usr,'passwd':pwd}
      bytes_dic = json.dumps(dic).encode('utf-8')
      sk = socket.socket()
      sk.connect(('127.0.0.1',9001))
      sk.send(bytes_dic)
      res = sk.recv(1024).decode('utf-8')
      dic = json.loads(res)
      if dic['flag']:
          print('登录成功')
          upload(sk)
      else:
          print('登录失败')
      sk.close()
       
    • 验证客户端链接合法性:当别人知道我ip,并且通过端口扫描知道我的相应的端口时,岂不是很危险,就必须进行验证,通过验证成功才能进来。通过 hmac模块加盐/也可以使用hashlib,不过比较麻烦。

       
       
       
      xxxxxxxxxx
       
       
       
       
      #服务端
      #其中os.urandom(n) 是一种bytes类型的随机生成n个字节字符串的方法,而且每次生成的值都不相同。再加上md5等加密的处理,就能够成内容不同长度相同的字符串了。
      from socket import *
      import hmac,os
      secret_key=b'Jedan has a big key!'  #密钥
      def conn_auth(conn):
          print('开始验证新链接的合法性')
          msg=os.urandom(32)#生成一个32字节的随机字符串
          conn.sendall(msg)
          h=hmac.new(secret_key,msg) 
          digest=h.digest()
          respone=conn.recv(len(digest))
          return hmac.compare_digest(respone,digest)
      def data_handler(conn,bufsize=1024):
          if not conn_auth(conn):
              print('该链接不合法,关闭')
              conn.close()
              return
          print('链接合法,开始通信')
          while True:
              data=conn.recv(bufsize)
              if not data:break
              conn.sendall(data.upper())
      def server_handler(ip_port,bufsize,backlog=5):
          tcp_socket_server=socket(AF_INET,SOCK_STREAM)
          tcp_socket_server.bind(ip_port)
          tcp_socket_server.listen(backlog)
          while True:
              conn,addr=tcp_socket_server.accept()
              print('新连接[%s:%s]' %(addr[0],addr[1]))
              data_handler(conn,bufsize)
      if __name__ == '__main__':
          ip_port=('127.0.0.1',9999)
          bufsize=1024
          server_handler(ip_port,bufsize)
          
       #客服端
      from socket import *
      import hmac,os
      secret_key=b'Jedan has a big key!'
      def conn_auth(conn):
          msg=conn.recv(32)
          h=hmac.new(secret_key,msg)
          digest=h.digest()
          conn.sendall(digest)
      def client_handler(ip_port,bufsize=1024):
          tcp_socket_client=socket(AF_INET,SOCK_STREAM)
          tcp_socket_client.connect(ip_port)
          conn_auth(tcp_socket_client)
          while True:
              data=input('>>: ').strip()
              if not data:continue
              if data == 'quit':break
              tcp_socket_client.sendall(data.encode('utf-8'))
              respone=tcp_socket_client.recv(bufsize)
              print(respone.decode('utf-8'))
          tcp_socket_client.close()
      if __name__ == '__main__':
          ip_port=('127.0.0.1',9999)
          bufsize=1024
          client_handler(ip_port,bufsize)
       
  • 相关阅读:
    Hadoop: No appenders could be found for logger (org.apache.hadoop.metrics2.lib.MutableMetricsFactory).解决办法
    Libnfc
    生产服务GC调优实践及基本流程总结
    万年历算法
    C#中操作Oracle时的SQL语句参数的用法
    对DataGrid的初步了解
    C#中HashTable的用法
    正则表达式
    C#动态调用WebService
    Ubuntu 故障处理笔记
  • 原文地址:https://www.cnblogs.com/double-W/p/10697739.html
Copyright © 2011-2022 走看看