socket收發消息的原理python
應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。linux
而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。怎樣定義消息呢?算法
能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。shell
#1:不論是recv仍是send都不是直接接收對方的數據,而是操做本身的操做系統內存--->不是一個send對應一個recv #2:recv: wait data 耗時很是長 copy data send: copy data
例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束編程
所謂粘包問題主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的。json
只有TCP有粘包現象,UDP永遠不會粘包windows
粘包不必定會發生:服務器
若是發生了:1.多是在客戶端已經粘了網絡
2.客戶端沒有粘,多是在服務端粘了dom
客戶端粘包:
發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據量很小,TCP優化算法會當作一個包發出去,產生粘包)
client端: import socket import time client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',9904)) client.send('hello'.encode('utf-8')) client.send('world'.encode('utf-8')) server端: import socket import time server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',9904)) #0-65535:0-1024給操做系統使用 server.listen(5) conn, addr=server.accept() print('connect by ',addr) res1 = conn.recv(100) print('第一次',res1) res2=conn.recv(10) print('第二次', res2)
服務端輸出結果
connect by ('127.0.0.1', 9787) 第一次 b'helloworld' 第二次 b''
服務端粘包
接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)
server端: import socket import time server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',9904)) #0-65535:0-1024給操做系統使用 server.listen(5) conn, addr=server.accept() print('connect by ',addr) res1 = conn.recv(2)#第一沒有接收完整 print('第一次',res1) time.sleep(6) res2=conn.recv(10)# 第二次會接收舊數據,再收取新的 print('第二次', res2) client端 import socket import time client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',9904)) client.send('hello'.encode('utf-8')) time.sleep(5) client.send('world'.encode('utf-8'))
服務端輸出
connect by ('127.0.0.1', 10184) 第一次 b'he' 第二次 b'lloworld'
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是發送端在發送數據前,發一個頭文件包,告訴發送的字節流總大小,而後接收端來一個死循環接收完全部數據
使用struct模塊能夠用於將Python的值根據格式符,轉換爲字符串(byte類型)
struct模塊中最重要的三個函數是pack(), unpack(), calcsize()
pack(fmt, v1, v2, ...) 按照給定的格式(fmt),把數據封裝成字符串(其實是相似於c結構體的字節流)
unpack(fmt, string) 按照給定的格式(fmt)解析字節流string,返回解析出來的tuple
calcsize(fmt) 計算給定的格式(fmt)佔用多少字節的內存
struct中支持的格式以下表:
Format | C Type | Python | 字節數 |
---|---|---|---|
x | pad byte | no value | 1 |
c | char | string of length 1 | 1 |
b | signed char | integer | 1 |
B | unsigned char | integer | 1 |
? | _Bool | bool | 1 |
h | short | integer | 2 |
H | unsigned short | integer | 2 |
i | int | integer | 4 |
I | unsigned int | integer or long | 4 |
l | long | integer | 4 |
L | unsigned long | long | 4 |
q | long long | long | 8 |
Q | unsigned long long | long | 8 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | string | 1 |
使用案例
import struct res = struct.pack('i',123) print(res,type(res), len(res)) # b'{\x00\x00\x00' <class 'bytes'> 4 封裝一個4個字節的包 res1=struct.pack('q',11122232323) print(res1,type(res1), len(res1)) # b'\x03\xcc\xef\x96\x02\x00\x00\x00' <class 'bytes'> 8 封裝一個8個字節的包 print(struct.unpack('i',res)[0]) # 拆包 print(struct.unpack('q',res1)[0]) # #輸出 # b'{\x00\x00\x00' <class 'bytes'> 4 # b'\x03\xcc\xef\x96\x02\x00\x00\x00' <class 'bytes'> 8 # (123,) # (11122232323,)
server
import socket import subprocess import struct def cmd_exec(cmd): """ 執行shell命令 :param cmd: :return: """ p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) stdout, stderr = p.communicate() if p.returncode != 0: return stderr return stdout sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用地址端口 sock_server.bind(('127.0.0.1', 8088)) sock_server.listen(1) # 開始監聽,1表明在容許有一個鏈接排隊,更多的新鏈接連進來時就會被拒絕 print('starting...') while True: conn, client_addr = sock_server.accept() # 阻塞直到有鏈接爲止,有了一個新鏈接進來後,就會爲這個請求生成一個鏈接對象 print(client_addr) while True: try: data = conn.recv(1024) # 接收1024個字節 if not data: break # 適用於linux操做系統,防止客戶端斷開鏈接後死循環 print('客戶端的命令', data.decode('gbk')) res = cmd_exec(data.decode('gbk')) # 執行cmd命令 # 第一步:製做固定長度的報頭4bytes total_size = len(res) header = struct.pack('i',total_size) # 第二步:把報頭髮送給客戶端 conn.send(header) # 第三步:再發送真實的數據 conn.sendall(res) except ConnectionResetError: # 適用於windows操做系統,防止客戶端斷開鏈接後死循環 break conn.close() server.close()
client
import socket import struct client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(client) client.connect(('127.0.0.1', 8088)) while True: data = input('input >>>') if not data: # 若是數據爲空,繼續輸入 continue client.send(data.encode('GBK')) # 發送數據 # 第一步:先收報頭 header = client.recv(4) # 第二步:從報頭中解析出對真實數據的描述信息(數據的長度) total_size = struct.unpack('i',header)[0] print('收到數據長度=',total_size) # 第三步:接收真實的數據 recv_size = 0 recv_data = b'' while recv_size < total_size: data = client.recv(1024) # 接收數據 recv_data += data recv_size += len(data) # 不能加1024,若是加進度條,會計算有誤 print('接收數據 =', recv_data.decode('gbk', 'ignore')) # 若是設置爲ignore,則會忽略非法字符; client.close() # 關閉
輸出結果:
server端 starting... ('127.0.0.1', 13338) 客戶端的命令 dir 客戶端的命令 ipconfig/all client端: "C:\Program Files\Python36\python.exe" "路飛/第三模塊/第二章網絡編程/01 簡單的套接字通訊/客戶端.py" <socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0> input >>>dir 收到數據長度= 477 接收數據 = 驅動器 C 中的卷是 BOOTCAMP 卷的序列號是 D471-4F4D C:路飛\第三模塊\第二章網絡編程\01 簡單的套接字通訊 的目錄 2018/07/07 14:02 <DIR> . 2018/07/07 14:02 <DIR> .. 2018/07/05 22:43 594 cmd_util.py 2018/07/07 14:02 971 客戶端.py 2018/07/07 14:01 1,673 服務端.py 3 個文件 3,238 字節 2 個目錄 28,749,410,304 可用字節 input >>>ipconfig/all 收到數據長度= 7702 接收數據 = Windows IP 配置 主機名 . . . . . . . . . . . . . : PC 主 DNS 後綴 . . . . . . . . . . . : 節點類型 . . . . . . . . . . . . : 混合 IP 路由已啓用 . . . . . . . . . . : 否 WINS 代理已啓用 . . . . . . . . . : 否 以太網適配器 本地鏈接 3: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : Bluetooth PAN Network Adapter 物理地址. . . . . . . . . . . . . : 60-F8-1D-zz-89-EF DHCP 已啓用 . . . . . . . . . . . : 是 自動配置已啓用. . . . . . . . . . : 是 無線局域網適配器 無線網絡鏈接: 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : Broadcom 802.11ac Network Adapter 物理地址. . . . . . . . . . . . . : 60-F8-1D-AD-zz-EE DHCP 已啓用 . . . . . . . . . . . : 是 自動配置已啓用. . . . . . . . . . : 是 本地連接 IPv6 地址. . . . . . . . : fe80::55d1:e185:f929:8ce3%13(首選) IPv4 地址 . . . . . . . . . . . . : 192.168.31.125(首選) 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 得到租約的時間 . . . . . . . . . : 2018年7月7日 9:27:54 租約過時的時間 . . . . . . . . . : 2018年7月8日 1:25:52 默認網關. . . . . . . . . . . . . : 192.168.31.1 DHCP 服務器 . . . . . . . . . . . : 192.168.31.1 DHCPv6 IAID . . . . . . . . . . . : 291567645 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-zz-7C-0D-6E-60-F8-1D-AD-89-EE DNS 服務器 . . . . . . . . . . . : 114.114.114.114 TCPIP 上的 NetBIOS . . . . . . . : 已啓用 以太網適配器 VirtualBox Host-Only Network: 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : VirtualBox Host-Only Ethernet Adapter 物理地址. . . . . . . . . . . . . : 0A-00-27-00-zz-13 DHCP 已啓用 . . . . . . . . . . . : 否 自動配置已啓用. . . . . . . . . . : 是 本地連接 IPv6 地址. . . . . . . . : fe80::7d26:2c96:84f1:6c4d%19(首選) 自動配置 IPv4 地址 . . . . . . . : 169.254.108.77(首選) 子網掩碼 . . . . . . . . . . . . : 255.255.0.0 默認網關. . . . . . . . . . . . . : 192.168.56.255 DHCPv6 IAID . . . . . . . . . . . : 336199719 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-21-7C-0zz60-F8-1D-AD-89-EE DNS 服務器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 fec0:0:0:ffff::2%1 fec0:0:0:ffff::3%1 TCPIP 上的 NetBIOS . . . . . . . : 已啓用 以太網適配器 VirtualBox Host-Only Network #2: 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : VirtualBox Host-Only Ethernet Adapter #2 物理地址. . . . . . . . . . . . . : 0A-00-27-00-00-14 DHCP 已啓用 . . . . . . . . . . . : 否 自動配置已啓用. . . . . . . . . . : 是 本地連接 IPv6 地址. . . . . . . . : fe80::641c:b67e:fa43:a28d%20(首選) IPv4 地址 . . . . . . . . . . . . : 192.168.96.1(首選) 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 默認網關. . . . . . . . . . . . . : DHCPv6 IAID . . . . . . . . . . . : 537526311 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-21-7C-0D-6E-60-F8-1D-AD-89-EE DNS 服務器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 fec0:0:0:ffff::2%1 fec0:0:0:ffff::3%1 TCPIP 上的 NetBIOS . . . . . . . : 已啓用 以太網適配器 VMware Network Adapter VMnet1: 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet1 物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-01 DHCP 已啓用 . . . . . . . . . . . : 是 自動配置已啓用. . . . . . . . . . : 是 本地連接 IPv6 地址. . . . . . . . : fe80::20c1:b2f0:8bff:626c%25(首選) IPv4 地址 . . . . . . . . . . . . : 192.168.109.1(首選) 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 得到租約的時間 . . . . . . . . . : 2018年7月7日 9:27:50 租約過時的時間 . . . . . . . . . : 2018年7月7日 14:27:49 默認網關. . . . . . . . . . . . . : DHCP 服務器 . . . . . . . . . . . : 192.168.109.254 DHCPv6 IAID . . . . . . . . . . . : 385896534 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-21-7C-0D-6E-60-F8-1D-AD-89-EE 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-56zz-00-08 DHCP 已啓用 . . . . . . . . . . . : 是 自動配置已啓用. . . . . . . . . . : 是 本地連接 IPv6 地址. . . . . . . . : fe80::61fd:5f66:1f70:cb3d%26(首選) IPv4 地址 . . . . . . . . . . . . : 192.168.5.1(首選) 子網掩碼 . . . . . . . . . . . . : 255.255.255.0 得到租約的時間 . . . . . . . . . : 2018年7月7日 9:27:49 租約過時的時間 . . . . . . . . . : 2018年7月7日 14:27:48 默認網關. . . . . . . . . . . . . : DHCP 服務器 . . . . . . . . . . . : 192.168.5.254 DHCPv6 IAID . . . . . . . . . . . : 402673750 DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-21-7C-0D-6E-60-F8-1D-AD-89-EE DNS 服務器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 fec0:0:0:ffff::2%1 fec0:0:0:ffff::3%1 主 WINS 服務器 . . . . . . . . . : 192.168.5.2 TCPIP 上的 NetBIOS . . . . . . . : 已啓用 隧道適配器 本地鏈接* 14: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #2 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP 已啓用 . . . . . . . . . . . : 否 自動配置已啓用. . . . . . . . . . : 是 隧道適配器 Teredo Tunneling Pseudo-Interface: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : Teredo Tunneling Pseudo-Interface 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP 已啓用 . . . . . . . . . . . : 否 自動配置已啓用. . . . . . . . . . : 是 隧道適配器 isatap.{0DA4A980-7247-4922-AAFB-55760B865C15}: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #3 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP 已啓用 . . . . . . . . . . . : 否 自動配置已啓用. . . . . . . . . . : 是 隧道適配器 isatap.localdomain: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #5 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP 已啓用 . . . . . . . . . . . : 否 自動配置已啓用. . . . . . . . . . : 是 隧道適配器 本地鏈接* 15: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #6 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP 已啓用 . . . . . . . . . . . : 否 自動配置已啓用. . . . . . . . . . : 是 隧道適配器 isatap.{94C5F926-3E20-4589-A88E-54A36934D42C}: 媒體狀態 . . . . . . . . . . . . : 媒體已斷開 鏈接特定的 DNS 後綴 . . . . . . . : 描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #8 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP 已啓用 . . . . . . . . . . . : 否 自動配置已啓用. . . . . . . . . . : 是 input >>>
server端
import socket import subprocess import struct import json def cmd_exec(cmd): """ 執行shell命令 :param cmd: :return: """ p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) stdout, stderr = p.communicate() if p.returncode != 0: return stderr return stdout sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用地址端口 sock_server.bind(('127.0.0.1', 8088)) sock_server.listen(1) # 開始監聽,1表明在容許有一個鏈接排隊,更多的新鏈接連進來時就會被拒絕 print('starting...') while True: conn, client_addr = sock_server.accept() # 阻塞直到有鏈接爲止,有了一個新鏈接進來後,就會爲這個請求生成一個鏈接對象 print(client_addr) while True: try: data = conn.recv(1024) # 接收1024個字節 if not data: break # 適用於linux操做系統,防止客戶端斷開鏈接後死循環 print('客戶端的命令', data.decode('gbk')) res = cmd_exec(data.decode('gbk')) # 執行cmd命令 # 第一步:製做固定長度的報頭dict header_dict ={ 'filename':'文件名', 'md5':'md5值', 'total_size':len(res) } header_json = json.dumps(header_dict, ensure_ascii='False',indent=2) # 序列化json print(header_json) header_bytes = header_json.encode('utf-8') header = struct.pack('i', len(header_bytes)) # 第二步:把報頭長度發送給客戶端 conn.send(header) # 第三步:把報頭內容發送給客戶端 conn.send(header_bytes) # 第四步:再發送真實的數據 conn.sendall(res) except ConnectionResetError: # 適用於windows操做系統,防止客戶端斷開鏈接後死循環 break conn.close() server.close()
client端
import socket import struct import json client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(client) client.connect(('127.0.0.1', 8088)) while True: data = input('input >>>') if not data: # 若是數據爲空,繼續輸入 continue client.send(data.encode('GBK')) # 發送數據 # 第一步:先收報頭 header = client.recv(4) # 第二步:從報頭中解析(header數據的長度) header_size = struct.unpack('i',header)[0] print('收到報頭長度=', header_size) # 第三步:收到報頭解析出對真實數據的描述信息 header_json = client.recv(header_size) header_dict = json.loads(header_json) print('收到報頭內容=',header_dict) total_size = header_dict['total_size'] # 第三步:接收真實的數據 recv_size = 0 recv_data = b'' while recv_size < total_size: data = client.recv(1024) # 接收數據 recv_data += data recv_size += len(data) # 不能加1024,若是加進度條,會計算有誤 print('接收數據 =', recv_data.decode('gbk', 'ignore')) # 若是設置爲ignore,則會忽略非法字符; client.close() # 關閉
結果
server端 starting... ('127.0.0.1', 15685) 客戶端的命令 ls { "filename": "\u6587\u4ef6\u540d", "md5": "md5\u503c", "total_size": 61 } 客戶端的命令 dir { "filename": "\u6587\u4ef6\u540d", "md5": "md5\u503c", "total_size": 477 } client端 <socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0> input >>>ls 收到報頭長度= 80 收到報頭內容= {'filename': '文件名', 'md5': 'md5值', 'total_size': 61} 接收數據 = 'ls' 不是內部或外部命令,也不是可運行的程序 或批處理文件。 input >>>dir 收到報頭長度= 81 收到報頭內容= {'filename': '文件名', 'md5': 'md5值', 'total_size': 477} 接收數據 = 驅動器 C 中的卷是 BOOTCAMP 卷的序列號是 D471-4F4D 簡單的套接字通訊 的目錄 2018/07/07 14:51 <DIR> . 2018/07/07 14:51 <DIR> .. 2018/07/05 22:43 594 cmd_util.py 2018/07/07 14:51 1,204 客戶端.py 2018/07/07 14:51 2,098 服務端.py 3 個文件 3,896 字節 2 個目錄 28,694,999,040 可用字節 input >>>ipconfig/all 收到報頭長度= 82 收到報頭內容= {'filename': '文件名', 'md5': 'md5值', 'total_size': 7702} 接收數據 = Windows IP 配置……