1. 程式人生 > 其它 >粘包問題及解決方案

粘包問題及解決方案

目錄

粘包問題及解決方案

一 什麼是粘包問題

	前提:只有TCP會發生粘包現象,UDP永遠不會粘包。
	粘包問題本質上就是接收方不知道訊息的邊界,不知道一次性該提取多少位元組流用於解析訊息,造成的訊息解析錯誤問題。

二 為何麼會有粘包問題

1 socket收發訊息的原理之流式協議

	傳送端可以是1K1K的傳送資料,而接收端的應用程式可以是兩K兩K地提取資料,也可以一次性全部提走,或者一次只提取幾個位元組地資料,也就是說,應用程式所看到的資料是一個整體,或者說是一個流(stream) ,一條訊息有多少個位元組對應用程式時不可見的,因此TCP協議是"""**面向流的協議**""",這也是容易出現粘包問題的原因。而UDP協議是面向訊息的協議,每個UDP欄位都是一條訊息,應用程式必須以訊息為單位提取資料,不能一次性提取任意位元組的資料,這和TCP很不相同。TCP協議下,一條訊息的傳送,無論底層如何分段分片,TCP協議層會把構成整條訊息的資料段排序完成後才呈現在核心緩衝區。
	例如:基於TCP的套接字客戶端往服務端上傳檔案,傳送時檔案內容是按照一段一段的位元組流傳送的,在接收方看來,根部不知道該檔案的位元組流是從何處開始,在何處結束。
	# **所謂的粘包問題,主要還是因為接收方不知道訊息之間的界限,不知道一次性提取多少位元組流的資料造成的。**
	此外,傳送放引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,傳送方往往要手機足夠多的資料後才傳送一個TCP段。若連續幾次需要send的資料都很少,通常TCP會根據優化演算法把這些資料整合成一個TCP段後一次性發出,這樣接收方就收到了粘包資料。

2 TCP與UDP的訊息邊界

### 1.TCP(transport control protocol,傳輸控制協議) 下的訊息邊界
	該協議是面向連線的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務端) 都要有一一成對的socket,因此,傳送端為了將多個法網接收端的包,更有效的發到對方,使用了優化演算法(Nagle演算法) ,將多次間隔較小且資料量小的資料,合併成一個大的資料塊,然後進行封包。這樣,接收端就難於分辨出來了,必須提供科學的拆包機制。**即面向流的通訊是無訊息保護邊界的。**

### 2.UDP(user datagram protocal,使用者資料報協議) 下的訊息邊界
	該協議是無連線的,面向訊息的,提供高效率服務。不會使用塊的合併優化演算法,由於UDP支援的是一對多的模式,所以接收端的skbuff(套接字緩衝區) 採用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中都有訊息頭(訊息來源地址,埠等訊息) ,這樣,對於接收端來說,就容易進行區分處理了。**即面向訊息的通訊是有訊息保護邊界的。**

### 3.總結
	由於TCP協議是基於資料流的,於是收發訊息不能為空,這就需要在客戶端和服務端都新增空訊息的處理機制,防止程式卡住,而udp是基於資料報的,即使是你輸入的是空內容(直接回車) ,那也不是空訊息,udp協議會幫你封裝上訊息頭。
	udp的recvfrom是阻塞的,一個recvfrom(x) 必須對唯一一個sendinto(y) ,收完了x 個位元組的資料就算完成,若是y > x 資料就丟失,這意味著udp根本不會粘包,但是會丟資料,不可靠。
	tcp協議資料不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩衝區內容。資料是可靠的,但是會粘包。

3 兩種發生粘包的情況

	1.傳送端需要等緩衝區滿才傳送出去,造成粘包(傳送資料時間間隔很短,資料流很小,會河道一起,產生粘包) 。
	2.接收方不及時接收緩衝區的包,造成多個包接收(客戶端傳送了一段資料,服務端只收了一小部分,服務端下次再接收的時候,還是從緩衝區拿上次一六的資料,產生粘包) 。

4 拆包發生的情況

	當傳送端緩衝區的長度大於網絡卡的MTU時,tcp會將這次傳送的資料拆成幾個資料包傳送出去。

5 補充知識兩則

### 1.為何tcp是可靠傳輸,udp是不可靠傳輸
	tcp在傳輸時,傳送端先把資料傳送到自己的快取中,然後協議控制將快取中的資料傳送往對端,對端返回一個ack=1,傳送端則清理快取中的資料,對端返回ack=0,則重新發送資料,所以tcp是可靠的。
	而udp傳送資料,對端是不會返回確認資訊的,因此不可靠。

### 2.send(位元組流) 和recv(1024) 及sendall
	recv裡指定的1024意思是從快取裡一次拿出了1024個位元組的資料。
	send的位元組流是先存放入己端快取,然後由協議控制將快取內容法網對端,如果待發送的位元組流大小大於快取剩餘空間,那麼資料丟失,用sendall就會迴圈呼叫send,資料不會丟失。

三 如何解決粘包問題

1 解決方法之low版本

	傳送位元組之前,先發送一段該位元組流的長度,然後接收位元組流長度的資料。

	缺陷:由於程式的執行速度遠快於網路傳輸速度,會因網路延遲造成效能損耗。

2 正確的解決方法

​ 為位元組流加入自定義固定長度報頭,報頭中包含位元組流長度,然後send一次到對端,對端在接收時,先從快取中取出定長的報頭,然後再取真實資料。使用struct模組,可以輔助實現此功能。

2.1 struct模組

# 	該模組可以把一個型別,如數字,轉成固定長度的bytes。
struct.pack(fmt,v1,v2,…)
返回的是一個字串,是引數按照fmt資料格式組合而成

struct.unpack(fmt,string)
按照給定資料格式解開(通常都是由struct.pack進行打包)資料,返回值是一個tuple

2.2 遠端執行命令程式解決粘包問題

服務端:

##——————————————————————————————————————server端遠端執行指令解決粘包問題

import subprocess
import struct
from socket import *

server = socket(AF_INET, SOCK_STREAM)

server.setsockopt(SOL_SOCKET, SO_REUSEADDR)

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

server.listen(5)

while True:
    conn, client_addr = server.accept()

    while True:
        try:
            cmd = conn.recv(1024)
            obj = subprocess.Popen(cmd.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            total_size = len(stdout) + len(stderr)

            # 先發送資料大小
            conn.send(struct.pack('i', total_size))

            # 再發送真正的資料
            conn.send(stdout)
            conn.send(stderr)

        except Exception:
            break
    conn.close()

server.close()

客戶端:

##——————————————————————————————————————client端遠端執行指令解決粘包問題

import struct
from socket import *

client = socket(AF_INET, SOCK_STREAM)

client.connect(('127.0.0.1', 8080))  # connect__連線

while True:
    cmd = input('>>>:').strip()
    if len(cmd) == 0:  # 禁止傳送空,規避可能的粘包問題
        continue
    client.send(cmd.encode('utf-8'))

    # 先接受資料長度(接收固定位元組的資料) 
    n = 0
    header = b''
    while n < 4:
        data = client.recv(1)
        header += data
        n += 1

    total_size = struct.unpack('i', header)[0]  # unpack出是一個元組,取第一個資料

    # 收真正的資料
    recv_size = 0
    res = b''
    while recv_size < total_size:
        data = client.recv(1024)
        res += data
        recv_size += len(data)

    print(res.decode('gbk'))  # windows下的系統命令,要以gbkg格式解碼

client.close()

2.3 定製複雜的報頭版本

客戶端

##——————————————————————————————————————server端————定製複雜的報頭
import os
import struct
import json
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080))

server.listen(5)
while True:
    conn, client_addr = server.accept()
    print(conn)
    print(client_addr)

    while True:
        try:
            msg = conn.recv(1024).decode('utf-8')
            cmd, file_path = msg.split()
            print(cmd, type(cmd), file_path)
            if cmd == 'get':
                # 一、製作報頭
                print(os.path.getsize(file_path))
                print(os.path.basename(file_path))
                header_dic = {
                    'total_size': os.path.getsize(file_path),
                    'filename': os.path.basename(file_path),
                    'md5': '123123123123'}
                print(header_dic)
                header_json = json.dumps(header_dic)  # 報頭字典使用json序列化
                header_json_bytes = header_json.encode('utf-8')  # 序列化的字串,轉為bytes型別

                # 二、傳送資料
                # 1、先發送報頭長度
                header_size = len(header_json_bytes)
                conn.send(struct.pack('i', header_size))
                # 2、再發送報頭
                conn.send(header_json_bytes)
                # 3、最後傳送真是的資料
                with open(r'%s' % file_path, mode='rb') as f:
                    for line in f:
                        conn.send(line)

        except Exception:
            break

    conn.close()

server.close()

服務端:

##——————————————————————————————————————client端----定製複雜的報頭import structimport jsonfrom socket import *client = socket(AF_INET, SOCK_STREAM)client.connect(('127.0.0.1', 8080))while True:    cmd = input('>>>:').strip()  # get 檔案路徑    if len(cmd) == 0:        continue    client.send(cmd.encode('utf-8'))    # 1.先接收報頭的長度    res = client.recv(4)  # 我們已知報頭長度定長為4    header_size = struct.unpack('i', res)[0]    # 2.再接收報頭    header_json_bytes = client.recv(header_size)    header_json = header_json_bytes.decode('utf-8')    header_dic = json.loads(header_json)    print(header_dic)    # 3.最後接收真實的資料    total_size = header_dic['total_size']    filename = header_dic['filename']    recv_size = 0    with open(r'D:\%s' % filename, mode='wb') as f:        while recv_size < total_size:            data = client.recv(1024)            f.write(data)            recv_size += len(data)client.close()