python/socket編程之粘包
python/socket編程之粘包
粘包:
只有TCP有尿包現象,UDP永遠不會粘包。
首先需要掌握一個socket收發消息的原理
發送端可以是1k,1k的發送數據而接受端的應用程序可以2k,2k的提取數據,當然也有可能是3k或者多k提取數據,也就是說,應用程序是不可見的,因此TCP協議是面來那個流的協議,這也是容易出現粘包的原因而UDP是面向笑死的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任一字節的數據,這一點和TCP是很同的。怎樣定義消息呢?認為對方一次性write/send的數據為一個消息,需要命的是當對方send一條信息的時候,無論鼎城怎麽樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈現在內核緩沖區。
例如基於TCP的套接字客戶端往服務器端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看來更笨不知道文件的字節流從何初開始,在何處結束。
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的
發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多數據後才發上一個TCP段。如連續幾次下需要send的數據都很少,通常TCP會根據優化算法把 這些數據合成一個TCP段後 一次發送出去,這樣接收方就收到了粘包數據
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
- tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略
udp的recvfrom是阻塞的,一個recvfrom(x)必須對一個一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味著udp根本不會粘包,但是會丟數據,不可靠
tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
兩種情況下會發生粘包:
1.發送端需要等本機的緩沖區滿了以後才發送出去,造成粘包(發送數據時間間隔很端,數據很小,會合在一個起,產生粘包)
2.接收端不及時接收緩沖區的包,造成多個包接受(客戶端發送一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據 ,就產生粘包)
粘包實例:
服務端 import socket import subprocess din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=(‘127.0.0.1‘,8080) din.bind(ip_port) din.listen(5) conn,deer=din.accept() data1=conn.recv(1024) data2=conn.recv(1024) print(data1) print(data2)
客戶端 import socket import subprocess din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=(‘127.0.0.1‘,8080) din.connect(ip_port) din.send(‘helloworld‘.encode(‘utf-8‘)) din.send(‘sb‘.encode(‘utf-8‘))
low比的解決粘包的方法:
在客戶端發送下邊添加一個時間睡眠,就可以避免粘包現象。在服務端接收的時候也要進行時間睡眠,才能有效的避免粘包情況
#客戶端 import socket import time import subprocess din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=(‘127.0.0.1‘,8080) din.connect(ip_port) din.send(‘helloworld‘.encode(‘utf-8‘)) time.sleep(3) din.send(‘sb‘.encode(‘utf-8‘))
#服務端 import socket import time import subprocess din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=(‘127.0.0.1‘,8080) din.bind(ip_port) din.listen(5) conn,deer=din.accept() data1=conn.recv(1024) time.sleep(4) data2=conn.recv(1024) print(data1) print(data2)
大聲解決粘包的方法:
為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然後依次send到對端,對端在接受時,先從緩存中取出定長的報頭,然後再取真是數據。
使用struct模塊
該模塊可以把一個類型,如數字,轉成固定長度的bytes字節
struct模塊
該模塊可以把一個類型,如數字,轉成固定長度的bytes>>> res=struct.pack(‘i‘,1111111111111) #打包成固定長度的bytes
>>> struct.unpack(“I”,res) #解包
采用自定義報頭的形式:
#服務端 import socket #導入模塊 import struct #導入模塊 import subprocess #導入模塊 din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #基於網路家族以數據流的形式傳輸 ip=(‘127.0.0.1‘,8080) #標識服務端唯一的地址 din.bind(ip) #綁定地址 din.listen(5) #設置鏈接緩沖池最大數量 while True: conn,addr=din.accept() #等待鏈接進入 print(‘------>‘,addr) while True: try: #開始捕捉異常 cmd=conn.recv(1024) #把接收的內容賦值給變量cmd res=subprocess.Popen(cmd.decode(‘utf-8‘),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) #設置一個管道,以shell的方式寫入,判斷如果是正確的命令就放到標準輸出的管道中,否則就放到錯誤輸出的管道中 dui=res.stdout.read() #讀取標準輸出管道中的內容賦值給dui cuo=res.stderr.read() #讀取錯誤輸出管道中的內容賦值給cuo data_bat=len(dui)+len(cuo) #把dui和cuo的內容長度相加賦值給data_bat conn.send(struct.pack(‘i‘,data_bat)) #通過模塊模仿報頭形式發送給客戶端 conn.send(dui) #把dui的發送給客戶端 conn.send(cuo) #把cuo的發送給客戶端 except Exception: #結束捕捉異常 break #跳出 conn.close() #斷開鏈接 din.close() #關閉基於網絡家族傳輸的鏈接
#客戶端 import socket import struct din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=(‘127.0.0.1‘,8080) din.connect(ip_port) while True: cmd=input(‘請輸入命令:‘).strip() if not cmd:continue din.send(bytes(cmd,encoding=‘utf-8‘)) baotou=din.recv(4) data_size=struct.unpack(‘i‘,baotou)[0] rec_size=0 rec_data=b‘‘ while rec_size < data_size: data=din.recv(1024) rec_size+=len(data) rec_data+=data print(rec_data.decode(‘gbk‘)) din.close()
牛逼報頭的形式:
服務端
import struct import socket import json import subprocess din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip=(‘127.0.0.1‘,8080) din.bind(ip) din.listen(5) while True: conn,addr=din.accept() print(‘------->>>‘) while True: try: dr=conn.recv(1024) res=subprocess.Popen(dr.decode(‘utf-8‘),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) dui=res.stdout.read() cuo=res.stderr.read() data_lik=len(dui)+len(cuo) head_dic={‘data_lik‘:data_lik} head_json=json.dumps(head_dic) head_bytes=head_json.encode(‘utf-8‘) head_len=len(head_bytes) #發送報頭長度 conn.send(struct.pack(‘i‘,head_len)) # 發送報頭 conn.send(head_bytes) #發送真是數據 conn.send(dui) conn.send(cuo) except Exception: break conn.close() din.close()
客戶端
import struct import socket import json din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip=(‘127.0.0.1‘,8080) din.connect(ip) while True: run=input(‘請輸入命令:‘) if not run:continue din.send(bytes(run,encoding=‘utf-8‘)) data=din.recv(4) head_len=struct.unpack(‘i‘,data)[0] head_bytes=din.recv(head_len) head_json=head_bytes.decode(‘utf-8‘) head_dic=json.loads(head_json) data_lik=head_dic[‘data_lik‘] recv_size=0 recv_data=b‘‘ while recv_size < data_lik: data=din.recv(1024) recv_size+=len(data) recv_data+=data print(recv_data.decode(‘gbk‘)) din.close()
python/socket編程之粘包