粘包

粘包

  • 粘包現象:html

    • TCP屬於長鏈接,當服務端與一個客戶端進行了鏈接之後,其餘客戶端須要(排隊)等待.若服務端想要鏈接另外一個客戶端,必須首先斷開與第一個客戶端的鏈接。node

    • 緩衝區:它是內存空間的一部分。也就是說,在內存空間中預留了必定的存儲空間,這些存儲空間用來緩衝輸入或輸出的數據,這部分預留的空間就叫作緩衝區,顯然緩衝區是具備必定大小的。緩衝區根據其對應的是輸入設備仍是輸出設備,分爲輸入緩衝區和輸出緩衝區python

    • 爲何引入緩衝區:高速設備與低速設備的不匹配,勢必會讓高速設備花時間等待低速設備,咱們能夠在這二者之間設立一個緩衝區,也就是一個臺階,怕低速的跟不上程序員

    • 緩衝區(buffer)的做用:web

      • 1.能夠解除二者的制約關係,數據能夠直接送往緩衝區,高速設備不用再等待低速設備,提升了計算機的效率。如:咱們使用打印機打印文檔,因爲打印機的打印速度相對較慢,咱們先把文檔輸出到打印機相應的緩衝區,打印機再自行逐步打印。
      • 能夠減小數據的讀寫次數,若是每次數據只傳輸一點數據,就須要傳送不少次,這樣會浪費不少時間,由於開始讀寫與終止讀寫所須要的時間很長,若是將數據送往緩衝區,待緩衝區滿後再進行傳送會大大減小讀寫次數,這樣就能夠節省不少時間。
    • 區別於緩存區(cache):CPU的Cache,它中文名稱是高速緩衝存儲器,讀寫速度很快,幾乎與CPU同樣。因爲CPU的運算速度太快,內存的數據存取速度沒法跟上CPU的速度,因此在cpu與內存間設置了cache爲cpu的數據快取區。當計算機執行程序時,數據與地址管理部件會預測可能要用到的數據和指令,並將這些數據和指令預先從內存中讀出送到Cache。一旦須要時,先檢查Cache,如有就從Cache中讀取,若無再訪問內存,如今的CPU還有一級cache,二級cache。算法

      img

    • 總結:也就是說緩衝區是內存中的對應的輸入輸出,而緩存區是cpu中的。shell

    • 每一個socket(套接字)被建立後,都會分配兩個緩衝區: 輸入緩衝區和輸出緩衝區。json

    • 在windows複製路徑時,從後往前複製路徑會自動添加看不見的符號,從前日後就不會有問題windows

      •  
         
         
         
         
         
         
         
        一、write()/send()並不當即向網絡中傳輸數據,而是先將數據寫入緩衝區中,再由TCP協議將數據從緩衝區發送到目標機器. 一旦將數據寫入到緩衝區,函數就已經完成任務能夠成功返回了,而不用去考慮數據什麼時候被髮送到網絡,也不用去考慮數據是否已經到達目標機器,由於這些後續操做都是TCP協議負責的事情.
        二、TCP協議獨立於 write()/send() 函數,數據有可能剛被寫入緩衝區就發送到網絡,也可能在緩衝區中不斷積壓,屢次寫入的數據被一次性發送到網絡,這取決於當時的網絡狀況,當前線程是否空閒等諸多因素,不禁程序員控制.
        三、read()/recv() 函數也是如此,也從輸入緩衝區中讀取數據,而不是直接從網絡中讀取
        四、這些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
      #第二次接收到的消息>>>
       

       

粘包的解決方案:

  • 方案1、粘包問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞"如何讓發送端在發送數據前,把本身將要發送的字節流總長度讓接收端知曉"

    •  
       
       
      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)
       
  • 方案2、經過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"))
       

2、粘包時的問題:

一、在每次文件傳送中,若是每次發送的數據長度是大於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)
相關文章
相關標籤/搜索