Python-粘包问题

粘包发生的场景

当应用程序使用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''

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

粘包问题的解决方案

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

  • 数据的传输可以像TCP的传输模式一样,定制一个固定长度的报头,在报头中指定数据的长度,和其它信息,这样在接收端就可以根据固定长度的报头解析出后面数据的长度信息等内容。
  • 如果使用一个固定长度的报头(一般使用struct模块),该模块可以把数字转成固定长度的bytes
  • 但是,有时候如果传输比较大的数据,如好几百G的文件,在报头指定的数据长度和其它信息会超过固定长度(struct在i类型的情况下,取值的范围是 -2147483648 <= number <= 2147483647,也就是最大只能标识2Gb的文件长度),这就需要采用再次迭代的思想,在传输报头前先传输一个固定长度的报头长度描述信息(这个定义的报头信息一般很小,不会超过1Kb),接收端根据此固定长度的信息按指定的长度接收报头信息,再根据报头中的信息接收数据信息,实现传输的控制。

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()


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户...
    树根曰阅读 5,920评论 1 16
  • 传输层-TCP, TCP头部结构 ,TCP序列号和确认号详解 TCP主要解决下面的三个问题 1.数据的可靠传输...
    抓兔子的猫阅读 4,454评论 1 46
  • 到了一个年纪之后,和朋友谈天说地时总少不了喝酒。我其实一点也不爱喝酒,一点儿也不爱的第一个原因是:喝不醉,体会...
    一头猪的闷哼阅读 399评论 0 6
  • 素有礼仪之邦之称的中国在进入近现代以来,国人的文明礼仪修养水平反而在下降。常可在公共场合看到民众不排队、大声接...
    云来无方阅读 74评论 0 2
  • Reading journal –The Gay Genius 20171217 Weekly Note My T...
    叶小静Stamy阅读 233评论 1 0