python socket網路編程之粘包問題詳解

來源:互聯網
上載者:User
這篇文章主要介紹了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 總結

  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時才會清除緩衝區內容。資料是可靠的,但是會粘包。

二,兩種情況下會發生粘包:

1,發送端需要等到原生緩衝區滿了以後才發出去,造成粘包(發送資料時間間隔很短,資料很小,python使用了最佳化演算法,合在一起,產生粘包)

用戶端

#_*_coding:utf-8_*_import socketBUFSIZE=1024ip_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 socketBUFSIZE=1024ip_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 socketimport subprocessdin=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 socketimport subprocessdin=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在資料轉送時,發送端先把資料發送到自己的緩衝中,然後協議控制將緩衝中的資料發往對端,對端返回一個ack=1,發送端則清理緩衝中的資料,對端返回ack=0,則重新發送資料,所以tcp是可靠的

而udp發送資料,對端是不會返回確認資訊的,因此不可靠

補充問題二:send(位元組流)和recv(1024)及sendall是什麼意思?

recv裡指定的1024意思是從緩衝裡一次拿出1024個位元組的資料

send的位元組流是先放入己端緩衝,然後由協議控制將緩衝內容發往對端,如果位元組流大小大於緩衝剩餘空間,那麼資料丟失,用sendall就會迴圈調用send,資料不會丟失。

五,粘包問題如何解決?

問題的根源在於,接收端不知道發送端將要傳送的位元組流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送資料前,把自己將要發送的位元組流總大小讓接收端知曉,然後接收端來一個死迴圈接收完所有資料。

5-1 簡單的解決方案(從表面解決):

在用戶端發送下邊添加一個時間睡眠,就可以避免粘包現象。在服務端接收的時候也要進行時間睡眠,才能有效避免粘包情況。

用戶端:

#用戶端import socketimport timeimport subprocessdin=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 socketimport timeimport subprocessdin=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 socketimport structphone = 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 socketimport subprocessimport structphone = 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 socketimport structimport jsonphone = 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 socketimport subprocessimport structimport jsonphone = 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)) #長度是4res2 = struct.pack('i',12345111)print(res2,len(res2),type(res2)) #長度也是4unpack_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值#為了該前序能傳送,需要序列化並且轉為byteshead_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個bytesconn.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)


相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.