黏包問題的成因與解決方案


一、黏包成因

tcp協議的拆包機制

當發送端緩沖區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去。 
MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。大部分網絡設備的MTU都是1500。
如果本機的MTU比網關的MTU大,大的數據包就會被拆開來傳送,這樣會產生很多數據包碎片,增加丟包率,降低網絡速度

面向流的通信特點和Nagle算法

TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。
收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。
這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。 
對於空消息:tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上消息頭發送過去。 
可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。

總結:

黏包有兩種:

一種是因為發送數據包時,每次發送的包小,因為系統進行優化算法,就將兩次的包放在一起發送,減少了資源的重復占用。多次發送會經歷多次網絡延遲,一起發送會減少網絡延遲的次數。因此在發送小數據時會將兩次數據一起發送,而客戶端接收時,則會一並接收。#即出現多次send會出現黏包

第二種是因為接收數據時,又多次接收,第一次接收的數據量小,導致數據還沒接收完,就停下了,剩余的數據會緩存在內存中,然后等到下次接收時和下一波數據一起接收。

二、黏包的解決方案

1,問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據。

#_*_coding:utf-8_*_
import socket,subprocess
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(ip_port)
s.listen(5)

while True:
    conn,addr=s.accept()
    print('客戶端',addr)
    while True:
        msg=conn.recv(1024)
        if not msg:break
        res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
                            stdin=subprocess.PIPE,\
                         stderr=subprocess.PIPE,\
                         stdout=subprocess.PIPE)
        err=res.stderr.read()
        if err:
            ret=err
        else:
            ret=res.stdout.read()
        data_length=len(ret)
        conn.send(str(data_length).encode('utf-8'))
        data=conn.recv(1024).decode('utf-8')
        if data == 'recv_ready':
            conn.sendall(ret)
    conn.close()

服務端
server
#_*_coding:utf-8_*_
import socket,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))
    length=int(s.recv(1024).decode('utf-8'))
    s.send('recv_ready'.encode('utf-8'))
    send_size=0
    recv_size=0
    data=b''
    while recv_size < length:
        data+=s.recv(1024)
        recv_size+=len(data)


    print(data.decode('utf-8'))

客戶端
client

2.使用time模塊,在每次send的時候加入一個time.sleep(0.01),這種方法可以有效地隔開兩次send,斷開系統的優化,此種方法雖然可以解決黏包問題,但是會造成發送數據時間長

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()

conn,addr = sk.accept()
ret1 = conn.recv(12)
print(ret1)
ret2 = conn.recv(12)   #
ret3 = conn.recv(12)   #
print(ret2)
print(ret3)
conn.close()
sk.close()
server
import socket

sk = socket.socket()
sk.connect(('127.0.0.1',8090))

sk.send(b'hello')
import time
time.sleep(0.01)
sk.send(b'egg')

sk.close()
client

3,先讀取文件的大小,然后將文件的大小發送給接收端,這樣接收端就可以以文件大小來寫入數據。

import json
import socket
import struct
sk =socket.socket()#創建一個socket對象
sk.bind(('127.0.0.1',8080))#綁定本地ip地址與端口
sk.listen()#開啟監聽
buffer =1024    #設置buffer值大小
conn,addr =sk.accept()#等待客戶端連接服務端,得到地址與雙共工通道
head_len=conn.recv(4)#接收用struck將數字轉長度為4的bytes
head_len =struct.unpack('i',head_len)[0]#調用struct模塊來解包,得到原來的數字(數字為報頭的長度)
json_head =conn.recv(head_len).decode('utf-8')#接收json序列化的報頭進行解碼
head =json.loads(json_head)#將json序列化的報頭進行反序列化
filesize =head['filesize']#拿到head字典中鍵filesize所對應的值
print(filesize)#打印filesize
with open(r'dir\%s'%head['filename'],'wb')as f:#dir\文件名,拿到文件的路徑,以wb模式打開
    while filesize:#當filesize(文件內剩余內容的大小)有值時
        if filesize >=buffer:#如果filesize>= buffer值,buffer值是設定的一次接收多少字節的內容
            print(filesize)  #打印filesize大小
            content =conn.recv(buffer)#接收buffer值大小的內容
            f.write(content)#寫入文件
            filesize -=buffer#原來的文件大小減去接收的內容,等於剩余文件的大小
        else:#如果文件剩余的內容大小<buffer設定的大小,就全部接收
            content =conn.recv(filesize)
            f.write(content)
            filesize =0
        print('=====>',len(content))
    print(filesize)
print('服務器端')
conn.close()
sk.close()
server
import struct
import os
import json
import socket
sk =socket.socket
sk.connect(('127.0.0.1',8090))
buffer =1024
head ={'filepath':r'D:\Documents\oCam',
       'filename':r'test.mp4',
       'filesize':None}#定義一個報頭
file_path =os.path.join(head['filepath'],head['filename'])#將文件名與文件路徑加載進目錄中
filesize = os.path.getsize(file_path)#得到目錄中文件的大小
head['filesize'] =filesize#將文件大小賦值回列表中
json_head =json.dumps(head)#將head字典序列化
bytes_head =json_head.encode('utf-8')#將序列化之后的字典進行解碼
head_len =len(bytes_head)#計算轉碼之后字典的長度
pack_len =struct.pack('i',head_len)#調用struct模塊將長度轉換成長度為4的bytes類型
sk.send(pack_len)#發送pack_len
sk.send(bytes_head)#發送bytes_head
with open(file_path,'rb')as f:
    while filesize:
        print(filesize)
        if filesize>=buffer:
            content =f.read(buffer)
            print('====>',len(content))
            sk.send(content)
            filesize-=buffer
        else:
            content =f.read(filesize)
            sk.send(content)
            filesize=0
sk.close()
client

 

 

 

為什么會出現黏包問題?

首先只有在TCP協議中才會出現黏包現象

是因為TCP協議是面向流的協議

在發送的數據傳輸的過程中還有緩存機制來避免數據丟失

因為在連續發送小數據的時候、以及接收大小不符的時候都容易出現黏包現象

本質還是因為我們在接收數據的時候不知道發送的數據的長短

 

解決黏包問題

在傳輸大量數據之前先告訴數據量的大小。

4,使用struct解決黏包 

import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

phone.bind(('127.0.0.1',8080))

phone.listen(5)

while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()


        conn.send(struct.pack('i',len(back_msg))) #先發back_msg的長度
        conn.sendall(back_msg) #在發真實的內容

    conn.close()
服務端(自定制報頭)
#_*_coding:utf-8_*_
import socket,time,struct

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))



    l=s.recv(4)
    x=struct.unpack('i',l)[0]
    print(type(x),x)
    # print(struct.unpack('I',l))
    r_s=0
    data=b''
    while r_s < x:
        r_d=s.recv(1024)
        data+=r_d
        r_s+=len(r_d)

    # print(data.decode('utf-8'))
    print(data.decode('gbk')) #windows默認gbk編碼
客戶端(自定制報頭)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM