[網絡知識]TCP協議中的粘包與拆包

在平時客戶端socket開發中,如果客戶端連續不斷的向服務端發送數據包時,服務端接收的數據會出現兩個數據包粘在一起的情況,這就是TCP協議中經常會遇到的粘包以及拆包的問題。

我們都知道TCP屬於傳輸層的協議,傳輸層除了有TCP協議外還有UDP協議。那麼UDP是否會發生粘包或拆包的現象呢?答案是不會。UDP是基於報文發送的,從UDP的幀結構可以看出,在UDP首部採用了16bit來指示UDP數據報文的長度,因此在應用層能很好的將不同的數據報文區分開,從而避免粘包和拆包的問題。而TCP是基於字節流的,雖然應用層和TCP傳輸層之間的數據交互是大小不等的數據塊,但是TCP把這些數據塊僅僅看成一連串無結構的字節流,沒有邊界;另外從TCP的幀結構也可以看出,在TCP的首部沒有表示數據長度的字段,基於上面兩點,在使用TCP傳輸數據時,纔有粘包或者拆包現象發生的可能。
TCP爲提高性能,發送端會將需要發送的數據發送到緩衝區,等待緩衝區滿了之後,再將緩衝中的數據發送到接收方。同理,接收方也有緩衝區這樣的機制,來接收數據。

粘包/拆包表現形式

現在假設客戶端向服務端連續發送了兩個數據包,用packet1和packet2來表示,那麼服務端收到的數據可以分爲三種,現列舉如下:

第一種情況,接收端正常收到兩個數據包,即沒有發生拆包和粘包的現象,此種情況不在本文的討論範圍內。

第二種情況,接收端只收到一個數據包,由於TCP是不會出現丟包的,所以這一個數據包中包含了發送端發送的兩個數據包的信息,這種現象即爲粘包。這種情況由於接收端不知道這兩個數據包的界限,所以對於接收端來說很難處理。

第三種情況,這種情況有兩種表現形式,如下圖。接收端收到了兩個數據包,但是這兩個數據包要麼是不完整的,要麼就是多出來一塊,這種情況即發生了拆包和粘包。這兩種情況如果不加特殊處理,對於接收端同樣是不好處理的。

粘包/拆包發生原因

發生TCP粘包或拆包有很多原因,現列出常見的幾點,可能不全面,歡迎補充

  1. 要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。
  2. 待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。
  3. 要發送的數據小於TCP發送緩衝區的大小,TCP將多次寫入緩衝區的數據一次發送出去,將會發生粘包。
  4. 接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包。

發送端需要等緩衝區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會當做一個包發出去,產生粘包)

########################
#服務端
from socket import *
phone = socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SOCK_STREAM,1)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
print('start running...')

coon,addr = phone.accept() #等待連接

data1 = coon.recv(10)
data2 = coon.recv(10)

print('------------>',data1.decode('utf-8'))
print('------------>',data2.decode('utf-8'))
coon.close()
phone.close()


###############################
#客戶端
from socket import *
import time
phone = socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8080))

phone.send('hello'.encode('utf-8'))
phone.send('helloworld'.encode('utf-8'))
phone.close()

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

########################
#服務端
from socket import *
phone = socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SOCK_STREAM,1)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
print('start running...')

coon,addr = phone.accept() #等待連接

data1 = coon.recv(2) #一次沒有接收完整
data2 = coon.recv(10)  #下一次接收的時候會先取舊的數據,然後取新的
# data3 = coon.recv(1024)  #接收等5秒後的信息
print('------------>',data1.decode('utf-8'))
print('------------>',data2.decode('utf-8'))
# print('------------>',data3.decode('utf-8'))
coon.close()
phone.close()


###############################
#客戶端
from socket import *
import time
phone = socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8080))

phone.send('hello'.encode('utf-8'))
time.sleep(5)
phone.send('haiyan'.encode('utf-8'))
phone.close()


粘包/拆包解決辦法

通過以上分析,我們清楚了粘包或拆包發生的原因,那麼如何解決這個問題呢?解決問題的關鍵在於如何給每個數據包添加邊界信息,常用的方法有如下幾個:

  1. 發送端給每個數據包添加包首部,首部中應該至少包含數據包的長度,這樣接收端在接收到數據後,通過讀取包首部的長度字段,便知道每一個數據包的實際長度了。
  2. 發送端將每個數據包封裝爲固定長度(不夠的可以通過補0填充),這樣接收端每次從接收緩衝區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。
  3. 可以在數據包之間設置邊界,如添加特殊符號,這樣,接收端通過這個邊界就可以將不同的數據包拆分開。

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

########################
#服務端
import socket
import subprocess
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
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()
        #先發報頭(轉成固定長度的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()



###############################
#客戶端

import socket
import struct
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_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()

解決粘包問題升級版

########################
#服務端
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()



###############################
#客戶端
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()

struct模塊

#該模塊可以把一個類型,如數字,轉成固定長度的bytes類型
import struct
res = struct.pack('i',12345)
print(res,len(res),type(res))  #長度是4

res2 = struct.pack('i',12345111)
print(res,len(res),type(res2))  #長度也是4

unpack_res =struct.unpack('i',res2)
print(unpack_res)  #(12345111,)
print(unpack_res[0]) #12345111

 

發佈了448 篇原創文章 · 獲贊 176 · 訪問量 86萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章