您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關python中socket網絡編程之粘包的示例分析的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
一,粘包問題詳情
1,只有TCP有粘包現象,UDP永遠不會粘包
你的程序實際上無權直接操作網卡的,你操作網卡都是通過操作系統給用戶程序暴露出來的接口,那每次你的程序要給遠程發數據時,其實是先把數據從用戶態copy到內核態,這樣的操作是耗資源和時間的,頻繁的在內核態和用戶態之前交換數據勢必會導致發送效率降低, 因此socket 為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一次數據給對方。若連續幾次需要send的數據都很少,通常TCP socket 會根據優化算法把這些數據合成一個TCP段后一次發送出去,這樣接收方就收到了粘包數據。
2,首先需要掌握一個socket收發消息的原理
發送端可以是1k,1k的發送數據而接受端的應用程序可以2k,2k的提取數據,當然也有可能是3k或者多k提取數據,也就是說,應用程序是不可見的,因此TCP協議是面來那個流的協議,這也是容易出現粘包的原因而UDP是面向無連接的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任一字節的數據,這一點和TCP是很同的。怎樣定義消息呢?認為對方一次性write/send的數據為一個消息,需要命的是當對方send一條信息的時候,無論鼎城怎么樣分段分片,TCP協議層會把構成整條消息的數據段排序完成后才呈現在內核緩沖區。
例如基于TCP的套接字客戶端往服務器端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看來更笨不知道文件的字節流從何初開始,在何處結束。
3,粘包的原因
3-1 直接原因
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的
3-2 根本原因
發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據 優化算法 把這些數據合成一個TCP段后一次發送出去,這樣接收方就收到了粘包數據。
3-3 總結
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,發送端需要等到本機的緩沖區滿了以后才發出去,造成粘包(發送數據時間間隔很短,數據很小,python使用了優化算法,合在一起,產生粘包)
客戶端
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('feng'.encode('utf-8'))
服務端
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
2,接收端不及時接受緩沖區的包,造成多個包接受(客戶端發送一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,就產生粘包) 客戶端
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello feng'.encode('utf-8'))
服務端
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(2) #一次沒有收完整 data2=conn.recv(10)#下次收的時候,會先取舊的數據,然后取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
三,粘包實例:
服務端
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'))
四,拆包的發生情況
當發送端緩沖區的長度大于網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送過去
補充問題一:為何tcp是可靠傳輸,udp是不可靠傳輸
關于tcp傳輸請參考: https://www.jb51.net/network/176867.html
tcp在數據傳輸時,發送端先把數據發送到自己的緩存中,然后協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則重新發送數據,所以tcp是可靠的
而udp發送數據,對端是不會返回確認信息的,因此不可靠
補充問題二:send(字節流)和recv(1024)及sendall是什么意思?
recv里指定的1024意思是從緩存里一次拿出1024個字節的數據
send的字節流是先放入己端緩存,然后由協議控制將緩存內容發往對端,如果字節流大小大于緩存剩余空間,那么數據丟失,用sendall就會循環調用send,數據不會丟失。
五,粘包問題如何解決?
問題的根源在于,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據。
5-1 簡單的解決方法(從表面解決):
在客戶端發送下邊添加一個時間睡眠,就可以避免粘包現象。在服務端接收的時候也要進行時間睡眠,才能有效的避免粘包情況。
客戶端:
#客戶端 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)
上面解決方法肯定會出現很多紕漏,因為你不知道什么時候傳輸完,時間暫停的長短都會有問題,長的話效率低,短的話不合適,所以這種方法是不合適的。
5-2 普通的解決方法(從根本看問題):
問題的根源在于,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據
為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后依次send到對端,對端在接受時,先從緩存中取出定長的報頭,然后再取真是數據。
使用struct模塊對打包的長度為固定4個字節或者八個字節,struct.pack.format參數是“i”時,只能打包長度為10的數字,那么還可以先將長度轉化為json字符串,再打包。
普通的客戶端
# _*_ coding: utf-8 _*_ import socket import struct phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8880)) #連接服 while True: # 發收消息 cmd = input('請你輸入命令>>:').strip() if not cmd:continue phone.send(cmd.encode('utf-8')) #發送 #先收報頭 header_struct = phone.recv(4) #收四個 unpack_res = struct.unpack('i',header_struct) total_size = unpack_res[0] #總長度 #后收數據 recv_size = 0 total_data=b'' while recv_size<total_size: #循環的收 recv_data = phone.recv(1024) #1024只是一個最大的限制 recv_size+=len(recv_data) # total_data+=recv_data # print('返回的消息:%s'%total_data.decode('gbk')) phone.close()
普通的服務端
# _*_ coding: utf-8 _*_ import socket import subprocess import struct phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機 phone.bind(('127.0.0.1',8880)) #綁定手機卡 phone.listen(5) #阻塞的最大數 print('start runing.....') while True: #鏈接循環 coon,addr = phone.accept()# 等待接電話 print(coon,addr) while True: #通信循環 # 收發消息 cmd = coon.recv(1024) #接收的最大數 print('接收的是:%s'%cmd.decode('utf-8')) #處理過程 res = subprocess.Popen(cmd.decode('utf-8'),shell = True, stdout=subprocess.PIPE, #標準輸出 stderr=subprocess.PIPE #標準錯誤 ) stdout = res.stdout.read() stderr = res.stderr.read() #先發報頭(轉成固定長度的bytes類型,那么怎么轉呢?就用到了struct模塊) #len(stdout) + len(stderr)#統計數據的長度 header = struct.pack('i',len(stdout)+len(stderr))#制作報頭 coon.send(header) #再發命令的結果 coon.send(stdout) coon.send(stderr) coon.close() phone.close()
5-3 優化版的解決方法(從根本解決問題)
優化的解決粘包問題的思路就是服務端將報頭信息進行優化,對要發送的內容用字典進行描述,首先字典不能直接進行網絡傳輸,需要進行序列化轉成json格式化字符串,然后轉成bytes格式服務端進行發送,因為bytes格式的json字符串長度不是固定的,所以要用struct模塊將bytes格式的json字符串長度壓縮成固定長度,發送給客戶端,客戶端進行接受,反解就會得到完整的數據包。
終極版的客戶端
# _*_ coding: utf-8 _*_ import socket import struct import json phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) #連接服務器 while True: # 發收消息 cmd = input('請你輸入命令>>:').strip() if not cmd:continue phone.send(cmd.encode('utf-8')) #發送 #先收報頭的長度 header_len = struct.unpack('i',phone.recv(4))[0] #吧bytes類型的反解 #在收報頭 header_bytes = phone.recv(header_len) #收過來的也是bytes類型 header_json = header_bytes.decode('utf-8') #拿到json格式的字典 header_dic = json.loads(header_json) #反序列化拿到字典了 total_size = header_dic['total_size'] #就拿到數據的總長度了 #最后收數據 recv_size = 0 total_data=b'' while recv_size<total_size: #循環的收 recv_data = phone.recv(1024) #1024只是一個最大的限制 recv_size+=len(recv_data) #有可能接收的不是1024個字節,或許比1024多呢, # 那么接收的時候就接收不全,所以還要加上接收的那個長度 total_data+=recv_data #最終的結果 print('返回的消息:%s'%total_data.decode('gbk')) phone.close()
終極版的服務端
# _*_ coding: utf-8 _*_ import socket import subprocess import struct import json phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',8080)) #綁定手機卡 phone.listen(5) #阻塞的最大數 print('start runing.....') while True: #鏈接循環 coon,addr = phone.accept()# 等待接電話 print(coon,addr) while True: #通信循環 # 收發消息 cmd = coon.recv(1024) #接收的最大數 print('接收的是:%s'%cmd.decode('utf-8')) #處理過程 res = subprocess.Popen(cmd.decode('utf-8'),shell = True, stdout=subprocess.PIPE, #標準輸出 stderr=subprocess.PIPE #標準錯誤 ) stdout = res.stdout.read() stderr = res.stderr.read() # 制作報頭 header_dic = { 'total_size': len(stdout)+len(stderr), # 總共的大小 'filename': None, 'md5': None } header_json = json.dumps(header_dic) #字符串類型 header_bytes = header_json.encode('utf-8') #轉成bytes類型(但是長度是可變的) #先發報頭的長度 coon.send(struct.pack('i',len(header_bytes))) #發送固定長度的報頭 #再發報頭 coon.send(header_bytes) #最后發命令的結果 coon.send(stdout) coon.send(stderr) coon.close() phone.close()
六,struct模塊
了解c語言的人,一定會知道struct結構體在c語言中的作用,它定義了一種結構,里面包含不同類型的數據(int,char,bool等等),方便對某一結構對象進行處理。而在網絡通信當中,大多傳遞的數據是以二進制流(binary data)存在的。當傳遞字符串時,不必擔心太多的問題,而當傳遞諸如int、char之類的基本數據的時候,就需要有一種機制將某些特定的結構體類型打包成二進制流的字符串然后再網絡傳輸,而接收端也應該可以通過某種機制進行解包還原出原始的結構體數據。python中的struct模塊就提供了這樣的機制,該模塊的主要作用就是對python基本類型值與用python字符串格式表示的C struct類型間的轉化(This module performs conversions between Python values and C structs represented as Python strings.)。stuct模塊提供了很簡單的幾個函數,下面寫幾個例子。
1,基本的pack和unpack
struct提供用format specifier方式對數據進行打包和解包(Packing and Unpacking)。例如:
#該模塊可以把一個類型,如數字,轉成固定長度的bytes類型 import struct # res = struct.pack('i',12345) # print(res,len(res),type(res)) #長度是4 res2 = struct.pack('i',12345111) print(res2,len(res2),type(res2)) #長度也是4 unpack_res =struct.unpack('i',res2) print(unpack_res) #(12345111,) # print(unpack_res[0]) #12345111
代碼中,首先定義了一個元組數據,包含int、string、float三種數據類型,然后定義了struct對象,并制定了format‘I3sf',I 表示int,3s表示三個字符長度的字符串,f 表示 float。最后通過struct的pack和unpack進行打包和解包。通過輸出結果可以發現,value被pack之后,轉化為了一段二進制字節串,而unpack可以把該字節串再轉換回一個元組,但是值得注意的是對于float的精度發生了改變,這是由一些比如操作系統等客觀因素所決定的。打包之后的數據所占用的字節數與C語言中的struct十分相似。
2,定義format可以參照官方api提供的對照表:
3,基本用法
import json,struct #假設通過客戶端上傳1T:1073741824000的文件a.txt #為避免粘包,必須自定制報頭 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T數據,文件路徑和md5值 #為了該報頭能傳送,需要序列化并且轉為bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并轉成bytes,用于傳輸 #為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節 head_len_bytes=struct.pack('i',len(head_bytes)) #這4個字節里只包含了一個數字,該數字是報頭的長度 #客戶端開始發送 conn.send(head_len_bytes) #先發報頭的長度,4個bytes conn.send(head_bytes) #再發報頭的字節格式 conn.sendall(文件內容) #然后發真實內容的字節格式 #服務端開始接收 head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的字節格式 x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度 head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式 header=json.loads(json.dumps(header)) #提取報頭 #最后根據報頭的內容提取真實的數據,比如 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
感謝各位的閱讀!關于“python中socket網絡編程之粘包的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。