1. 程式人生 > >Python Socket通訊黏包問題分析及解決方法

Python Socket通訊黏包問題分析及解決方法

參考:http://www.cnblogs.com/Eva-J/articles/8244551.html#_label5

1.黏包的表現(以客戶端遠端操作服務端命令為例)

注:只有以TCP協議通訊的情況下,才會產生黏包問題

基於TCP協議實現的黏包

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# tcp_server_cmd.py

import socket
import subprocess

ip_port    = ('127.0.0.1', 8080) #服務端地址及埠
BUFFERSIZE = 1024 #設定緩衝區大小

tcp_server_socket 
= socket.socket(socket.AF_INET, socket.SOCK_STREAM) #設定為通過TCP協議通訊(預設) tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1)#用於socket關閉後,重用socket tcp_server_socket.bind(ip_port) #繫結ip和埠 tcp_server_socket.listen() #開始監聽客戶端連線 while True: conn, addr = tcp_server_socket.accept() #與客戶端建立連線
print('客戶端地址:', addr) while True: cmd = conn.recv(BUFFERSIZE).decode('utf-8') #接收客戶端輸入 print('cmd:', cmd) if len(cmd)<1 or cmd == 'quit': break res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr
=subprocess.PIPE) #執行客戶端輸入命令 #以下標準輸出資訊都只能讀取一次 std_out = res.stdout.read() #獲取輸出到標準輸出裝置的成功資訊 std_err = res.stderr.read() #獲取輸出到標準輸出裝置的錯誤資訊 print("stdout:",std_out.decode('gbk')) print("stderr:",std_err.decode('gbk')) conn.send(std_out) conn.send(std_err) conn.close() #關閉連線 tcp_server_socket.close() #關閉socket
tcp-server-package
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#tcp_client_cmd.py

import socket

ip_port = ('127.0.0.1', 8080)  #服務端地址及埠
BUFFERSIZE = 1024   #設定緩衝區大小
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #獲取socket物件
tcp_client_socket.connect(ip_port) #與服務端建立連線

while True:
    cmd = input("Please input cmd<<< ").strip() #輸入命令
    if len(cmd) < 1: 
        continue     #跳過本次迴圈,開始下一次迴圈
    elif cmd == 'quit': 
        tcp_client_socket.send(cmd.encode('utf-8')) #傳送中斷請求給服務端
        break     #中斷迴圈

    tcp_client_socket.send(cmd.encode('utf-8'))
    ret = tcp_client_socket.recv(BUFFERSIZE)
    print(ret.decode('gbk'))

tcp_client_socket.close()
tcp-client-package

基於UDP協議實現(無黏包現象)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# udp_server_cmd.py

import socket
import subprocess

ip_port    = ('127.0.0.1', 8080)
BUFFERSIZE = 2048

udp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #設定為通過UDP協議通訊
udp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
udp_server_socket.bind(ip_port)

while True:
    cmd, addr = udp_server_socket.recvfrom(BUFFERSIZE)
    print('client ip:',addr)

    cmd = cmd.decode('utf-8')
    print('cmd:',cmd)
    if len(cmd)<1 or cmd == 'quit':break

    res = subprocess.Popen(cmd, shell=True, 
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE)


    std_out = res.stdout.read()
    std_err = res.stderr.read()
    print('stdout:', std_out.decode('gbk'))
    print('stderr:', std_err.decode('gbk'))


    udp_server_socket.sendto(std_out, addr)
    udp_server_socket.sendto(std_err, addr)

udp_server_socket.close()
udp-server-package
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# udp_client_cmd.py

import socket

ip_port    = ('127.0.0.1', 8080)
BUFFERSIZE = 2048

udp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_client_socket.connect(ip_port)

while True:
    cmd = input("Please input cmd<<< ").strip()
    if len(cmd)<1: continue
    elif cmd == 'quit': 
        udp_client_socket.sendto(cmd.encode('utf-8'), ip_port)
        break

    udp_client_socket.sendto(cmd.encode('utf-8'), ip_port)
    ret, addr = udp_client_socket.recvfrom(BUFFERSIZE)

    print(ret.decode('gbk'))

udp_client_socket.close()
udp-client-cmd

2.黏包的成因(基於TCP協議傳輸)

  • tcp協議的拆包機制
  • tcp面向流的通訊是無訊息保護邊界的
  • tcp的Nagle優化演算法:若連續幾次需要send的資料都很少,通常TCP會根據優化演算法把這些資料合成一個TCP段後一次傳送出去,這樣接收方就收到了粘包資料
  • 接收方和傳送方的快取機制

3.導致黏包的根本因素

  • 接收方不知道訊息之間的界限,不知道一次性提取多少位元組的資料

4.黏包的解決方法

 由於導致黏包的根本原因是接收端不知道傳送端將要傳送的位元組流的長度,故有如下兩種解決方案

方案一:在傳送訊息前,將要傳送的位元組流總大小讓接收端知曉,然後接收端來一個死迴圈接收完所有資料

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# tcp_server_cmd.py

"""
實現客戶端遠端操作服務端命令
"""
import socket
import subprocess

ip_port    = ('127.0.0.1', 8080) #服務端地址及埠
BUFFERSIZE = 1024 #設定緩衝區大小

tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #設定為通過TCP協議通訊(預設)
tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1)#用於socket關閉後,重用socket
tcp_server_socket.bind(ip_port) #繫結ip和埠
tcp_server_socket.listen() #開始監聽客戶端連線

flag = True

while flag:
    conn, addr = tcp_server_socket.accept() #與客戶端建立連線
    print('client ip addr:', addr)

    while True:
        cmd = conn.recv(BUFFERSIZE).decode('utf-8') #接收客戶端輸入
        if len(cmd)<1 or cmd == 'quit': 
            flag = False #防止死迴圈,在多個客戶端連線時,可以去掉
            break

        res = subprocess.Popen(cmd, shell=True, 
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE) #執行客戶端輸入命令
        #以下標準輸出資訊都只能讀取一次
        std_err = res.stderr.read() #獲取輸出到標準輸出裝置的錯誤資訊
        if std_err:  #判斷返回資訊的型別
            ret = std_err
        else:
            ret = res.stdout.read() #獲取輸出到標準輸出裝置的成功資訊
        
        """
        以下是方案一的核心部分
        """
        conn.send(str(len(ret)).encode('utf-8')) #傳送要傳送資訊的長度
        print("ret:",ret.decode('gbk'))

        data = conn.recv(BUFFERSIZE).decode('utf-8') #接收客戶端準備確認資訊
        if data == 'recv_ready': 
            conn.sendall(ret) #傳送所有資訊

    conn.close() #關閉連線

tcp_server_socket.close() #關閉socket
tcp_server_package
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#client_tcp_cmd.py

import socket

ip_port = ('127.0.0.1', 8080)  #服務端地址及埠
BUFFERSIZE = 1024   #設定緩衝區大小
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #獲取socket物件
tcp_client_socket.connect(ip_port) #與服務端建立連線

while True:
    cmd = input("Please input cmd<<< ").strip() #輸入命令
    if len(cmd) < 1: 
        continue     #跳過本次迴圈,開始下一次迴圈
    elif cmd == 'quit': 
        tcp_client_socket.send(cmd.encode('utf-8')) #傳送中斷請求給服務端
        break     #中斷迴圈

    tcp_client_socket.send(cmd.encode('utf-8')) #傳送要執行的命令

    """
    以下是方案一的核心部分
    """
    info_len = tcp_client_socket.recv(BUFFERSIZE).decode('utf-8') #接收要接收的資訊長度

    tcp_client_socket.send(b'recv_ready') #給服務端傳送已經準備好接收資訊

    data     = b''
    ret_size = 0
    while ret_size < int(info_len): #判斷資訊是否已接收完
        data += tcp_client_socket.recv(BUFFERSIZE) #接收指定大小的資訊
        ret_size += len(data)  #將已經接收的資訊長度累加

    print(data.decode('gbk'))

tcp_client_socket.close()      #關閉socket
tcp_client_package
存在的問題:
程式的執行速度遠快於網路傳輸速度,所以在傳送一段位元組前,先用send去傳送該位元組流長度,這種方式會放大網路延遲帶來的效能損耗

方案二:針對方案一的問題,引入struct模組,struct模組可以將傳送的資料長度轉換成固定長度的位元組

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# tcp_server_cmd.py

"""
實現客戶端遠端操作服務端命令
"""
import socket
import subprocess
import struct
import json

ip_port    = ('127.0.0.1', 8080) #服務端地址及埠
BUFFERSIZE = 1024 #設定緩衝區大小

tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #設定為通過TCP協議通訊(預設)
tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1)#用於socket關閉後,重用socket
tcp_server_socket.bind(ip_port) #繫結ip和埠
tcp_server_socket.listen() #開始監聽客戶端連線

flag = True

while flag:
    conn, addr = tcp_server_socket.accept() #與客戶端建立連線
    print('client ip addr:', addr)

    while True:
        cmd = conn.recv(BUFFERSIZE).decode('utf-8') #接收客戶端輸入
        if len(cmd)<1 or cmd == 'quit': 
            flag = False #防止死迴圈,在多個客戶端連線時,可以去掉
            break

        res = subprocess.Popen(cmd, shell=True, 
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE) #執行客戶端輸入命令
        #以下標準輸出資訊都只能讀取一次
        std_err = res.stderr.read() #獲取輸出到標準輸出裝置的錯誤資訊
        if std_err:  #判斷返回資訊的型別
            back_info = std_err
        else:
            back_info = res.stdout.read() #獲取輸出到標準輸出裝置的成功資訊
        
        """
        以下是方案二的核心部分(定製化報頭)
        """
        head        = {'data_size':len(back_info)}
        head_json   = json.dumps(head) #將python物件轉化為json字串
        head_bytes  = bytes(head_json, encoding='utf-8') #將json字串轉化為bytes位元組碼物件
        head_struct_len = struct.pack('i', len(head_bytes)) #使用struct將定製化的報頭打包為4個位元組的長度
        conn.send(head_struct_len)  #傳送定製報頭的長度,4個位元組
        conn.send(head_bytes) #傳送定製報頭資訊

        print("back_info:",back_info.decode('gbk'))
        conn.sendall(back_info) #傳送所有的真實資訊

    conn.close() #關閉連線

tcp_server_socket.close() #關閉socket
tcp_server_package
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#client_tcp_cmd.py

import socket
import struct
import json

ip_port = ('127.0.0.1', 8080)  #服務端地址及埠
BUFFERSIZE = 1024   #設定緩衝區大小
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #獲取socket物件
tcp_client_socket.connect(ip_port) #與服務端建立連線

while True:
    cmd = input("Please input cmd<<< ").strip() #輸入命令
    if len(cmd) < 1: 
        continue     #跳過本次迴圈,開始下一次迴圈
    elif cmd == 'quit': 
        tcp_client_socket.send(cmd.encode('utf-8')) #傳送中斷請求給服務端
        break     #中斷迴圈

    tcp_client_socket.send(cmd.encode('utf-8')) #傳送要執行的命令

    """
    以下是方案二的核心部分(定製化報頭)
    """
    head_struct = tcp_client_socket.recv(4) #接收4位元組的定製報頭
    head_json_len = struct.unpack('i', head_struct)[0] #struct解包定製報頭後是一個tuple,如(1024,)
    head_json = tcp_client_socket.recv(head_json_len).decode('utf-8') #將接收的bytes位元組碼報頭解碼為json字串
    head = json.loads(head_json) #將json字串轉化為python物件
    print('head:',head)


    data     = b''
    ret_size = 0
    while ret_size < head['data_size']: #判斷資訊是否已接收完
        data += tcp_client_socket.recv(BUFFERSIZE) #接收指定緩衝大小的資訊
        ret_size += len(data)  #將已經接收的資訊長度累加

    print(data.decode('gbk'))  #windows預設編碼是gbk

tcp_client_socket.close()      #關閉socket
tcp_client_package

5.TCP和UDP協議的簡介

 待補充。。。

6.補充

1.[WinError 10013] 以一種訪問許可權不允許的方式做了一個訪問套接字的嘗試

原因:埠被佔用導致

解決:

Windows下
C:\> netstat -ano|findstr 8080             #查詢8080端口占用程序號
TCP    127.0.0.1:8080         0.0.0.0:0              LISTENING       17496
C:\> tasklist |findstr 17496               #查詢17496程序號對應的程式
python.exe                   17496 Console                    1     10,664 K
C:\> taskkill /pid 17496 /F                #殺掉17496程序
成功: 已終止 PID 為 17496 的程序。

Linux下
[[email protected]]# netstat -nltup | grep 80 #查詢80埠上的程式
tcp        0      0 0.0.0.0:80                  0.0.0.0:*                   LISTEN      1479/nginx  
[[email protected]]# ps -ef | grep nginx      #查詢nginx對應程序號
root      1479     1  0 Jul23 ?        00:00:00 nginx: master process ./nginx
[[email protected]]# kill -9  1479            #殺掉1479程序

 2.struct模組可打包和解包的資料型別

3.socket模組方法說明

服務端套接字函式
s.bind()    繫結(主機,埠號)到套接字
s.listen()  開始TCP監聽
s.accept()  被動接受TCP客戶的連線,(阻塞式)等待連線的到來

客戶端套接字函式
s.connect()     主動初始化TCP伺服器連線
s.connect_ex()  connect()函式的擴充套件版本,出錯時返回出錯碼,而不是丟擲異常

公共用途的套接字函式
s.recv()            接收TCP資料
s.send()            傳送TCP資料
s.sendall()         傳送TCP資料
s.recvfrom()        接收UDP資料
s.sendto()          傳送UDP資料
s.getpeername()     連線到當前套接字的遠端的地址
s.getsockname()     當前套接字的地址
s.getsockopt()      返回指定套接字的引數
s.setsockopt()      設定指定套接字的引數
s.close()           關閉套接字

面向鎖的套接字方法
s.setblocking()     設定套接字的阻塞與非阻塞模式
s.settimeout()      設定阻塞套接字操作的超時時間
s.gettimeout()      得到阻塞套接字操作的超時時間

面向檔案的套接字的函式
s.fileno()          套接字的檔案描述符
s.makefile()        建立一個與該套接字相關的檔案

socket模組方法