Day 6-3 粘包現象
服務端:
1 import socket 2 import subprocess 3 4 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 5 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 6 phone.bind(("127.0.0.1", 8990)) 7 8 phone.listen(10) 9 10 print("運行中...") 11 while True: 12 conn, client_ipaddr = phone.accept()13 print("客戶端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1])) 14 while True: # 通信循環 15 try: 16 # 1,接收客戶端發送的命令 17 cmd = conn.recv(1024) 18 if not cmd: break 19 # 2,在服務器上執行客戶端發過來的命令 20 cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True, 21 stdout=subprocess.PIPE, 22 stderr=subprocess.PIPE) 23 stdout = cmd.stdout.read() 24 stderr=cmd.stderr.read() 25 # 3,把執行結果發送給客戶端 26 conn.send(stdout+stderr) 27 exceptConnectionResetError: # 針對windows系統,客戶端強制斷開後,會報這個錯誤. 28 break 29 conn.close() 30 phone.close()
客戶端:
1 import socket 2 import os 3 if os.name =="nt": 4 code = "GBK" 5 else: 6 code="utf-8" 7 8 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 9 10 phone1.connect(("127.0.0.1", 8990)) 11 12 while True: 13 #1,發送命令給服務器 14 cmd = input("請輸入你要發送的信息:").strip() 15 if not cmd:continue 16 phone1.send(cmd.encode("utf-8")) 17 #2,接收服務器執行命令後的結果. 18 data = phone1.recv(1024) 19 print(data.decode(code)) 20 phone1.close()
我們分別啟動服務端和客戶端.然後在客戶端上執行一個名 tree c:\ (windows系統).服務端返回的結果如下:
1 C: 2 ├─e_object 3 ├─GeePlayerDownload 4 ├─Intel 5 │ └─Logs 6 ├─Program Files 7 │ ├─Common Files 8 │ │ ├─Microsoft Shared 9 │ │ │ ├─Filters 10 │ │ │ ├─ink 11 │ │ │ │ ├─ar-SA 12 │ │ │ │ ├─bg-BG 13 │ │ │ │ ├─cs-CZ 14 │ │ │ │ ├─da-DK 15 │ │ │ │ ├─de-DE 16 │ │ │ │ ├─el-GR 17 │ │ │ │ ├─en-US 18 │ │ │ │ ├─es-ES 19 │ │ │ │ ├─et-EE 20 │ │ │ │ ├─fi-FI 21 │ │ │ │ ├─fr-FR 22 │ │ │ │ ├─fsdefinitions 23 │ │ │ │ │ ├─auxpad 24 │ │ │ │ │ ├─keypad 25 │ │ │ │ │ ├─main 26 │ │ │ │ │ ├─numbers 27 │ │ │ │ │ ├─oskmenu 28 │ │ │ │ │ ├─osknumpad 29 │ │ │ │ │ ├─oskpred 30 │ │ │ │ │ ├─symbols 31 │ │ │ │ │ └─web 32 │ │ │ │ ├─he-IL 33 │ │ │ │ ├─hr-HR 34 │ │ │ │ ├─hu-HU 35 │ │ │ │ ├─HWRCustomization 36 │ │ │ │ ├─it-IT 37 │ │ │ │ ├─ja-
我們此時,在客戶端繼續輸入ifconfig 命令,發現返回的數據依然是上次tree c:\的結果.這是為什麽呢?
這是因為,客戶端一次只能接收1024個字節的數據,如果超過1024個字節,那麽這些數據就會在服務器的IO緩存區裏暫存下來.如果現在在客戶端輸入ipconfg命令後,在服務端返回數據給客戶端時,因為IO緩存區還有上次tree命令存留的信息,所以會先把上次的信息返回給客戶端.等tree命令所有的數據都返回給客戶端後,才會返回ipconfig的數據.就造成了兩條命令的結果都在某一次的返回數據中.這種現象就叫做粘包.
粘包發生需要滿足的條件:
一,在客戶端:
由於TCP協議使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然後進行封包。如果連續發送2個2bytes的包,這時候在客戶端就已經發生了粘包現象.但是此時在服務端不一定會發生粘包.
二,服務端:
如果這2個包沒有超出服務器接收的最大字節數(1024),就不會發生粘包.如果服務器每次只接收1bytes,那麽在服務端也會發生粘包.
怎麽解決粘包這種現象呢?有人說把客戶端接收的最大字節值改成其他更大的數字,不就可以了嗎?一般情況下,最大接收字節數的值不超過8192.超過這個數,會影響接收的穩定性和速度.
send和recv對比:
1.不管是send還是recv,都不是直接把數據發送給對方,而是通過系統發送.然後從系統內存中讀取返回的數據.
2.send和recv不是一一對應的.
3.send工作流程:把數據發送給操作系統,讓系統調用網卡進行發送.send就完成了工作
recv工作流程,等待客戶端發送過來的數據.這個時間比較長.接收到數據後,再從系統內存中調用數據.
粘包問題只存在於TCP中,Not UDP
還是看上圖,發送端可以是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。怎樣定義消息呢?可以認為對方一次性write/send的數據為一個消息,需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈現在內核緩沖區。
例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
總結
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
2. UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
3. tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭
解決粘包現象的思路:
通過上述的實驗和例子,我們知道,粘包現象的產生,主要是客戶端不知道要接收多少數據(或者說多大的數據).那麽,按照這個思路,那麽我們知道,在服務端執行完命令後,我們可以在服務端獲取結果的大小.再發送給客戶端,讓客戶端知道被接收數據的大小,然後再通過一個循環,來接收數據即可.這時我們需要用一個新的模塊,struct來制作報頭信息.發送給客戶端.
import struct pack = struct.pack("i",10000) # 定義格式 print(pack,len(pack),type(pack)) # pack的類型是bytes,傳輸的時候,就不用encode了. t = struct.unpack("i",pack) #解包, print(t) # 獲取元組形式的數據. t = struct.unpack("i",pack)[0] # 直接獲取數據的值. """ b"\x10‘\x00\x00" 4 <class ‘bytes‘> (10000,) 直接獲取: 10000 """
1 #!_*_ coding:utf-8 _*_ 2 import socket 3 import subprocess 4 import struct 5 6 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 7 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 8 phone.bind(("127.0.0.1", 8990)) 9 10 phone.listen(10) 11 12 print("運行中...") 13 while True: 14 conn, client_ipaddr = phone.accept() 15 print("客戶端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1])) 16 while True: # 通信循環 17 try: 18 # 1,接收客戶端發送的命令 19 cmd = conn.recv(1024) 20 if not cmd: break 21 # 2,在服務器上執行客戶端發過來的命令 22 cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True, 23 stdout=subprocess.PIPE, 24 stderr=subprocess.PIPE) 25 stdout = cmd.stdout.read() 26 stderr=cmd.stderr.read() 27 # 3,把執行結果發送給客戶端 28 #3-1 把報頭(固定長度)發送給客戶端 29 total_size = len(stdout+stderr) 30 print(total_size) 31 header = struct.pack("i",total_size) # i是類型,total_size是值.這個命令會把total_size打包成一個4個字節長度的字節數據類型 32 conn.send(header) # 把報頭發送給客戶端 33 #302 發送數據給客戶端 34 35 conn.send(stdout) 36 conn.send(stderr) 37 except ConnectionResetError: # 針對windows系統,客戶端強制斷開後,會報這個錯誤. 38 break 39 conn.close() 40 phone.close()粘包解決服務端
1 #!_*_ coding:utf-8 _*_ 2 import socket 3 import os 4 import struct 5 6 if os.name == "nt": 7 code = "GBK" 8 else: 9 code = "utf-8" 10 11 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 12 13 phone1.connect(("127.0.0.1", 8990)) 14 15 while True: 16 # 1,發送命令給服務器 17 cmd = input("請輸入你要發送的信息:").strip() 18 if not cmd: continue 19 phone1.send(cmd.encode("utf-8")) 20 # 2,接收服務器執行命令後的結果. 21 # 2-1 接收服務器發過來的報頭 22 header = phone1.recv(4) # 收報頭 23 total_size = struct.unpack("i", header)[0] #解包,並取出報頭中數據 24 25 # 2-2 循環接收數據 26 recv_size = 0 27 recv_data = b"" 28 while recv_size < total_size: 29 data = phone1.recv(1024) # 接收數據 30 recv_data += data # 拼接數據 31 recv_size += len(data) # 設置已接收數據的大小 32 print(recv_data.decode(code)) 33 phone1.close()粘包解決客戶端
Day 6-3 粘包現象