socket粘包問題解決




粘包
client.send(data1)
client.send(data2)
這兩次send緊挨在一起,處理的時候會放在一起發過去
在Linux里每次都粘包,Windows里面某次會出現粘包
在兩次send中間放一個time.time(0.5)可以解決這個問題,這個比較low

為什么只有TCP粘包,UDP沒有粘包?
  1. TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
  2. UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
  3. tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略


粘包產生原因:

在tcp協議中,有一個合包機制,將多次連續發送且間隔較小的數據,
打包成一塊數據傳送

還有一個機制是拆包,在發送端,因為受到網卡MTU限制,會將大的超過MTU
限制的數據,進行拆分,拆分成多個小的數據,進行傳輸,當傳輸到目標主機
的操作系統層時,會將多個小數據合並成原本的數據。

所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。

 

 

 

 

 

粘包問題解決(一)


解決辦法是在第一次send之后等待客戶端確認,客戶端確認之后,發給服務端,服務端收到之后進行第二次send

 

粘包問題解決(二)

import socket
'''
server.send(data1)
server.send(md5)
如果知道第一次data1的長度,
可以不用收到確認信息,也能保證不粘包的方法,怎么做呢?
'''
# 客戶端
client = socket.socket()
client.connect(("localhost",9999))
total_size = 50000
received_size = 0
while received_size < total_size:
    if total_size - received_size > 1024:  #要收不止一次
        size = 1024
    else:  #最后一次了,剩多少收多少
        size = total_size - received_size
    data = client.recv(size)
    received_size += len(data)
else:
    print("receive done", total_size, received_size)
md5 = client.recv(1024)

 

粘包解決三

import json,struct
#假設通過客戶端上傳1T:1073741824000的文件a.txt

#為避免粘包,必須自定制報頭
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T數據,文件路徑和md5值

#為了該報頭能傳送,需要序列化並且轉為bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化並轉成bytes,用於傳輸

#為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節
head_len_bytes=struct.pack('i',len(head_bytes)) #這4個字節里只包含了一個數字,該數字是報頭的長度

#客戶端開始發送
conn.send(head_len_bytes) #先發報頭的長度,4個bytes
conn.send(head_bytes) #再發報頭的字節格式
conn.sendall(文件內容) #然后發真實內容的字節格式

#服務端開始接收
head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的字節格式
x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度

head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式
header=json.loads(json.dumps(header)) #提取報頭

#最后根據報頭的內容提取真實的數據,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)

我們可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化,然后用struck將序列化后的數據長度打包成4個字節(4個自己足夠用了)

發送時:

  1. 先發報頭長度
  2. 再編碼報頭內容然后發送
  3. 最后發真實內容

接收時:

  1. 先收報頭長度,用struct取出來
  2. 根據取出的長度收取報頭內容,然后解碼,反序列化
  3. 從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容

 

服務端:

# 前面省略....這是執行遠程執行命令的服務端
while True:
    conn, addr = sock.accept()
    while True:
        cmd = conn.recv(1024)   #  獲取客戶端的命令
        res = subprocess.Popen(cmd.decode('utf-8'),
                               shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
        err = res.stderr.read()   # 獲取cmd錯誤信息
        print(err)
        if err:
            back_msg = err
        else:
            back_msg = res.stdout.read()

        headers = {'data_size': len(back_msg)}   
        head_json = json.dumps(headers)        
        head_json_bytes = bytes(head_json, encoding='utf-8')   # 報頭

        conn.send(struct.pack('i', len(head_json_bytes)))     # 1.先發報頭的長度
        conn.send(head_json_bytes)                            # 2.再發報頭
        conn.sendall(back_msg)                                # 3.在發真實的內容

    conn.close()

客戶端:

from socket import *
import struct, json

ip_port = ('127.0.0.1', 8080)
client = socket(AF_INET, SOCK_STREAM)
client.connect(ip_port)

while True:
    cmd = input('>>: ')
    if not cmd: continue
    client.send(bytes(cmd, encoding='utf-8'))     # 發送cmd的byte

    head = client.recv(4)                                               # 1.先收報頭長度
    head_json_len = struct.unpack('i', head)[0]                         # 2.提取報頭長度
    head_json = json.loads(client.recv(head_json_len).decode('utf-8'))  # 3.按照報頭長度,收取報頭的bytes格式,轉換為json
    data_len = head_json['data_size']                                   # 4. 根據報頭,獲取數據長度

    recv_size = 0
    recv_data = b''
    while recv_size < data_len:                                         # 5.根據數據長度,循環接收
        recv_data += client.recv(1024)
        recv_size += len(recv_data)

    print(recv_data.decode('utf-8'))
    # print(recv_data.decode('gbk')) #windows默認gbk編碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM