1.什麼是粘包 寫在前面:只有TCP有粘包現象,UDP永遠不會粘包 1.TCP下的粘包 因為TCP協議是面向連接、面向流的,收發兩端(客戶端和伺服器端)都要有成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle演算法),將多次間隔較小且數據量小的數據, ...
1.什麼是粘包
寫在前面:只有TCP有粘包現象,UDP永遠不會粘包
1.TCP下的粘包
因為TCP協議是面向連接、面向流的,收發兩端(客戶端和伺服器端)都要有成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle演算法),將多次間隔較小且數據量小的數據,合併成一個大的數據塊,然後進行封包,這就導致了數據量小的粘包現象;同時因為tcp的協議的安全可靠性,在沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩衝區內容。數據是可靠的,但是會導致數據量大的粘包;
2.UDP沒粘包原因:
UDP是無連接的,面向消息的,為提供高效率服務 ,並不會使用塊的合併優化演算法;同時由於UDP支持一對多的模式,所以接收端緩衝區採用了鏈式結構來記錄每一個到達的UDP包,也就是在每個UDP包中有頭(消息來源地址,埠等信息),這樣,對於接收端來說就是有邊界的,所以UDP永遠沒粘包。
2.兩種粘包情況
1.情況一 發送方的緩存機制
發送端需要等緩衝區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,優化機制會合到一起,產生粘包)
1 # 連續傳小包被優化機制合併導致的粘包 2 3 import socket 4 5 server = socket.socket() 6 ip_port = ('127.0.0.1',8081) 7 server.bind(ip_port) 8 server.listen() 9 conn,addr = server.accept() 10 11 from_client_msg1 = conn.recv(1024).decode('utf-8') 12 from_client_msg2 = conn.recv(1024).decode('utf-8') 13 14 print(from_client_msg1) 15 print(from_client_msg2) 16 # heheheenenen 兩條消息被優化合併在了一塊 17 18 conn.close() 19 server.close()View 小數據粘包_server Code
1 import socket 2 3 client = socket.socket() 4 ip_port = ('127.0.0.1',8081) 5 client.connect(ip_port) 6 7 To_server_msg1 = client.send(b'hehehe') 8 To_server_msg2 = client.send(b'enenen') 9 10 client.close()View 小數據粘包_client Code
2.情況二 接收方的緩存機制
接收方不及時接收緩衝區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩衝區拿上次遺留的數據,產生粘包)
1 # 傳輸數據超出接收範圍導致的粘包 2 import socket 3 import subprocess 4 5 server = socket.socket() 6 ip_port = ('127.0.0.1',8001) 7 server.bind(ip_port) 8 server.listen() 9 conn,addr = server.accept() 10 11 while 1: 12 from_client_cmd = conn.recv(1024).decode('utf-8') 13 14 sub_obj = subprocess.Popen( 15 from_client_cmd, 16 shell=True, 17 stdout=subprocess.PIPE, 18 stderr=subprocess.PIPE, 19 ) 20 cmd_res = sub_obj.stdout.read() 21 print('結果長度>>>', len(cmd_res)) 22 conn.send(cmd_res) # 發內容給客戶端數據大粘包_server
1 import socket 2 3 client = socket.socket() 4 ip_port = ('127.0.0.1',8001) 5 client.connect(ip_port) 6 7 8 while 1: 9 client_cmd = input('請輸入系統指令>>>') 10 client.send(client_cmd.encode('utf-8')) 11 12 from_server_msg = client.recv(1024) # 接收返回的消息 13 14 print(from_server_msg.decode('gbk')) 15 # 可以看到沒一次性接收完畢,執行下條命令出現了上次的結果,這就是數據大超出接收範圍的粘包。數據大粘包_client
3.總結
黏包現象只發生在tcp協議中:
1.從錶面上看,黏包問題主要是因為發送方和接收方的緩存機制、tcp協議面向流通信的特點。
2.實際上,主要還是因為接收方不知道消息間的界限,不知道一次性提取多少位元組的數據所造成。
3.粘包解決方案
1.解決方案一:
問題的根源在於,接收端不知道發送端將要傳送的位元組流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的位元組流總大小讓接收端知曉,然後接收端來一個死迴圈接收完所有數據。
1 import socket 2 import subprocess 3 4 server = socket.socket() 5 ip_port = ('127.0.0.1',8001) 6 server.bind(ip_port) 7 server.listen() 8 conn,addr = server.accept() 9 10 while 1: 11 from_client_cmd = conn.recv(1024).decode('utf-8') # 接收傳來的命令 12 sub_obj = subprocess.Popen( 13 from_client_cmd, 14 shell=True, 15 stdout=subprocess.PIPE, 16 stderr=subprocess.PIPE, 17 ) 18 # subprocess對象.read 得到命令結果,是bytes類型的 19 str_byt = sub_obj.stdout.read() 20 str_len = len(str_byt) 21 print(str_len) 22 conn.send(str(str_len).encode('utf-8')) # 先髮長度 23 24 from_client_msg = conn.recv(1024).decode('utf-8') 25 if from_client_msg == 'ok': 26 conn.send(str_byt) # 客戶端確認收到長度後 再發送真實內容 27 else: 28 print("客戶端未收到長度!") 29 break 30 31 conn.close() 32 server.close()解決小包粘包_服務端
1 import socket 2 3 client = socket.socket() 4 ip_port = ('127.0.0.1',8001) 5 client.connect(ip_port) 6 7 8 while 1: 9 client_cmd = input('請輸入系統指令>>>') 10 client.send(client_cmd.encode('utf-8')) 11 12 from_server_len = client.recv(1024).decode('utf-8') # 接收返回的長度 13 print(from_server_len) 14 client.send(b'ok') 15 16 from_server_msg = client.recv(int(from_server_len)) # 註意還原成int 17 print(from_server_msg.decode('gbk'))解決小包粘包_客戶端
不足之處:程式的運行速度遠快於網路傳輸速度,所以在發送一段位元組前,先用send去發送該位元組流長度,這種方式會放大網路延遲帶來的性能損耗
2.解決方案進階:
可藉助struct模塊,這個模塊可把要發的數據長度轉成固定長度位元組。這樣客戶端每次接收消息前只要先收到這個固定長度位元組的內容,收到接下來要接收的信息大小,那麼最終接受的數據只要達到這個值就停止,就能完整接收的數據了。
struct模塊:該模塊可以把一個類型,如數字,轉成固定長度的bytes
1 import struct 2 3 res = struct.pack('i',1111111) # i 模式 1111111 要轉換的內容 4 print(res) # b'G\xf4\x10\x00'
模式,見下圖:
1 import socket 2 import struct 3 import subprocess 4 5 server = socket.socket() 6 ip_port = ('127.0.0.1',8001) 7 server.bind(ip_port) 8 server.listen() 9 conn,addr = server.accept() 10 11 while 1: 12 from_client_cmd = conn.recv(1024).decode("utf-8") # 註意轉碼 13 # print(from_client_cmd) 14 sub_obj = subprocess.Popen( 15 from_client_cmd, 16 shell=True, 17 stdout=subprocess.PIPE, 18 stderr=subprocess.PIPE, 19 ) 20 cmd_res = sub_obj.stdout.read() # 得到bytes類型所有內容 21 str_len = len(cmd_res) 22 print(str_len) 23 24 str_len1 = struct.pack('i',str_len) # 把長度打包成4位元組的bytes 25 26 conn.send(str_len1 + cmd_res) # 拼接位元組 把長度和內容打包發給客戶端struct方案解決粘包_server
1 import socket, struct 2 3 client = socket.socket() 4 ip_port = ('127.0.0.1',8001) 5 client.connect(ip_port) 6 7 while 1: 8 str_cmd = input('請輸入命令>>> ').encode('utf-8') 9 client.send(str_cmd) 10 # 先接收4個位元組,4個位元組是數據的真實長度轉換而成的 11 str_len = client.recv(4) 12 # print(str_len) 13 num = struct.unpack('i',str_len)[0] # 註意解出來是一個元組 用下標把真實長度取出來 14 15 print(num) 16 17 str_res = client.recv(num) 18 print(str_res.decode('gbk'))struct方案解決粘包_client
補充:獲取緩衝區大小
1 # 獲取socket緩衝區大小 2 import socket 3 from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF 4 sk = socket.socket(type=socket.SOCK_DGRAM) 5 # sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024) 6 sk.bind(('127.0.0.1',8090)) 7 print('>>>>', (sk.getsockopt(SOL_SOCKET, SO_SNDBUF))/1024) 8 print('>>>>', sk.getsockopt(SOL_SOCKET, SO_RCVBUF))