粘包問題

桃源氏發表於2024-03-20

粘包問題

  • 須知:只有TCP有粘包現象,UDP永遠不會粘包

一、什麼是粘包問題

  • 什麼時候會發生粘包問題?
    • 當TCP傳輸和接收的資料並非我們規定的最大資料量時,就會發生粘包
    • 我們日常傳輸的資料幾乎不可能等於我們規定的資料量,所以我們必須要解決這個問題
  • 為什麼只有TCP會發生粘包問題?
    • TCP是面向流的協議,就是TCP為了提高傳輸效率傳送資料時往往都是收集到足夠多的資料後才傳送一個段
    • 大白話講就是TCP會收集一定的資料然後再傳送,但是接收方並沒有辦法知道發來的資料要按怎麼樣的方式切分,而無法讀取資料
    • UDP則不會使用合併演算法,且接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了訊息頭(訊息來源地址,埠等資訊)
    • 這樣,對於接收端來說,就容易進行區分處理了。 即面向訊息的通訊是有訊息保護邊界的。

[小結]什麼是粘包問題

  • 客戶端傳送需要執行的程式碼
  • 服務端接收到客戶端傳過來的程式碼
    • 服務端呼叫方法執行程式碼並拿到執行後的結果
    • 服務端將執行後的結果進行返回
  • 客戶端接收到服務端返回的結果並做列印輸出

二、粘包問題的演示

  • 我們來使用客戶端傳送一段cmd命令給服務端,在服務端執行並返回資料給客戶端

  • 客戶端接收資料並列印

  • 由於返回的資料長度大於1024所以無法全部顯示

  • 服務端

import socket
import subprocess

# 建立服務物件
server = socket.socket(type=socket.SOCK_STREAM)

# 設定預設的IP和埠號
IP = '127.0.0.1'
PORT = 8081

server.bind((IP, PORT))

server.listen(5)

while True:
    conn, addr = server.accept()
    while True:
        # 檢測可能出現的異常並進行處理
        try:
        # 取到 客戶端發來的cmd命令
            cmd_from_client = conn.recv(1024)

            # 不允許傳入的命令為空
            if not cmd_from_client:
                break

            # 對接收的命令進行解碼
            cmd_from_client = cmd_from_client.decode('gbk')
            # 執行客戶端傳過來的cmd命令並獲取結果
            msg_server = subprocess.Popen(cmd_from_client,
                                          shell=True,  # 執行shell命令
                                          stdout=subprocess.PIPE,  # 管道一
                                          stderr=subprocess.PIPE,  # 管道二
                                          )
            true_res = msg_server.stdout.read()  # 讀取正確結果
            false_res = msg_server.stderr.read()  # 讀取錯誤結果

            conn.send(true_res)
            conn.send(false_res)
        except ConnectionResetError as e:
            break
    conn.close()
  • 客戶端
import socket

while True:
    client = socket.socket(type=socket.SOCK_STREAM)

    # 設定預設的IP和埠號
    IP = '127.0.0.1'
    PORT = 8081

    client.connect((IP, PORT))
    cmd_send_to_server = input('請輸入想要實現的cmd命令:').strip()

    if not cmd_send_to_server:
        break
    cmd_send_to_server = cmd_send_to_server.encode('utf-8')

    client.send(cmd_send_to_server)

    msg_from_server = client.recv(1024)

    msg_from_server = msg_from_server.decode('gbk')

    print(msg_from_server)

    client.close()

  • 客戶端收到的返回的不完整的結果

  • cmd中的結果

  • 可以看出客戶端接收的結果並不完全

三、解決方案

[1]解決問題的思路

  • 拿到資料的總大小 recv_total_size
  • recv_size = 0 ,迴圈接收,每接收一次,recv_size += 接收的長度
  • 直到 recv_size = recv_total_size 表示接受資訊完畢,結束迴圈

[2]解決方案

(1)基礎版

  • 直接使用struct模組打包資料的長度,傳輸到客戶端,方便對資料的處理

  • 服務端

import socket
import subprocess
import struct

server = socket.socket(type=socket.SOCK_STREAM)

IP = '127.0.0.1'
PORT = 8083

server.bind((IP, PORT))

server.listen(5)

while True:
    conn, addr = server.accept()
    cmd_from_client = conn.recv(1024)
    cmd_from_client = cmd_from_client.decode('utf-8')

    msg_server = subprocess.Popen(cmd_from_client,
                                  shell=True,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE,
                                  )
    true_msg = msg_server.stdout.read()
    false_msg = msg_server.stderr.read()

    total_len = len(true_msg) + len(false_msg)

    total_len_pack = struct.pack('i', total_len)

    conn.send(total_len_pack)

    conn.send(true_msg)
    conn.send(false_msg)
    conn.close()
    break
server.close()
  • 客戶端
import socket
import struct

while True:
    client = socket.socket(type=socket.SOCK_STREAM)

    IP = '127.0.0.1'
    PORT = 8083

    client.connect((IP, PORT))

    cmd_send_to_server = input('輸入你想要實現的cmd命令:')

    cmd_send_to_server = cmd_send_to_server.encode('utf8')

    client.send(cmd_send_to_server)

    cmd_from_server_pack = client.recv(4)

    cmd_from_server_unpack = struct.unpack('i', cmd_from_server_pack)

    total_len = cmd_from_server_unpack[0]

    txt = b''
    while total_len > 0:
        msg = client.recv(1024)
        txt += msg
        total_len -= 1024
    print(txt.decode('gbk'))
    break

(2)進階版

  • 思路

    1. 將資料的名字,長度等資訊放入字典中
    2. 透過json模組將字典轉換成json字串並編碼成二進位制資料(需要傳輸的資料:json字串)
    3. 使用struct模組將json字串的長度打包成四位二級制資料(需要傳輸的資料:struct包)
    4. 將原始資料(不是二進位制格式的轉換成二進位制)進行傳輸(需要傳輸的資料:原始資料)
    5. 客戶端可以透過解struct包和json字串的二進位制資料得到之後每一部分資料的長度,並透過迴圈取出每個部分
  • 服務端

import socket
import struct
import subprocess
import json

server = socket.socket()

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

server.listen(5)

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

    msg_from_client = conn.recv(1024)

    msg_from_client = msg_from_client.decode('utf-8')

    if msg_from_client == 'q':
        conn.send(b'q')
        break
    msg_server = subprocess.Popen(msg_from_client,
                                  shell=True,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE,
                                  )
    true_msg = msg_server.stdout.read()
    false_msg = msg_server.stderr.read()

    total_len = len(true_msg) + len(false_msg)

    msg_dict = {
        'name': 'cmd',
        'total_len': total_len
    }

    msg_dict_json = json.dumps(msg_dict)

    msg_dict_json_b = msg_dict_json.encode('utf-8')

    struct_pack = struct.pack('i', len(msg_dict_json_b))

    conn.send(struct_pack)
    conn.send(msg_dict_json_b)
    conn.send(true_msg)
    conn.send(false_msg)

  • 客戶端
import json
import socket
import struct

while True:
    client = socket.socket()

    client.connect(('127.0.0.1', 8080))

    while True:
        cmd_send_to_server = input('請輸入想要實現的cmd命令:')
        if not cmd_send_to_server:
            print('命令不能是空白!')
            continue
        cmd_send_to_server = cmd_send_to_server.encode('utf-8')
        break
    client.send(cmd_send_to_server)

    struct_pack = client.recv(4)

    if struct_pack == b'q':
        break
    struct_unpack = struct.unpack('i', struct_pack)

    dict_json_b = client.recv(struct_unpack[0])

    dict_json = dict_json_b.decode('utf-8')

    msg_dict = json.loads(dict_json)

    total_len = msg_dict['total_len']

    txt = b''
    while total_len > 0:
        msg = client.recv(1024)
        txt += msg
        total_len -= 1024
    print(txt.decode('gbk'))

client.close()

四、案例

  • 一個資料夾中的將一個影片檔案放入另一個資料夾中

  • 服務端

    conn, addr = server.accept()

    # conn.send(json_file_list_b)

    msg_from_client_b = conn.recv(1024)

    if msg_from_client_b == b'q':
        conn.send(b'q')
        break

    msg_from_client = msg_from_client_b.decode('utf-8')

    if msg_from_client not in file_list:
        conn.send('f'.encode('utf-8'))
        continue

    file_path = os.path.join(os.getcwd(), msg_from_client)

    with open(file_path, 'rb') as fp:
        video_data = fp.read()

    video_len = len(video_data)

    headers = {
        'file_name': msg_from_client,
        'video_len': video_len
    }

    headers_json = json.dumps(headers)

    headers_json_b = headers_json.encode('utf-8')

    struct_package = struct.pack('i', len(headers_json_b))

    conn.send(struct_package)

    conn.send(headers_json_b)

    conn.send(video_data)

  • 客戶端
    print('以下是檔案列表:\n')
    # 將操作的目錄轉到目標資料夾
    os.chdir(r'D:\Users\21277\Desktop\home')
    # 將資料夾中的所有檔名放入列表
    file_list = os.listdir(os.getcwd())
    # 將檔名列印出來
    for file_name in file_list:
        print(file_name)

    print('\n')

    client = socket.socket()

    client.connect(('127.0.0.1', 8080))

    # json_file_list_b = client.recv(1024)

    # file_list = json.loads(json_file_list_b.decode('utf-8'))
    #
    # for file_name in file_list:
    #     print(file_name)

    while True:
        msg_send_to_server = input('請輸入想要傳輸的檔案:')
        if not msg_send_to_server:
            print('不能是空白!')
            continue
        msg_send_to_server = msg_send_to_server.encode('utf-8')
        break

    client.send(msg_send_to_server)

    struct_package = client.recv(4)

    if struct_package == b'q':
        break
    if struct_package == b'f':
        print('未找到該檔案!請重新輸入檔名。')
        continue

    headers_json_len = struct.unpack('i', struct_package)[0]

    headers_json_b = client.recv(headers_json_len)

    headers = json.loads(headers_json_b.decode('utf-8'))

    video_len = headers['video_len']

    video_data = b''
    while video_len > 0:
        video_data += client.recv(1024)
        video_len -= 1024

    new_file_name = input('請重新命名檔案(不重新命名則跳過):')
    if not new_file_name:
        new_file_name = msg_send_to_server.decode('utf-8')
    else:
        new_file_name = new_file_name + '.mp4'

    with open(os.path.join(r'D:\Users\21277\Desktop\new', new_file_name), 'wb') as fp:
        fp.write(video_data)

    client.close()

相關文章