1. 程式人生 > >Day 6-3 粘包現象

Day 6-3 粘包現象

nagle 套接字 元組 player none PE 怎樣 bin print

服務端:

 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 except
ConnectionResetError: # 針對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的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束

所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。

總結

  1. 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 粘包現象