python套接字解決tcp粘包問題
目錄
什么是粘包
演示粘包現象 解決粘包 實際應用
什么是粘包
首先只有tcp有粘包現象,udp沒有粘包
socket收發消息的原理
發送端可以是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),
一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節
的數據,這一點和TCP是很不同的。怎樣定義消息呢?可以認為對方一次性write/send的數據為一個消息,需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成后
才呈現在內核緩沖區。 例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束
粘包問題的根源
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段后一次發
送出去,這樣接收方就收到了粘包數據。
tcp和udp協議
TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對
方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記
錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上
消息頭,實驗略
補充
拆包的發生情況 當發送端緩沖區的長度大於網卡的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,數據不會丟失
總結
udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味着udp根本不會粘包,但是會丟數據,不可靠 tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
演示粘包現象
兩種情況下會發生粘包
發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據量很小,會合到一起,產生粘包),這是由於tcp的優化算法。 接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
第一種情況
客戶端多次間隔時間短,數據量小的發送數據
#服務端
import socket def main(): ip_port= ('127.0.0.1',4444) back_log=5 buffer_size=1024 s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #基於tcp的網絡通信 s1.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) s1.bind(ip_port) #綁定ip和端口 s1.listen(back_log) # 最多連接幾個客戶端 conn, addr = s1.accept() data1=conn.recv(buffer_size) data2=conn.recv(buffer_size) data3=conn.recv(buffer_size) print('第一次',data1.decode('utf-8')) print('第二次',data2.decode('utf-8')) print('第三次',data3.decode('utf-8')) conn.close() s1.close() if __name__ == '__main__': main()
#客戶端 import socket def main(): ip_port = ('127.0.0.1', 4444) buffer_size = 1024 s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2.connect(ip_port) # 連接服務端 data1 = 'hello' s2.send(data1.encode('utf-8')) data2 ='wrold' s2.send(data2.encode('utf-8')) data3 = 'pop' s2.send(data3.encode('utf-8')) s2.close() if __name__ == '__main__': main()
演示
可以看出來服務端在第一次就把三次發送的數據都接收了,這就是粘包,服務端不知道一次讀取多少的數據,一次全部讀取出來。
首先我們要知道並不是客戶端發幾次,服務端就要接收幾次,一次發的數據也可以三次讀取出來,收發信息都是從自己的內核緩存區讀取。
第二種情況
接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
#服務端
import socket def main(): ip_port= ('127.0.0.1',4444) back_log=5 buffer_size=1024 s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #基於tcp的網絡通信 s1.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) s1.bind(ip_port) #綁定ip和端口 s1.listen(back_log) # 最多連接幾個客戶端 conn, addr = s1.accept() data1=conn.recv(5) data2=conn.recv(buffer_size) print('第一次',data1.decode('utf-8')) print('第二次',data2.decode('utf-8')) conn.close() s1.close() if __name__ == '__main__': main()
#客戶端 import socket def main(): ip_port = ('127.0.0.1', 4444) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2.connect(ip_port) # 連接服務端 data1 = 'hellowroldpop' s2.send(data1.encode('utf-8')) s2.close() if __name__ == '__main__': main()
演示
服務端讀取數據沒有全部讀取出來,導致第一次應該接收完的數據還要第二次讀取出來
解決粘包
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據
第一種解決方法
#服務端 import socket def main(): ip_port = ('127.0.0.1', 4444) back_log = 5 buffer_size = 1024 s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基於tcp的網絡通信 s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s1.bind(ip_port) # 綁定ip和端口 s1.listen(back_log) # 最多連接幾個客戶端 conn, addr = s1.accept() while True: #接收數據大小 length= conn.recv(buffer_size).decode('utf-8')
#為防止客戶端連續發包,回應 conn.send('ready'.encode('utf-8')) length=int(length) recv_size=0 #已經接收到數據的大小 recv_msg=b'' #已經接收到的數據
#接收數據 while recv_size<length: r_msg = conn.recv(buffer_size) recv_msg+=r_msg recv_size +=len(r_msg) #另一種方法接收數據的方法
#recv_msg+=conn.recv(buffer_size) #recv_size=len(recv_msg) s1.close() if __name__ == '__main__': main()
#客戶端 import socket def main(): ip_port = ('127.0.0.1', 4444) buffer_size = 1024 s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2.connect(ip_port) # 連接服務端 while True: data1 = input('input:')
#將數據大小轉為字符型然后編碼發出去 s2.send(str(len(data1)).encode('utf-8'))
#接收服務端的回應 server_Ready=s2.recv(buffer_size)
#接收到服務端回應 if server_Ready==b'ready': s2.send(data1.encode('utf-8')) s2.close() if __name__ == '__main__': main()
總結:客戶端在發送數據時,先發送數據大小,這時不能把數據內容一起發送出去,服務端第一次接收的時候,並不知道該讀取多少的數據大小和多少的數據內容,所以還是會造成粘包,我們的解決辦法是,服務端獲取到數據大小后,要回應一次,然后根據數據大小來循環讀取內容。 這種方法不好,需要服務端多發一次回應,這很影響服務端的性能。 程序的運行速度遠快於網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗
第二種解決方法
為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然后再取真實數據
struct模塊
該模塊可以把一個類型,如數字,轉成固定長度的bytes >>> struct.pack('i',1111111111111) #第一個參數是要封裝的格式類型,第二個參數是要封裝的內容 struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個封裝數據的范圍,只要在這個范圍里面,就可以把內容封裝成固定大小
#服務端 import socket import struct def main(): ip_port = ('127.0.0.1', 4444) back_log = 5 buffer_size = 1024 s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基於tcp的網絡通信 s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s1.bind(ip_port) # 綁定ip和端口 s1.listen(back_log) # 最多連接幾個客戶端 conn, addr = s1.accept() while True: length_data= conn.recv(4) length=struct.unpack('i',length_data)[0] recv_size=0 #已經接收到數據的大小 recv_msg=b'' #已經接收到的數據 while recv_size<length: r_msg = conn.recv(buffer_size) recv_msg+=r_msg recv_size +=len(r_msg) #recv_msg+=conn.recv(buffer_size) #recv_size=len(recv_msg) s1.close() if __name__ == '__main__': main()
#客戶端 import socket import struct def main(): ip_port = ('127.0.0.1', 4444) buffer_size = 1024 s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2.connect(ip_port) # 連接服務端 while True: data1 = input('input:') length=len(data1) #定制包頭 i為4個字節,所以接收方為四個字節,這個大小並不是輸入的大小,而是封裝固定的大小 data_length=struct.pack('i',length) #使用struct,直接將int轉為二進制型數據傳輸,對方使用struct解包 s2.send(data_length) s2.send(data1.encode('utf-8')) s2.close() if __name__ == '__main__': main()
總結:客戶端把數據長度封裝成一個固定大小的數據,這時服務端就可以指定讀取固定大小的內容,不會讀取數據的內容,服務端只要根據數據長度再來接收數據內容就好了,所以客戶端連續兩次發數據,不會粘包,因為服務
端每次接收都只接收了本次該接收的數據。
實際應用
#服務端
from socket import * import subprocess import struct def main(): ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 s1 = socket(AF_INET,SOCK_STREAM) s1.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) s1.bind(ip_port) s1.listen(back_log) while True: conn,addr=s1.accept() while True: try: #收信息 cmd = conn.recv(buffer_size) if not cmd:break print('收到的命令是:',cmd.decode('utf-8')) #執行命令 res = subprocess.Popen(cmd.decode('utf-8'),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err = res.stderr.read() if err: cmd_res=err else: cmd_res=res.stdout.read() if not cmd_res: cmd_res='執行成功'.encode('gbk') length=len(cmd_res) #第一次發送數據大小 data_length = struct.pack('i', length) # 使用struct,直接將int轉為二進制型數據傳輸,對方使用struct解包 conn.send(data_length) #發信息 #注意:執行的結果默認jbk編碼方式,所以客戶端必須使用gbk方式解碼 conn.send(cmd_res) except Exception: break conn.close() s1.close() # 關閉服務端套接字 if __name__ == '__main__': main()
#客戶端
from socket import * import struct def main(): ip_port=('127.0.0.1',8080) buffer_size=1024 s1 = socket(AF_INET,SOCK_STREAM) s1.connect(ip_port) while True: cmd = input('-->') if not cmd:continue if cmd =='quite':break s1.send(cmd.encode('utf-8')) length_data =s1.recv(4) length = struct.unpack('i', length_data)[0] recv_size = 0 # 已經接收到數據的大小 recv_msg = b'' # 已經接收到的數據 while recv_size < length: r_msg = s1.recv(buffer_size) recv_msg += r_msg recv_size += len(r_msg) # recv_msg+=conn.recv(buffer_size) # recv_size=len(recv_msg) print('命令執行結果:',recv_msg.decode('gbk')) s1.close() if __name__=='__main__': main()
總結
如果沒有粘包的處理 服務端把命令執行的結果發給客戶端的時候,數據太大,客戶端一次沒有接收完,在客戶端第二次執行命令的時候,就會把第一次沒有讀取完的部分也讀取出來,這屬於我們剛才說的第二種粘包的情況。 有了粘包的處理 只要服務端把結果發過來,就算超過網卡的限制(拆包發送),客戶端能保證在循環的過程中接收完結果