1. 程式人生 > >python套接字解決tcp粘包問題

python套接字解決tcp粘包問題

python套接字解決tcp粘包問題

目錄

什麼是粘包

演示粘包現象 解決粘包 實際應用

 

 

 

什麼是粘包

首先只有tcp有粘包現象,udp沒有粘包

 

 

socket收發訊息的原理

 

 

傳送端可以是一K一K地傳送資料,而接收端的應用程式可以兩K兩K地提走資料,當然也有可能一次提走3K或6K資料,或者一次只提走幾個位元組的資料,也就是說,應用程式所看到的資料是一個整體,或說是一個流(stream),
一條訊息有多少位元組對應用程式是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。而UDP是面向訊息的協議,每個UDP段都是一條訊息,應用程式必須以訊息為單位提取資料,不能一次提取任意位元組
的資料,這一點和TCP是很不同的。怎樣定義訊息呢?可以認為對方一次性write/send的資料為一個訊息,需要明白的是當對方send一條資訊的時候,無論底層怎樣分段分片,TCP協議層會把構成整條訊息的資料段排序完成後
才呈現在核心緩衝區。 例如基於tcp的套接字客戶端往服務端上傳檔案,傳送時檔案內容是按照一段一段的位元組流傳送的,在接收方看了,根本不知道該檔案的位元組流從何處開始,在何處結束

 

粘包問題的根源

所謂粘包問題主要還是因為接收方不知道訊息之間的界限,不知道一次性提取多少位元組的資料所造成的。
此外,傳送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,傳送方往往要收集到足夠多的資料後才傳送一個TCP段。若連續幾次需要send的資料都很少,通常TCP會根據優化演算法把這些資料合成一個TCP段後一次發
送出去,這樣接收方就收到了粘包資料。

 

tcp和udp協議

TCP(transport control protocol,傳輸控制協議)是面向連線的,面向流的,提供高可靠性服務。收發兩端(客戶端和伺服器端)都要有一一成對的socket,因此,傳送端為了將多個發往接收端的包,更有效的發到對
方,使用了優化方法(Nagle演算法),將多次間隔較小且資料量小
的資料,合併成一個大的資料塊,然後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無訊息保護邊界的。
UDP(user datagram protocol,使用者資料報協議)是無連線的,面向訊息的,提供高效率服務。不會使用塊的合併優化演算法,, 由於UDP支援的是一對多的模式,所以接收端的skbuff(套接字緩衝區)採用了鏈式結構來記
錄每一個到達的UDP包,在每個UDP包中就有了訊息頭(訊息來源地址,埠等資訊),這樣,對於接收端來說,就容易進行區分處理了。 即面向訊息的通訊是有訊息保護邊界的。
tcp是基於資料流的,於是收發的訊息不能為空,這就需要在客戶端和服務端都新增空訊息的處理機制
,防止程式卡住,而udp是基於資料報的,即便是你輸入的是空內容(直接回車),那也不是空訊息,udp協議會幫你封裝上
訊息頭,實驗略

 

 

補充

拆包的發生情況

當傳送端緩衝區的長度大於網絡卡的MTU時,tcp會將這次傳送的資料拆成幾個資料包傳送出去。
補充問題一:為何tcp是可靠傳輸,udp是不可靠傳輸 基於tcp的資料傳輸請參考我的另一篇文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在資料傳輸時,傳送端先把資料傳送到自己的快取中,然後協議控制將快取中的資料發往對端,對端返回
一個ack=1,傳送端則清理快取中的資料,對端返回ack=0,則重新發送資料,所以tcp是可靠的 而udp傳送資料,對端是不會返回確認資訊的,因此不可靠
補充問題二:send(位元組流)和recv(1024)及sendall recv裡指定的1024意思是從快取裡一次拿出1024個位元組的資料 send的位元組流是先放入己端快取,然後由協議控制將快取內容發往對端,如果待發送的位元組流大小大於快取剩餘空間,那麼資料丟失,用sendall就會迴圈呼叫send,資料不會丟失

  

 

總結

udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個位元組的資料就算完成,若是y>x資料就丟失,這意味著udp根本不會粘包,但是會丟資料,不可靠

tcp的協議資料不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩衝區內容。資料是可靠的,但是會粘包。

  

 

 

演示粘包現象

 

兩種情況下會發生粘包

傳送端需要等緩衝區滿才傳送出去,造成粘包(傳送資料時間間隔很短,資料量很小,會合到一起,產生粘包),這是由於tcp的優化演算法。


接收方不及時接收緩衝區的包,造成多個包接收(客戶端傳送了一段資料,服務端只收了一小部分,服務端下次再收的時候還是從緩衝區拿上次遺留的資料,產生粘包) 

  

 

 

第一種情況

客戶端多次間隔時間短,資料量小的傳送資料

#服務端
import socket def main(): ip_port= ('127.0.0.1',4444) back_log=5 buffer_size=1024 s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #基於tcp的網路通訊 s1.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) s1.bind(ip_port) #繫結ip和埠 s1.listen(back_log) # 最多連線幾個客戶端 conn, addr = s1.accept() data1=conn.recv(buffer_size) data2=conn.recv(buffer_size) data3=conn.recv(buffer_size) print('第一次',data1.decode('utf-8')) print('第二次',data2.decode('utf-8')) print('第三次',data3.decode('utf-8')) conn.close() s1.close() if __name__ == '__main__': main()

 

#客戶端
import socket


def main():
    ip_port = ('127.0.0.1', 4444)

    buffer_size = 1024

    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s2.connect(ip_port)  # 連線服務端

    data1 = 'hello'
    s2.send(data1.encode('utf-8'))
    data2 ='wrold'
    s2.send(data2.encode('utf-8'))
    data3 = 'pop'
    s2.send(data3.encode('utf-8'))
    s2.close()

if __name__ == '__main__':
    main()

 

 

演示

可以看出來服務端在第一次就把三次傳送的資料都接收了,這就是粘包,服務端不知道一次讀取多少的資料,一次全部讀取出來。

首先我們要知道並不是客戶端發幾次,服務端就要接收幾次,一次發的資料也可以三次讀取出來,收發資訊都是從自己的核心快取區讀取。

 

 

第二種情況

接收方不及時接收緩衝區的包,造成多個包接收(客戶端傳送了一段資料,服務端只收了一小部分,服務端下次再收的時候還是從緩衝區拿上次遺留的資料,產生粘包) 

#服務端
import socket def main(): ip_port= ('127.0.0.1',4444) back_log=5 buffer_size=1024 s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #基於tcp的網路通訊 s1.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) s1.bind(ip_port) #繫結ip和埠 s1.listen(back_log) # 最多連線幾個客戶端 conn, addr = s1.accept() data1=conn.recv(5) data2=conn.recv(buffer_size) print('第一次',data1.decode('utf-8')) print('第二次',data2.decode('utf-8')) conn.close() s1.close() if __name__ == '__main__': main()
#客戶端 import socket def main(): ip_port = ('127.0.0.1', 4444) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2.connect(ip_port) # 連線服務端 data1 = 'hellowroldpop' s2.send(data1.encode('utf-8')) s2.close() if __name__ == '__main__': main()

 

 

演示

服務端讀取資料沒有全部讀取出來,導致第一次應該接收完的資料還要第二次讀取出來

 

 

 

 

 

解決粘包

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

 

第一種解決方法

#服務端
import socket


def main():
    ip_port = ('127.0.0.1', 4444)
    back_log = 5
    buffer_size = 1024

    s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於tcp的網路通訊
    s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s1.bind(ip_port)  # 繫結ip和埠
    s1.listen(back_log)  # 最多連線幾個客戶端
    conn, addr = s1.accept()

    while True:
     #接收資料大小
        length= conn.recv(buffer_size).decode('utf-8')
     #為防止客戶端連續發包,迴應 conn.send('ready'.encode('utf-8')) length=int(length) recv_size=0 #已經接收到資料的大小 recv_msg=b'' #已經接收到的資料
     #接收資料 while recv_size<length: r_msg = conn.recv(buffer_size) recv_msg+=r_msg recv_size +=len(r_msg)        #另一種方法接收資料的方法
#recv_msg+=conn.recv(buffer_size) #recv_size=len(recv_msg) s1.close() if __name__ == '__main__': main()

 

 

 

#客戶端
import socket


def main():
    ip_port = ('127.0.0.1', 4444)

    buffer_size = 1024

    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s2.connect(ip_port)  # 連線服務端

    while True:

        data1 = input('input:')
     #將資料大小轉為字元型然後編碼發出去 s2.send(str(len(data1)).encode('utf-8'))
      #接收服務端的迴應 server_Ready=s2.recv(buffer_size)
      #接收到服務端迴應 if server_Ready==b'ready': s2.send(data1.encode('utf-8')) s2.close() if __name__ == '__main__': main()

 

 總結:客戶端在傳送資料時,先發送資料大小,這時不能把資料內容一起傳送出去,服務端第一次接收的時候,並不知道該讀取多少的資料大小和多少的資料內容,所以還是會造成粘包,我們的解決辦法是,服務端獲取到資料大小後,要回應一次,然後根據資料大小來迴圈讀取內容。

這種方法不好,需要服務端多發一次迴應,這很影響服務端的效能。

程式的執行速度遠快於網路傳輸速度,所以在傳送一段位元組前,先用send去傳送該位元組流長度,這種方式會放大網路延遲帶來的效能損耗

  

 

 

第二種解決方法

為位元組流加上自定義固定長度報頭,報頭中包含位元組流長度,然後一次send到對端,對端在接收時,先從快取中取出定長的報頭,然後再取真實資料

 

struct模組 

該模組可以把一個型別,如數字,轉成固定長度的bytes

>>> struct.pack('i',1111111111111) #第一個引數是要封裝的格式型別,第二個引數是要封裝的內容

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個封裝資料的範圍,只要在這個範圍裡面,就可以把內容封裝成固定大小

 

 

 

#服務端

import socket
import struct


def main():
    ip_port = ('127.0.0.1', 4444)
    back_log = 5
    buffer_size = 1024

    s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於tcp的網路通訊
    s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s1.bind(ip_port)  # 繫結ip和埠
    s1.listen(back_log)  # 最多連線幾個客戶端
    conn, addr = s1.accept()

    while True:

        length_data= conn.recv(4)
        length=struct.unpack('i',length_data)[0]

        recv_size=0   #已經接收到資料的大小
        recv_msg=b''  #已經接收到的資料

        while recv_size<length:
            r_msg = conn.recv(buffer_size)
            recv_msg+=r_msg
            recv_size +=len(r_msg)

            #recv_msg+=conn.recv(buffer_size)
            #recv_size=len(recv_msg)


    s1.close()


if __name__ == '__main__':
    main()

  

 

 

#客戶端

import socket
import struct


def main():
    ip_port = ('127.0.0.1', 4444)

    buffer_size = 1024

    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s2.connect(ip_port)  # 連線服務端

    while True:

        data1 = input('input:')
        length=len(data1)
        #定製包頭 i為4個位元組,所以接收方為四個位元組,這個大小並不是輸入的大小,而是封裝固定的大小
        data_length=struct.pack('i',length) #使用struct,直接將int轉為二進位制型資料傳輸,對方使用struct解包
        s2.send(data_length)

        s2.send(data1.encode('utf-8'))
    s2.close()


if __name__ == '__main__':
    main()

  

 

總結:客戶端把資料長度封裝成一個固定大小的資料,這時服務端就可以指定讀取固定大小的內容,不會讀取資料的內容,服務端只要根據資料長度再來接收資料內容就好了,所以客戶端連續兩次發資料,不會粘包,因為服務
端每次接收都只接收了本次該接收的資料。

  

 

 

實際應用

 

#服務端
from socket import * import subprocess import struct def main(): ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 s1 = socket(AF_INET,SOCK_STREAM) s1.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) s1.bind(ip_port) s1.listen(back_log) while True: conn,addr=s1.accept() while True: try: #收資訊 cmd = conn.recv(buffer_size) if not cmd:break print('收到的命令是:',cmd.decode('utf-8')) #執行命令 res = subprocess.Popen(cmd.decode('utf-8'),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err = res.stderr.read() if err: cmd_res=err else: cmd_res=res.stdout.read() if not cmd_res: cmd_res='執行成功'.encode('gbk') length=len(cmd_res) #第一次傳送資料大小 data_length = struct.pack('i', length) # 使用struct,直接將int轉為二進位制型資料傳輸,對方使用struct解包 conn.send(data_length) #發信息 #注意:執行的結果預設jbk編碼方式,所以客戶端必須使用gbk方式解碼 conn.send(cmd_res) except Exception: break conn.close() s1.close() # 關閉服務端套接字 if __name__ == '__main__': main()

  

 

#客戶端
from socket import * import struct def main(): ip_port=('127.0.0.1',8080) buffer_size=1024 s1 = socket(AF_INET,SOCK_STREAM) s1.connect(ip_port) while True: cmd = input('-->') if not cmd:continue if cmd =='quite':break s1.send(cmd.encode('utf-8')) length_data =s1.recv(4) length = struct.unpack('i', length_data)[0] recv_size = 0 # 已經接收到資料的大小 recv_msg = b'' # 已經接收到的資料 while recv_size < length: r_msg = s1.recv(buffer_size) recv_msg += r_msg recv_size += len(r_msg) # recv_msg+=conn.recv(buffer_size) # recv_size=len(recv_msg) print('命令執行結果:',recv_msg.decode('gbk')) s1.close() if __name__=='__main__': main()

 

總結

如果沒有粘包的處理

服務端把命令執行的結果發給客戶端的時候,資料太大,客戶端一次沒有接收完,在客戶端第二次執行命令的時候,就會把第一次沒有讀取完的部分也讀取出來,這屬於我們剛才說的第二種粘包的情況。

有了粘包的處理

只要服務端把結果發過來,就算超過網絡卡的限制(拆包傳送),客戶端能保證在迴圈的過程中接收完結果