一、subprocess 註:如果是Windows,那麼res.stdout.read()讀出的是GBK編碼的信息,在接收端需要用GBK解碼且只能從管道里讀一次結果,PIPE稱為管道。 二、粘包現象 1. TCP會粘包,UDP永遠不會粘包 發送端可以是一K一K地發送數據,而接收端的應用程式可以兩K兩 ...
一、subprocess
import subprocess cmd = input("請輸入指令>>>") res = subprocess.Popen( cmd, # 字元串指令:"dir", "ipconfig"等 shell=True, # 使用shell,就相當於使用cmd視窗 stderr=subprocess.PIPE, # 標準錯誤輸出,凡是輸入錯誤指令,錯誤指令輸出的報錯信息就會被它拿到 stdout=subprocess.PIPE, # 標準輸出,正確指令的輸出結果被它拿到 ) print(res.stdout.read()) print(res.stderr.read())
註:如果是Windows,那麼res.stdout.read()讀出的是GBK編碼的信息,在接收端需要用GBK解碼且只能從管道里讀一次結果,PIPE稱為管道。
二、粘包現象
1. TCP會粘包,UDP永遠不會粘包
發送端可以是一K一K地發送數據,而接收端的應用程式可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個位元組的數據,也就是說,應用程式所看到的數據是一個整體,或說是一個流(stream),一條消息有多少位元組對應用程式是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。而UDP是面向消息的協議,每個UDP段都是一條消息,應用程式必須以消息為單位提取數據,不能一次提取任意位元組的數據,這一點和TCP是很不同的。怎樣定義消息呢?可以認為對方一次性write/send的數據為一個消息,需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈現在內核緩衝區。 例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的位元組流發送的,在接收方看了,根本不知道該文件的位元組流從何處開始,在何處結束 所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少位元組的數據所造成的。 此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化演算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。 1.TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和伺服器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle演算法),將多次間隔較小且數據量小的數據,合併成一個大的數據塊,然後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。 2.UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合併優化演算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,埠等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。 3.tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程式卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略 udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個位元組的數據就算完成,若是y>x數據就丟失,這意味著udp根本不會粘包,但是會丟數據,不可靠 tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩衝區內容。數據是可靠的,但是會粘包解釋原因
2. UDP是面向包的,不存在粘包現象
import socket import subprocess server = socket.socket(type=socket.SOCK_DGRAM) ip_port = ("127.0.0.1", 8001) server.bind(ip_port) while 1: cmd, addr = server.recvfrom(1024) cmd = cmd.decode("utf-8") if cmd == "q": break res = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) server.sendto(res.stdout.read(), addr)server端
import socket client = socket.socket(type=socket.SOCK_DGRAM) ip_port = ("127.0.0.1", 8001) cmd = input(">>>").strip().encode("utf-8") client.sendto(cmd, ip_port) msg, addr = client.recvfrom(1024) print(msg.decode("gbk"))client端
結果:
報錯原因為客戶端設置接收的數大小小於消息包的大小,所以報錯。
3. TCP粘包現象
1) 第一種:接收方沒有及時接收緩衝區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再接收的時候還是從緩衝區拿上次遺留的數據,產生粘包)
服務端:
import socket import subprocess server = socket.socket() ip_port = ("127.0.0.1", 8001) server.bind(ip_port) server.listen() conn, addr = server.accept() while 1: cmd = conn.recv(1024).decode("utf-8") res = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) conn.send(res.stdout.read())server端
客戶端:
import socket client = socket.socket() ip_port = ("127.0.0.1", 8001) client.connect(ip_port) while 1: cmd = input(">>>").strip().encode("utf-8") client.send(cmd) msg = client.recv(1024) print(msg.decode("gbk"))client端
結果:
>>>ipconfig Windows IP 配置 無線區域網適配器 本地連接* 2: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開連接 連接特定的 DNS 尾碼 . . . . . . . : 乙太網適配器 乙太網: 連接特定的 DNS 尾碼 . . . . . . . : 本地鏈接 IPv6 地址. . . . . . . . : fe80::642e:112d:ce7:7ce4%14 IPv4 地址 . . . . . . . . . . . . : 192.168.12.55 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 預設網關. . . . . . . . . . . . . : 192.168.12.254 乙太網適配器 VMware Network Adapter VMnet1: 連接特定的 DNS 尾碼 . . . . . . . : 本地鏈接 IPv6 地址. . . . . . . . : fe80::89c5:67be:1c8f:1493%18 IPv4 地址 . . . . . . . . . . . . : 192.168.75.1 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 預設網關. . . . . . . . . . . . . : 乙太網適配器 VMware Network Adapter VMnet8: 連接特定的 DNS 尾碼 . . . . . . . : 本地鏈接 IPv6 地址. . . . . . . . : fe80::7d7e:c23:b290:1420%12 IPv4 地址 . . . . . . . . . . . . : 192.168.174.1 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 >>>dir 預設網關. . . . . . . . . . . . . : 無線區域網適配器 WLAN: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開連接 連接特定的 DNS 尾碼 . . . . . . . : 隧道適配器 本地連接* 11: 連接特定的 DNS 尾碼 . . . . . . . : IPv6 地址 . . . . . . . . . . . . : 2001:0:9d38:953c:8be:267a:3f57:f3c8 本地鏈接 IPv6 地址. . . . . . . . : fe80::8be:267a:3f57:f3c8%5 預設網關. . . . . . . . . . . . . : ::結果
由結果可知,在輸入dir命令時,列印的還是上次ipconfig的數據,由此產生粘包
2) 第二種:發送端需要等緩衝區滿才發送出去,造成粘包(發送數據時間間隔很短,數據也很小,會合到一起發送,產生粘包)
服務端:
import socket server = socket.socket() ip_port = ("127.0.0.1", 8001) server.bind(ip_port) server.listen() conn, addr = server.accept() from_client_msg1 = conn.recv(1024).decode("utf-8") from_client_msg2 = conn.recv(1024).decode("utf-8") print("from_client_msg1>>>", from_client_msg1) print("from_client_msg2>>>", from_client_msg2) conn.close() server.close()server端
客戶端:
import socket client = socket.socket() server_ip_port = ("127.0.0.1", 8001) client.connect(server_ip_port) client.send(b"11") client.send(b"22") client.close()client端
結果:
如果兩次發送有一定的時間間隔,那麼就不會出現這種粘包情況。
三、粘包的解決方案
產生粘包現象的根源在於接收端不知道發送端要傳送的位元組流長度。
1. 方案一:
發送端發送數據之前,先將數據長度讓接收端知曉,接收端根據總長度利用迴圈完成消息的接收。
服務端:
import socket import subprocess server = socket.socket() ip_port = ("127.0.0.1", 8001) server.bind(ip_port) server.listen() conn, addr = server.accept() while 1: from_client_cmd = conn.recv(1024).decode("utf-8") sub_obj = subprocess.Popen( from_client_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) cmd_res = sub_obj.stdout.read() conn.send(str(len(cmd_res)).encode("utf-8")) # 將數據長度發送給接收方 client_stutas = conn.recv(1024).decode("utf-8") # 接收接收方確認結果 if client_stutas == "ok": # 如果確認結果為"ok",則發送數據 conn.send(cmd_res) else: print("客戶端長度信息沒有收到")server端
客戶端:
import socket client = socket.socket() server_ip_port = ("127.0.0.1", 8001) client.connect(server_ip_port) while 1: client_cmd = input("請輸入系統指令>>>").strip().encode("utf-8") client.send(client_cmd) # 發送指令 from_server_datalen = client.recv(1024).decode("utf-8") # 接收數據長度信息 client.send(b"ok") # 發送確認信息 from_server_result = client.recv(int(from_server_datalen)) print(from_server_result.decode("gbk"))client端
結果:
請輸入系統指令>>>ipconfig -all Windows IP 配置 主機名 . . . . . . . . . . . . . : DESKTOP-1E4JOLI 主 DNS 尾碼 . . . . . . . . . . . : 節點類型 . . . . . . . . . . . . : 混合 IP 路由已啟用 . . . . . . . . . . : 否 WINS 代理已啟用 . . . . . . . . . : 否 無線區域網適配器 本地連接* 2: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開連接 連接特定的 DNS 尾碼 . . . . . . . : 描述. . . . . . . . . . . . . . . : Microsoft Wi-Fi Direct Virtual Adapter 物理地址. . . . . . . . . . . . . : 16-6D-57-0C-4D-7E DHCP 已啟用 . . . . . . . . . . . : 是 自動配置已啟用. . . . . . . . . . : 是 乙太網適配器 乙太網: 連接特定的 DNS 尾碼 . . . . . . . : 描述. . . . . . . . . . . . . . . : Realtek PCIe GBE Family Controller 物理地址. . . . . . . . . . . . . : F0-DE-F1-DF-A4-FB DHCP 已啟用 . . . . . . . . . . . : 是 自動配置已啟用. . . . . . . . . . : 是 本地鏈接 IPv6 地址. . . . . . . . : fe80::642e:112d:ce7:7ce4%14(首選) IPv4 地址 . . . . . . . . . . . . : 192.168.12.55(首選) 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 獲得租約的時間 . . . . . . . . . : 2018年11月22日 18:43:32 租約過期的時間 . . . . . . . . . : 2018年11月24日 14:41:01 預設網關. . . . . . . . . . . . . : 192.168.12.254 DHCP 伺服器 . . . . . . . . . . . : 192.168.12.254 DHCPv6 IAID . . . . . . . . . . . : 217112305 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-23-50-F6-63-F0-DE-F1-DF-A4-FB DNS 伺服器 . . . . . . . . . . . : 202.96.134.33 202.96.128.86 TCPIP 上的 NetBIOS . . . . . . . : 已啟用 乙太網適配器 VMware Network Adapter VMnet1: 連接特定的 DNS 尾碼 . . . . . . . : 描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet1 物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-01 DHCP 已啟用 . . . . . . . . . . . : 是 自動配置已啟用. . . . . . . . . . : 是 本地鏈接 IPv6 地址. . . . . . . . : fe80::89c5:67be:1c8f:1493%18(首選) IPv4 地址 . . . . . . . . . . . . : 192.168.75.1(首選) 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 獲得租約的時間 . . . . . . . . . : 2018年11月23日 14:40:54 租約過期的時間 . . . . . . . . . : 2018年11月23日 18:10:54 預設網關. . . . . . . . . . . . . : DHCP 伺服器 . . . . . . . . . . . : 192.168.75.254 DHCPv6 IAID . . . . . . . . . . . : 587223126 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-23-50-F6-63-F0-DE-F1-DF-A4-FB DNS 伺服器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 fec0:0:0:ffff::2%1 fec0:0:0:ffff::3%1 TCPIP 上的 NetBIOS . . . . . . . : 已啟用 乙太網適配器 VMware Network Adapter VMnet8: 連接特定的 DNS 尾碼 . . . . . . . : 描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet8 物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-08 DHCP 已啟用 . . . . . . . . . . . : 是 自動配置已啟用. . . . . . . . . . : 是 本地鏈接 IPv6 地址. . . . . . . . : fe80::7d7e:c23:b290:1420%12(首選) IPv4 地址 . . . . . . . . . . . . : 192.168.174.1(首選) 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 獲得租約的時間 . . . . . . . . . : 2018年11月23日 14:40:53 租約過期的時間 . . . . . . . . . : 2018年11月23日 18:10:54 預設網關. . . . . . . . . . . . . : DHCP 伺服器 . . . . . . . . . . . : 192.168.174.254 DHCPv6 IAID . . . . . . . . . . . : 604000342 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-23-50-F6-63-F0-DE-F1-DF-A4-FB DNS 伺服器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 fec0:0:0:ffff::2%1 fec0:0:0:ffff::3%1 主 WINS 伺服器 . . . . . . . . . : 192.168.174.2 TCPIP 上的 NetBIOS . . . . . . . : 已啟用 無線區域網適配器 WLAN: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開連接 連接特定的 DNS 尾碼 . . . . . . . : 描述. . . . . . . . . . . . . . . : Qualcomm Atheros AR9285 Wireless Network Adapter 物理地址. . . . . . . . . . . . . : 44-6D-57-0C-4D-7E DHCP 已啟用 . . . . . . . . . . . : 是 自動配置已啟用. . . . . . . . . . : 是 隧道適配器 本地連接* 11: 連接特定的 DNS 尾碼 . . . . . . . : 描述. . . . . . . . . . . . . . . : Microsoft Teredo Tunneling Adapter 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP 已啟用 . . . . . . . . . . . : 否 自動配置已啟用. . . . . . . . . . : 是 IPv6 地址 . . . . . . . . . . . . : 2001:0:9d38:953c:8be:267a:3f57:f3c8(首選) 本地鏈接 IPv6 地址. . . . . . . . : fe80::8be:267a:3f57:f3c8%5(首選) 預設網關. . . . . . . . . . . . . : :: DHCPv6 IAID . . . . . . . . . . . : 83886080 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-23-50-F6-63-F0-DE-F1-DF-A4-FB TCPIP 上的 NetBIOS . . . . . . . : 已禁用 請輸入系統指令>>>dir 驅動器 E 中的捲沒有標簽。 捲的序列號是 A473-0ACA E:\python_個人\day 028 粘包現象\第二種 的目錄 2018/11/23 17:51 <DIR> . 2018/11/23 17:51 <DIR> .. 2018/11/23 17:51 500 客戶端.py 2018/11/23 17:47 810 服務端.py 2 個文件 1,310 位元組 2 個目錄 123,297,423,360 可用位元組 請輸入系統指令>>>結果
此時,數據之間就不存在粘包現象了。
2. 方案二:
通過struck模塊將需要發送的內容的長度進行打包成一個4位元組長度的數據發送給接收端,接收端只要取出前4個位元組,然後對這個位元組的數據進行解包,拿到你要發送的內容的長度,然後通過這個長度來繼續接收我們實際要發送的內容。
1. struct模塊
對python基本類型值與用python字元串格式表示的c struct類型間的轉化。
Format | C Type | Python type | Standard size | Notes |
x | pad type | no value | ||
c | char | string of length 1 | 1 | |
b | char | integer | 1 | (3) |
B | unsigned char | integer | 1 | (3) |
? | _Bool | bool | 1 | (1) |
h | short | integer | 2 | (3) |
H | unsigned short | integer | 2 | (3) |
i | int | integer | 4 | (3) |
I | unsigned int | integer | 4 | (3) |
l | long | integer | 4 | (3) |
L | unsigned long | integer | 4 | (3) |
q | long long | integer | 8 | (2),(3) |
Q | unsigned long long | integer | 8 | (2),(3) |
f | float | float | 4 | (4) |
d | double | float | 8 | (4) |
s | char[] | string | ||
P | char[] | string | ||
p | void * | integer | (5),(3) |
- pack(format, value):打包,對於int類型,只能打包[-2147483648,2147483647]範圍內數據,否則報錯struct.error錯誤。對於int類型,打包後得到是4位元組的bytes類型
- unpack(format, string):解包,將bytes類型轉化為format對應的類型,返回的結果是元組
import struct a = 10 print(struct.pack("i", a)) # b'\n\x00\x00\x00' b = b'\n\x00\x00\x00' print(struct.unpack("i", b)) # (10,)
服務端:
import socket import subprocess import struct server = socket.socket() ip_port = ("127.0.0.1", 8001) server.bind(ip_port) # 綁定埠 server.listen() # 監聽 conn, addr = server.accept() # 等待連接 while 1: from_client_cmd = conn.recv(1024).decode("utf-8") # 接收消息 sub_obj = subprocess.Popen( from_client_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) cmd_res = sub_obj.stdout.read() # 得到的是bytes類型 data_len = len(cmd_res) # 數據長度 print(data_len) # 列印長度 # 將真實數據長度打包成4個位元組的數據 struct_data_len = struct.pack("i", data_len) # 將長度信息和數據內容發送給客戶端 conn.send(struct_data_len + cmd_res)server端
客戶端:
import socket import struct client = socket.socket() server_ip_port = ("127.0.0.1", 8001) client.connect(server_ip_port) # 連接 while 1: client_cmd = input("請輸入系統指令>>>").strip() client.send(client_cmd.encode("utf-8")) # 發送消息 # 先接收4個位元組,得到數據內容長度 recv_data_len = client.recv(4) # 將4個位元組長度的數據,解包成後面真實數據的長度 real_data_len = struct.unpack("i", recv_data_len)[0] print(real_data_len) # 列印長度 server_result = client.recv(real_data_len) # 接收數據內容 print(server_result.decode("gbk"))client端