python--socket粘包

socket粘包

1 什麼是粘包

須知:只有TCP有粘包現象,UDP永遠不會粘包,首先須要掌握一個socket收發消息的原理,python

所謂粘包問題主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的。算法

 

發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。shell

TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。json

例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束。緩存

此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。數據結構

1.TCP(傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務,收發兩端(客戶端和服務端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端,就難於分辨出來,必須提供科學的拆包機制,即面向流的通訊是無消息保護邊界的。socket

2.tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,放置程序卡主。tcp

Tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端老是在收到ack時纔會清除緩衝區內容,數據是可靠的,可是會粘包。優化

兩種狀況下會發生粘包:編碼

發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據量很小,會合到一塊兒,產生粘包)

#服務端
import socket,subprocess
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(("127.0.0.1",8000))
s.listen(5)

conn,addr=s.accept()

data1=conn.recv(1024)
data2=conn.recv(1024)
print("第一個包",data1)
print("第二個包",data2)

conn.close()
s.close()
執行結果
第一個包 b'helloworldSB'
第二個包 b''



#客戶端
import socket,subprocess
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",8000))
s.send("helloworld".encode("utf-8"))
s.send("SB".encode("utf-8"))

s.close()

解決粘包問題1:low..low方法 在客戶端加個時間延遲,暫且能夠解決問題。

#服務端
import socket,subprocess
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(("127.0.0.1",8000))
s.listen(5)

conn,addr=s.accept()

data1=conn.recv(1024)
data2=conn.recv(1024)
print("第一個包",data1)
print("第二個包",data2)

conn.close()
s.close()

執行結果
第一個包 b'helloworld'
第二個包 b'SB'



#客戶端
import socket,subprocess,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",8000))


s.send("helloworld".encode("utf-8"))
time.sleep(3)
s.send("SB".encode("utf-8"))

s.close()

  

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

#服務端
import socket,subprocess,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(("127.0.0.1",8000))
s.listen(5)

conn,addr=s.accept()


data1=conn.recv(1)   #第一次收了個"h"
# time.sleep(5)
data2=conn.recv(1024) #第二次收了"elloworld"
print("第一個包",data1)
print("第二個包",data2)

conn.close()
s.close()



#客戶端
import socket,subprocess,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",8000))


s.send("helloworld".encode("utf-8"))
time.sleep(3)
s.send("SB".encode("utf-8"))

s.close()

解決粘包問題2:比方法1要減小一個low 

問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端知曉,而後接收端來一個死循環接收完全部數據

low版本的解決方法 

模擬以太網協議封裝報頭:

報頭 特色:固定長度

         包含對將要發送數據的描述信息

#服務端
import socket,subprocess,struct
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(("127.0.0.1",8000))
s.listen(5)

while True:
    conn,addr=s.accept()
    while True:
        try:
            msg=conn.recv(1024)
            res=subprocess.Popen(msg.decode("utf-8"),
                                 shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)

            out_res=res.stdout.read()
            err_res=res.stderr.read()
            data_size=(len(out_res)+len(err_res))
            #發送報頭
            conn.send(struct.pack("i",data_size))
            #發送真實數據部分
            conn.send(out_res)
            conn.send(err_res)
        except Exception:
            break
    conn.close()
s.close()





#客戶端
#粘包 本身封裝報頭
import socket,struct
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",8000))

while True:
    msg=input("請輸入命令:").strip()
    if not msg:continue
    s.send(bytes(msg,encoding="utf-8"))
    #收報頭
    baotou=s.recv(4)
    data_size=struct.unpack("i",baotou)[0]

    #收收據
    recv_size=0
    recv_data=b""
    while recv_size <data_size:
        data=s.recv(1024)
        recv_size+=len(data)
        recv_data+=data
    print(recv_data.decode("gbk"))
s.close()

爲字節流加上自定義固定長度報頭,報頭中包含字節流長度,而後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,而後再取真實數據

3.struct模塊 

該模塊能夠把一個類型,如數字,轉成固定長度的bytes

>>> res=struct.pack('i',1111111111111)  #打包成固定長度的bytes

>>> struct.unpack(「I」,res)             #解包

 

2 大神解決粘包的方法

咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)

發送時:

先發報頭長度

再編碼報頭內容而後發送

最後發真實內容

 

接收時:

先手報頭長度,用struct取出來

根據取出的長度收取報頭內容,而後解碼,反序列化

從反序列化的結果中取出待取數據的詳細信息,而後去取真實的數據內容

 

#服務端
import socket,subprocess,struct,json
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(("127.0.0.1",8000))
s.listen(5)

while True:
    conn,addr=s.accept()

    while True:
        try:
            msg=conn.recv(1024)
            res=subprocess.Popen(msg.decode("utf-8"),
                                 shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            out_res=res.stdout.read()
            err_res=res.stderr.read()
            data_size=len(out_res)+len(err_res)
            head_dic={"data_size":data_size}
            head_json=json.dumps(head_dic)
            head_bytes=head_json.encode("utf-8")#報頭
            #part1:先發報頭的長度
            head_len=len(head_bytes)
            conn.send(struct.pack("i",head_len))
            #part2:再發送報頭
            conn.send(head_bytes)
            #part3:最後發送數據真實部分
            conn.send(err_res)
            conn.send(out_res)
        except Exception:
             break

    conn.close()
s.close()


#客戶端
import socket,struct,json
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",8000))

while True:
    msg=input("請輸入命令:").strip()
    if not msg:continue
    s.send(bytes(msg,encoding="utf-8"))

    #part1:先收報頭的長度
    head_struct=s.recv(4)
    head_len=struct.unpack("i",head_struct)[0]

    #part2:再收報頭
    head_bytes=s.recv(head_len)
    head_json=head_bytes.decode("utf-8")
    head_dic=json.loads(head_json)
    data_size=head_dic["data_size"]

    #part3:收數據
    recv_size=0
    recv_data=b""
    while recv_size < data_size:
        data=s.recv(1024)
        recv_size+=len(data)
        recv_data+=data
    print(recv_data.decode("gbk"))
s.close()
相關文章
相關標籤/搜索