1.為什么會出現粘包??
讓我們基於tcp先制作一個遠程執行命令的程序(1:執行錯誤命令 2:執行ls 3:執行ifconfig)
注意注意注意:
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
的結果的編碼是以當前所在的系統為准的,如果是windows,那么res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼
發送端可以是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。怎樣定義消息呢?可以認為對方一次性write/send的數據為一個消息,需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成后才呈現在內核緩沖區。
例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段后一次發送出去,這樣接收方就收到了粘包數據。
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
- tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略
udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味着udp根本不會粘包,但是會丟數據,不可靠
tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
兩種情況下會發生粘包。
發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close() 服務端
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('feng'.encode('utf-8')) 客戶端
接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(2) #一次沒有收完整 data2=conn.recv(10)#下次收的時候,會先取舊的數據,然后取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close() 服務端
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello feng'.encode('utf-8')) 客戶端
拆包的發生情況
當發送端緩沖區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去。
補充問題一:為何tcp是可靠傳輸,udp是不可靠傳輸
基於tcp的數據傳輸請參考我的另一篇文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在數據傳輸時,發送端先把數據發送到自己的緩存中,然后協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則重新發送數據,所以tcp是可靠的
而udp發送數據,對端是不會返回確認信息的,因此不可靠
補充問題二:send(字節流)和recv(1024)及sendall
recv里指定的1024意思是從緩存里一次拿出1024個字節的數據
send的字節流是先放入己端緩存,然后由協議控制將緩存內容發往對端,如果待發送的字節流大小大於緩存剩余空間,那么數據丟失,用sendall就會循環調用send,數據不會丟失
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據
程序的運行速度遠快於網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗
2.解決粘包的方法
為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然后再取真實數據
struct模塊 該模塊可以把一個類型,如數字,轉成固定長度的bytes >>> struct.pack('i',1111111111111) 。。。。。。。。。 struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是范圍 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取出來
根據取出的長度收取報頭內容,然后解碼,反序列化
從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容
#為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后一次send到對端, # 對端在接收時,先從緩存中取出定長的報頭,然后再取真實數據 #struct模塊 # 該模塊可以把一個類型,如數字,轉成固定長度的bytes ''' 我們可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化, 然后用struck將序列化后的數據長度打包成4個字節(4個自己足夠用了) 發送時: 先發報頭長度 再編碼報頭內容然后發送 最后發真實內容 接收時: 先手報頭長度,用struct取出來 根據取出的長度收取報頭內容,然后解碼,反序列化 從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容 ''' # >>> struct.pack("i","abc") # Traceback (most recent call last): # File "<pyshell#1>", line 1, in <module> # struct.pack("i","abc") # struct.error: required argument is not an intege #服務端(定制稍微復雜一點的報頭) import socket,struct,json import subprocess phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_sort=("127.0.0.1",8080) back_log=5 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(ip_sort) phone.listen(back_log) while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print("cmd: %s" %cmd) res=subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE ,stdin=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() headers={'data_size':len(back_msg)} head_json=json.dumps(headers)#序列化成字符串 print(type(head_json)) head_json_bytes=bytes(head_json,encoding="utf-8") #struct.pack("i"轉換成包的類型,第二個參數必須是數字) conn.send(struct.pack("i",len(head_json_bytes)))#先發報頭的長度 conn.send(head_json.encode("utf-8"))#再發報頭 conn.sendall(back_msg)#再發真實的內容 conn.close()
#客戶端解決粘包的方法 import socket,struct,json ip_port=("127.0.0.1",8080) tcp_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) tcp_client.connect_ex(ip_port) while True: cmd=input(">>") if not cmd:continue tcp_client.send(cmd.encode('utf-8')) data=tcp_client.recv(4)#接收報頭消息長度 num=struct.unpack("i",data)[0]#unpack解包出來是一個元祖 print(num) header=json.loads(tcp_client.recv(num).decode("utf-8"))#通過接受報頭長度接受報頭 data_len=header["data_size"]#獲取發送消息的長度 recv_size=0 recv_data=b'' while recv_size<data_len: recv_data+=tcp_client.recv(1024) recv_size=len(recv_data) print(recv_data.decode("gbk")) #print(recv_data.decode("GBK")) #windows默認編碼為GBK