101 解決粘包問題

1、什麼是粘包

粘包問題是全部語言中都會有的問題,由於只要使用了TCP協議,即便是經過socket編程也都會產生的問題。linux

注意:只有TCP有粘包現象,UDP永遠不會粘包,爲什麼,且聽我娓娓道來。算法

首先須要掌握一個socket收發消息的原理shell

發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。編程

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

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

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

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

udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x個字節的數據就算完成,如果y>x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠socket

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

2、tcp發送數據的四種狀況

假設客戶端分別發送了兩個數據包D1和D2給服務端,因爲服務端一次讀取到的字節數是不肯定的,故可能存在如下4種狀況。

  1. 服務端分兩次讀取到了兩個獨立的數據包,分別是D1和D2,沒有粘包和拆包;
  2. 服務端一次接收到了兩個數據包,D1和D2粘合在一塊兒,被稱爲TCP粘包;
  3. 服務端分兩次讀取到了兩個數據包,第一次讀取到了完整的D1包和D2包的部份內容,第二次讀取到了D2包的剩餘內容,這被稱爲TCP拆包;
  4. 服務端分兩次讀取到了兩個數據包,第一次讀取到了D1包的部份內容D1_1,第二次讀取到了D1包的剩餘內容D1_2和D2包的整包。

特例:若是此時服務端TCP接收滑窗很是小,而數據包D1和D2比較大,頗有可能會發生第五種可能,即服務端分屢次才能將D1和D2包接收徹底,期間發生屢次拆包。

3、struct模塊

使用方法:

struct模塊

import struct
#把一個數字打包成固定長度的4字節,獲得字節格式數據
obj=struct.pack('i',1024) # 'i'是格式
print(obj)
print(len(obj))

# 解包,獲得元祖類型數據
l=struct.unpack('i',obj)[0]
print(l)

4、解決粘包問題

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

4.1 簡單版解決方案

服務器:

import socket
import subprocess
import struct
HOST = "192.168.11.237"
PORT = 8082

soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.bind((HOST,PORT))
soc.listen(5)


while 1:
    print("等待鏈接。。。")
    conn,addr = soc.accept()
    print("鏈接成功。。。\n")
    while 1:
        try:
            data = conn.recv(1024)
            if len(data)==0:    # 長度0說明斷開了鏈接。在windows中沒用,在linux中才有用
                break

            # 把執行正確的內容放到管道中
            obj = subprocess.Popen(str(data, encoding="utf8"), shell=True, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            # 執行的結果 b 格式,gbk編碼(windows平臺)
            suc_data = obj.stdout.read()
            fail_data = obj.stderr.read()
            if suc_data:
                # 1.先打包數據長度,爲四個字節,在發過去
                head = struct.pack("i", len(suc_data))
                conn.send(head)  # 做爲頭信息發過去,字節格式

                # 2.發真正的數據信息
                conn.send(suc_data)

            else:
                # 1.先打包數據長度,爲四個字節,在發過去
                head = struct.pack("i", len(fail_data))
                conn.send(head)  # 做爲頭信息發過去,字節格式

                # 發錯誤信息
                conn.send(fail_data)

        except:
            print("客戶機鏈接中斷。。。")
            break
    conn.close()

soc.close()

客戶端:

import socket
import struct
HOST = "192.168.11.237"
PORT = 8082

soc = socket.socket()
soc.connect((HOST,PORT))

while 1:
    try:
        cmd = input("請輸入須要執行的命令")
        soc.send(cmd.encode("utf8"))

        # 1.獲得數據的頭信息並解包
        head = soc.recv(4)
        data_len = struct.unpack("i",head)[0]

        if head == 0:
            break

        # 2.根據頭信息,拿到數據長度來接收數據
        str_data = b""
        while data_len:
            if data_len > 1024:
                data = soc.recv(1024)  # 你要接收的數據長度必須等於真實接收到的數據長度才能夠中止
                str_data += data
                data_len -= len(data)
            else:
                data = soc.recv(data_len)
                str_data += data
                data_len -= len(data)
                
        print(str_data.decode("gbk"))

    except Exception as a:
        print("服務器關閉了:", a)
        break

# 4.關閉鏈接
soc.close()

4.1 終極版解決方案(xc版本)

簡單版解決方案確實能解決粘包問題,可是若是當要傳輸的數據太大則就會發生問題。下面給出個人解決方案

服務器:

import socket
import subprocess
import struct
import json

HOST = "192.168.11.24"
PORT = 8080

soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.bind((HOST,PORT))
soc.listen(5)

while 1:
    print("等待鏈接。。。")
    conn,addr = soc.accept()
    print(f"主機{addr}鏈接成功,準備接收消息中。。。")
    while 1:
        try:
            # 接收命令
            cmd = conn.recv(1024)
            # 處理命令
            obj = subprocess.Popen(cmd.decode("utf8"), shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            suc_data =  obj.stdout.read()
            fail_data = obj.stderr.read()
            if suc_data:
                # 1.先構造一個字典,把頭信息放到字典中
                head_dic = {"data_size": len(suc_data), "md5":None, "file_name": None}
                # 2.對該字典序列化,獲得字符串(json串)
                head_dic_json_byte = (json.dumps(head_dic)).encode("utf8")
                # 3.將 序列化後的字典 長度打包,做爲頭部
                head = struct.pack("i",len(head_dic_json_byte))

                # 4.先發頭部
                conn.send(head)
                # 5.再發 序列化後的字典(字節類型)
                conn.send(head_dic_json_byte)
                # 6.最後再發真正的數據
                conn.send(suc_data)
            else:
                # 1.先構造一個字典,把頭信息放到字典中
                head_dic = {"data_size": len(fail_data), "md5": None, "file_name": None}
                # 2.對該字典序列化,獲得字符串(json串)
                head_dic_json_byte = (json.dumps(head_dic)).encode("utf8")
                # 3.將 序列化後的字典 長度打包,做爲頭部(字節類型)
                head = struct.pack("i", len(head_dic_json_byte))

                # 4.先發頭部
                conn.send(head)
                # 5.再發 序列化後的字典(字節類型)
                conn.send(head_dic_json_byte)
                # 6.最後再發真正的數據
                conn.send(fail_data)

        except:
            print("客戶機鏈接中斷。。。\n")
            break
    conn.close()

soc.close()

客戶端

import socket
import struct
import json

HOST = "192.168.11.24"
PORT = 8080

soc = socket.socket()
soc.connect((HOST,PORT))

while 1:
    try:
        cmd = input("請輸入須要執行的命令")
        soc.send(cmd.encode("utf8"))

        # 1.先拿到頭數據,並解包
        head = soc.recv(4)
        head_dic_len = struct.unpack("i",head)[0]

        if len(head) == 0:  # 服務器關閉了
            break

        # 2.根據頭字典長度拿到頭字典信息,進行反序列化,拿到頭字典
        head_dic_btye = soc.recv(head_dic_len)
        head_dic = json.loads(head_dic_btye)   # json能夠直接反序列化bytes類型

        # 3.根據頭字典中的數據長度拿到真正的數據
        data_len= head_dic.get("data_size")
        str_data = b""
        while data_len:
            if data_len > 1024:
                data = soc.recv(1024)  # 你要接收的數據長度必須等於真實接收到的數據長度才能夠中止
                str_data += data
                data_len -= len(data)
            else:
                data = soc.recv(data_len)
                str_data += data
                data_len -= len(data)

        print(str_data.decode("gbk"))

    except Exception as a:
        print("服務器關閉了:", a)
        break

# 4.關閉鏈接
soc.close()
相關文章
相關標籤/搜索