粘包
client.send(data1)
client.send(data2)
這兩次send緊挨在一起,處理的時候會放在一起發過去
在Linux里每次都粘包,Windows里面某次會出現粘包
在兩次send中間放一個time.time(0.5)可以解決這個問題,這個比較low
為什么只有TCP粘包,UDP沒有粘包?
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
- 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個自己足夠用了)
發送時:
- 先發報頭長度
- 再編碼報頭內容然后發送
- 最后發真實內容
接收時:
- 先收報頭長度,用struct取出來
- 根據取出的長度收取報頭內容,然后解碼,反序列化
- 從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容
服務端:
# 前面省略....這是執行遠程執行命令的服務端 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編碼