1. 程式人生 > >Python之路 - 網絡編程之粘包

Python之路 - 網絡編程之粘包

end spro 獲取 無連接 lse decode break etop bre

  • Python之路 - 網絡編程之粘包
    • 粘包 ??
    • 解決方法 ??
      • low方法 ??
      • 制作報頭 ??

粘包 ??

由上一篇<Python之路 - Socket實現遠程執行命令>中所出現的問題引出了粘包這個問題 , 粘包到底是什麽?

首先 , 粘包現象只出現在TCP中 , 為什麽說只有在TCP中才會發生粘包現象 , 先來詳細解釋一下TCP與UDP吧

TCP

TCP (transprot control protocol, 傳輸控制協議) 是面向連接的 , 面向流的 , 提供高可靠性服務 . 收發兩端都有要一一對應的socket(一對一模式) , 因此發送端為了將多個發往接收端的包 , 更有效的發到對方 , 使用了優化方法(Nagle算法) , 將多次間隔較小且數據量小的數據 , 合並成一個大的數據塊 , 然後進行封包 .

必須提供科學的拆包機制 , 才能進行合理的分辨 , 所以說面向流的通信是無消息保護邊界的

UDP

UDP(user datagram protocol, 用戶數據報協議) 是無連接的 , 面向消息的 , 提供高效率服務 . 不使用塊的合並優化算法 , 由於UDP支持的是一對多的模式 , 所以接收端的skbuff (套接字緩沖區) 采用了鏈式結構來記錄每一個到達的UDP包 , 在每個UDP包中就有了消息頭 (消息來源地址 , 端口等信息) , 這樣 , 對於接收端來說 , 就容易進行區分處理了 . 即面向的通信是有消息保護邊界的

區別

TCP是基於數據流的 , 於是收發的消息不能為空 , 這就需要在客戶端和服務端都添加空消息的處理機制 , 防止程序卡住 , 而UDP是基於數據報的 , 就算收發空內容 , 也不是空消息 , UDP協議會自動幫你封裝上消息頭

粘包現象發生的原因

粘包分為兩種

  1. 發送方引起的粘包

    這種情況下引起的粘包是TCP協議本身造成的 , TCP為了提高傳輸效率 , 發送方往往要收集到足夠多的數據後才發送一個TCP段 (超過時間間隔也會發送,時間間隔是很短的) , 如果連續幾次需要發送的數據都很少 , 通常TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去 , 所以幾次的數據到接收方時就粘成一包了

    如下 :

    # 發送方第一次發送
    send(b"I‘m ")
    # 立馬第二次,不超過時間間隔
    send(b"Lyon")
    -------------
    # 接收
    data = recv(1024)
    # 收到的是兩次粘在一起的數據
    print(data.decode())
    # 打印結果: I‘m Lyon
    
  2. 接收方引起的粘包

    這種情況引起的粘包則是因為接收方不及時接收緩沖區的數據包造成的 , 比如發送方一次發送了10字節的數據 , 而接收方只接收了2字節 , 那麽剩余的8字節的數據將都在緩沖區等待接收 , 而此時發送方又發送了2字節的數據 , 過了一會接收方接收了20字節(大於剩余10字節) , 接收完畢 , 緩沖區剩余的數據就和第二次發送的數據粘成了一個包 , 產生粘包

    如下 :

    # 發送4字節內容
    send(b"I‘m ")
    # 接收1字節,緩沖區還有3字節
    data1 = recv(1)
    print("data1:",data1)
    # 發送4字節內容,粘到緩沖區中剩余的3字節後面
    send(b"Lyon")
    # 接收7字節,接收完畢
    data2 = recv(7)
    print("data2:",data2)
    ‘‘‘
    打印結果:
    data1:I
    data2:‘m Lyon
    ‘‘‘
    

SO : 所以所謂粘包問題主要還是因為接收方不知道消息之間的界限 , 不知道一次性提取多少字節的數據所造成的

解決方法 ??

既然粘包是因為接收方不知道消息界限 , 那麽我們就自己創建界限

low方法 ??

我們只需要對上一篇中subprocess_server.py以及subprocess_client.py 做一點點修改就行了

subprocess_server_development.py

import socket
import subprocess
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((‘127.0.0.1‘, 8080))
sock.listen(5)
while True:
    print("Waitting for connection...")
    conn, addr = sock.accept()
    print("{}successful connection...".format(addr))
    while True:
      # 接收指令
        cmd = conn.recv(1024)
        if not cmd:
            print("Client is disconnected...")
            break
        print("The command is {}".format(cmd.decode()))
        # 獲取執行結果
        data = subprocess.Popen(cmd.decode(),shell=True,
                                stdout=subprocess.PIPE,
                                stdin=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        # 獲取錯誤句柄
        err = data.stderr.read()
        if err:
            res = err
        else:
            res = data.stdout.read()
        # 發送數據長度
        conn.send(str(len(res)).encode(‘utf-8‘))
        # 防止與兩次發送數據粘在一起
        ready = conn.recv(1024)
        if ready == b‘OK‘:
            # sendall連續調用send完成發送
            conn.sendall(res)
    conn.close()
sock.close()

subprocess_client_development.py

import socket
sock = socket.socket()
sock.connect((‘127.0.0.1‘, 8080))
while True:
    cmd = input("Please input the command:").strip()
    if not cmd:
        print("Can‘t empty...")
        continue
    elif cmd == ‘exit‘:
        break
    # 發送指令
    sock.send(cmd.encode(‘utf-8‘))
    # 獲取數據長度
    length = sock.recv(1024).decode(‘utf-8‘)
    # 發送標誌
    sock.send(b‘OK‘)
    recvsize = 0
    data = b‘‘
    # 循環接收
    while recvsize < int(length):
        recvdata = sock.recv(1024)
        recvsize += len(recvdata)
        data += recvdata
    print(data.decode(‘gbk‘))
sock.close()

利用這種方式 , 我們需要提前先將數據大小發送過去 , 這無疑會放大網絡延遲帶來的性能損耗

制作報頭 ??

既然需要將大小發送過去 , 那我們是不是可以為字節流加上自定義固定長度報頭 , 報頭中包換數據大小等信息 , 然後一次直接發送過去 , 對方只要在接收的時候先從取出報頭 , 再取數據

所以我們只需要固定好報頭的長度可以了 , 我們可以利用struct模塊來制作報頭 , 只需對上方法稍作修改

subprocess_struct_server.py

import socket,struct
import subprocess
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((‘127.0.0.1‘, 8080))
sock.listen(5)
while True:
    print("Waitting for connection...")
    conn, addr = sock.accept()
    print("{}successful connection...".format(addr))
    while True:
        cmd = conn.recv(1024)
        if not cmd:
            print("Client is disconnected...")
            break
        print("The command is {}".format(cmd.decode()))
        data = subprocess.Popen(cmd.decode(),shell=True,
                                stdout=subprocess.PIPE,
                                stdin=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        err = data.stderr.read()
        if err:
            res = err
        else:
            res = data.stdout.read()
        # 制作4位固定報頭並發送
        conn.send(struct.pack(‘i‘, len(res)))
        # 直接循環發送
        conn.sendall(res)
    conn.close()
sock.close()

subprocess_struct_client.py

import socket,struct
sock = socket.socket()
sock.connect((‘127.0.0.1‘, 8080))
while True:
    cmd = input("Please input the command:").strip()
    if not cmd:
        print("Can‘t empty...")
        continue
    elif cmd == ‘exit‘:
        break
    sock.send(cmd.encode(‘utf-8‘))
    res = sock.recv(4)
    # 解開報頭取出數據長度
    length = struct.unpack(‘i‘, res)[0]
    recvsize = 0
    data = b‘‘
    # 循環接收
    while recvsize < length:
        data += sock.recv(1024)
        recvsize += len(data)
    print(data.decode(‘gbk‘))
sock.close()

Python之路 - 網絡編程之粘包