Python-粘包问题

2018-01-30  本文已影响0人  断尾壁虎V

粘包发生的场景

当应用程序使用TCP协议发送数据时,由于TCP是基于流式的数据协议,会将数据像水流一样粘在一起,当接收方的数据容量小于发送的数据时,如果不指定接收的数据长度,就会将所有的数据混合在一起,让接收的数据发生混乱。
如:

# 服务端代码:

# coding=utf-8
import subprocess
import socket

server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 在链接异常终止后,再次启动会复用之前的IP端口,防止资源没有释放而产生地址冲突
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  

server.bind(('127.0.0.1',8080))  # 绑定的IP和端口
server.listen(5)        # 参数表示最大可以挂起的连接数
while True:              # 循环建立链接
    conn,client_addr=server.accept()  # 客户端的链接信息

    while True:  # 循环收发消息
        try:
            client_data=conn.recv(1024) # 表示最大收取的消息
            res = subprocess.Popen(client_data.decode('utf-8'),   # 将接收的命令交给shell执行
                                   shell=True,                    # 并将返回的错误输出和标准输出输出到管道
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout=res.stdout.read()
            stderr=res.stderr.read()
            
            if not client_data: break   # 如果收到的消息为空就跳出循环(主要针对在Linux系统上,客户端意外断开,
            conn.send(stdout)           # Linux的服务端出现无穷循环收空包的情况)
            conn.send(stderr)
        except ConnectionResetError:    # 在 Windows系统上,客户端意外断开服务端会出现ConnectionResetError的异常
            break
    conn.close()  # 关闭链接

server.close()    

客户端:

import socket

client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))

while True:
    send_data=input(">>: ").strip()
    if not send_data: continue        # 禁止输入空,防止死锁
    client.send(send_data.encode('utf-8'))  # 发送的文件为bytes类型
    server_data=client.recv(1024)
    print(server_data.decode('gbk'))   # 在windows上,系统命令的返回结果为GBK格式
client.close()

上面的代码在Windows平台上执行tasklist后再执行其它命令就会出现粘包现象,如下是执行结果:

>>: tasklist

映像名称                       PID 会话名              会话#       内存使用 
========================= ======== ================ =========== ============
System Idle Process              0 Services                   0          4 K
System                           4 Services                   0      7,840 K
smss.exe                       420 Services                   0        424 K
csrss.exe                      608 Services                   0      1,384 K
wininit.exe                    704 Services                   0      2,112 K
services.exe                   832 Services                   0      4,576 K
lsass.exe                      840 Services                   0     10,804 K
svchost.exe                    928 Services                   0     10,888 K
svchost.exe                    992 Services                   0      6,836 K
svchost.exe                    724 Services                   0     15,076 K
svchost.exe                    892 Services                   0     87,968 K
svchost.
>>: 
>>: dir
exe                   1160 Services                   0     30,812 K
svchost.exe                   1280 Services                   0     13,680 K
svchost.exe                   1288 Services                   0     16,064 K
svchost.exe                   1296 Services                   0      2,960 K
igfxCUIService.exe            1464 Services                   0      3,696 K
DisplayLinkManager.exe        1744 Services                   0      4,028 K
svchost.exe                   2172 Services                   0     17,288 K
ZhuDongFangYu.exe             2188 Services                   0      7,660 K
svchost.exe                   2296 Services                   0      2,660 K
spoolsv.exe                   2596 Services                   0      9,756 K
dasHost.exe                   2984 Services                   0      6,864 K
ibtsiva.exe                   2124 Services                   0      1,420 K
capiws.exe                    1976 Services                   0      7,464 K
openvpnserv.exe   
>>: 

再次输入的命令后,会依然取出上次命令没有取完的结果(由于我们指定了接受收数据最大为固定的1024字节)。

TCP协议在传输数据的时候,为了提高效率,会启用Nagle算法,将多个较小,且间隔时间很短的两个数据包合并在一起发送,于是就会出现如下粘包现象:

# 服务端

# coding=utf-8
from socket import *
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8081))
server.listen(5)
conn,client_addr=server.accept()
data=conn.recv(10)
print(data)
data1=conn.recv(10)
print(data1)
conn.close() 
server.close()    


# 客户端

from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8081))
client.send('Hello'.encode('utf8'))
client.send('World'.encode('utf8'))
client.close()

运行得到的结果为:

b'HelloWorld'
b''

无论是哪一种 情况,只要在收的时候指定长度,就可以避免此问题。

粘包问题的解决方案

如果知道每次服务端发送的数据长度,按照长指定的长度取数据就不会出现这种情况,对于过长的数据可以循环去取。可以按照如下方式:

struct的应用示例:

import struct

res=struct.pack('i', 2147483647)
print(type(res),res,len(res))
res=struct.pack('i', 2)
print(type(res),res,len(res))

# 输出:
<class 'bytes'> b'\xff\xff\xff\x7f' 4
<class 'bytes'> b'\x02\x00\x00\x00' 4
# 可以看出,无论数据是'2147483647'还是'2',最终都转化为了4个字节长度

优化后的传输代码:

# 服务端
# coding=utf-8

import subprocess
import socket
import struct
import json

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 在链接异常终止后,再次启动会复用之前的IP端口,防止资源没有释放而产生地址冲突
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 绑定的IP和端口
server.bind(('127.0.0.1', 8080))

# 参数5表示最大可以挂起的连接数
server.listen(5)

# 循环建立链接
while True:
    # 客户端的链接信息
    conn, client_addr = server.accept()  
    # 循环收发消息
    while True:  
        try:
            # 表示最大收取的消息
            client_data = conn.recv(1024)  

            # 将接收的命令交给shell执行,并将返回的错误输出和标准输出输出到管道
            res = subprocess.Popen(client_data.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = res.stdout.read()
            stderr = res.stderr.read()
            total_size = len(stdout) + len(stderr)
            # 自定义报头信息
            header = {'total_size': total_size, 'MD5': '123456', 'msg_type': 'cmd_res'}
            # 将字典转化为json格式后才能被反解
            header_json = json.dumps(header)
            # 将json转为bytes用于传输
            header_json_bytes = bytes(header_json, encoding='utf-8')
            # 将header_json_bytes打包为固定的4个字节长度
            header_size = struct.pack('i', len(header_json_bytes))
            # 如果收到的消息为空就跳出循环(主要针对在Linux系统上,客户端意外断开,Linux的服务端出现无穷循环收空包的情况)
            if not client_data: break
            # 发送头长度信息,为4个字节
            conn.send(header_size)
            # 发送头信息
            conn.send(header_json_bytes)  
            conn.send(stdout)
            conn.send(stderr)
            
            # 在 Windows系统上,客户端意外断开服务端会出现ConnectionResetError的异常
        except ConnectionResetError:  
            break
    conn.close()  # 关闭链接

server.close()


# 客户端

import socket
import struct
import json

client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))

while True:
    send_data=input(">>: ").strip()
    if not send_data: continue        # 禁止输入空,防止死锁
    client.send(send_data.encode('utf-8'))  # 发送的文件为bytes类型
    header_size=client.recv(4)
    header_json_lens=struct.unpack('i',header_size)[0]
    header_json_bytes=client.recv(header_json_lens)
    header_json=json.loads(header_json_bytes.decode('utf-8'))
    total_size=header_json['total_size']
    file_MD5=header_json['MD5']
    print(file_MD5)
    data_size=0
    server_data=b''
    while total_size > data_size:
        server_data+=client.recv(1024)
        data_size=len(server_data)

    print(server_data.decode('gbk'))   # 在windows上,系统命令的返回结果为GBK格式
client.close()

FTP小示例

# ftp-server.py

import subprocess
import socket
import struct
import json
import os
import hashlib

# 上传下载文件,通过read的方式读取bytes格式的文件.
ftp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 在链接异常终止后,再次启动会复用之前的IP端口,防止资源没有释放而产生地址冲突
ftp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 绑定的IP和端口
ftp_server.bind(('127.0.0.1', 8080))

# 参数5表示最大可以挂起的连接数
ftp_server.listen(5)
Base_Dir = "D:\\temp\\"
# 循环建立链接
while True:
    # 客户端的链接信息
    conn, client_addr = ftp_server.accept()
    # 循环收发消息
    while True:
        try:
            # 从服务器下载文件到客户端
            client_data = conn.recv(1024)
            if not client_data: break
            method = client_data.decode('utf-8').split()[0]
            print(method)
            filename = client_data.decode('utf-8').split()[1]
            print(filename)
            filename_path = Base_Dir + filename
            print(filename_path)


            if method == 'get':     # 生成MD5
                if not os.path.exists(filename_path):
                    conn.send("0000".encode('utf-8'))
                    continue
                total_size = os.path.getsize(filename_path)
                m = hashlib.md5()
                with open(filename_path,'rb') as f:
                    for line in f:
                        m.update(line)
                MD5 = m.hexdigest()
                header = {'total_size': total_size, 'MD5': MD5, 'filename': filename}
                header_json = json.dumps(header)
                header_json_bytes = bytes(header_json, encoding='utf-8')
                header_size = struct.pack('i', len(header_json_bytes))
                conn.send(header_size)
                conn.send(header_json_bytes)
                with open(filename_path,'rb') as f1:
                    for line1 in f1:
                        conn.send(line1)

            if method == 'upload':
                header_size = conn.recv(4)
                header_json_lens = struct.unpack('i', header_size)[0]
                print(header_json_lens)
                header_json_bytes = conn.recv(header_json_lens)
                header_json = json.loads(header_json_bytes.decode('utf-8'))
                total_size = header_json['total_size']
                file_MD5 = header_json['MD5']
                filename = header_json['filename']
                filename_path = Base_Dir + filename
                print(file_MD5)
                data_size = 0
                server_data = b''
                with open(filename_path, 'ab') as f:
                    while total_size > data_size:
                        server_data = conn.recv(1024)
                        f.write(server_data)
                        data_size += len(server_data)

        except ConnectionResetError:
            break
    conn.close()  # 关闭链接

ftp_server.close()



# ftp-client.py

import socket
import struct
import json
import hashlib
import os

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))

while True:
    send_data = input(">>: ").strip()
    if not send_data: continue  # 禁止输入空,防止死锁
    if send_data.upper() == 'Q': break
    method = send_data.split()[0]
    file_name = send_data.split()[1]

    if method == 'get':
        client.send(send_data.encode('utf-8'))  # 发送的文件为bytes类型
        header_size=client.recv(4)
        if header_size.decode('utf-8') == '0000':
            print("FTP Server上不存在此文件!")
            continue
        header_json_lens=struct.unpack('i',header_size)[0]
        print(header_json_lens)
        header_json_bytes=client.recv(header_json_lens)
        header_json=json.loads(header_json_bytes.decode('utf-8'))
        total_size=header_json['total_size']
        file_MD5 = header_json['MD5']
        filename = header_json['filename']
        print(file_MD5)
        data_size=0
        server_data=b''
        with open(filename,'ab') as f:
            while total_size > data_size:
                server_data = client.recv(1024)
                f.write(server_data)
                data_size += len(server_data)
    elif method == 'upload':
        if not os.path.exists(file_name):
            print("文件不存在!")
            continue
        client.send(send_data.encode('utf-8'))  # 发送的文件为bytes类型
        total_size = os.path.getsize(file_name)
        m = hashlib.md5()
        with open(file_name, 'rb') as f:
            for line in f:
                m.update(line)
        MD5 = m.hexdigest()
        header = {'total_size': total_size, 'MD5': MD5, 'filename': file_name}
        header_json = json.dumps(header)
        header_json_bytes = bytes(header_json, encoding='utf-8')
        header_size = struct.pack('i', len(header_json_bytes))
        client.send(header_size)
        client.send(header_json_bytes)
        with open(file_name, 'rb') as f1:
            for line1 in f1:
                client.send(line1)

    else:
        print("没有此方法!")
        continue

client.close()


上一篇 下一篇

猜你喜欢

热点阅读