python中TCP协议中的粘包问题

简介: 1.粘包现象基于TCP实现一个简易远程cmd功能复制代码服务端import socketimport subprocesssever = socket.socket()sever.bind(('127.

1.粘包现象

基于TCP实现一个简易远程cmd功能

复制代码

服务端

import socket
import subprocess
sever = socket.socket()
sever.bind(('127.0.0.1', 33521))
sever.listen()
while True:

client, address = sever.accept()
while True:
    try:
        cmd = client.recv(1024).decode('utf-8')
        p1 = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr= subprocess.PIPE)
        data = p1.stdout.read()
        err_data = p1.stderr.read()
        client.send(data)
        client.send(err_data)
    except ConnectionResetError:
        print('connect broken')
        client.close()
        break

sever.close()


客户端

import socket
client = socket.socket()
client.connect(('127.0.0.1', 33521))
while True:

cmd = input('请输入指令(Q\q退出)>>:').strip().lower()
if cmd == 'q':
    break
client.send(cmd.encode('utf-8'))
data = client.recv(1024)
print(data.decode('gbk'))

client.close()
复制代码
​ 上述是基于TCP协议的远程cmd简单功能,在运行时会发生粘包。

2、什么是粘包?

只有TCP会发生粘包现象,UDP协议永远不会发生粘包;

TCP:(transport control protocol,传输控制协议)流式协议。在socket中TCP协议是按照字节数进行数据的收发,数据的发送方发出的数据往往接收方不知道数据到底长度是多长,而TCP协议由于本身为了提高传输的效率,发送方往往需要收集到足够的数据才会进行发送。使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

UDP:(user datagram protocol,用户数据报协议)数据报协议。在socket中udp协议收发数据是以数据报为单位,服务端和客户端收发数据是以一个单位,所以不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

TCP协议不会丢失数据,UDP协议会丢失数据。

udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

3、什么情况下会发生粘包?

1.由于TCP协议的优化算法,当单个数据包较小的时候,会等到缓冲区满才会发生数据包前后数据叠加在一起的情况。然后取的时候就分不清了到底是哪段数据,这是第一种粘包。

2.当发送的单个数据包较大超过缓冲区时,收数据方一次就只能取一部分的数据,下次再收数据方再收数据将会延续上次为接收数据。这是第二种粘包。

粘包的本质问题就是接收方不知道发送数据方一次到底发送了多少数据,解决问题的方向也是从控制数据长度着手,也就是如何设置缓冲区的问题

4、如何解决粘包问题?

解决问题思路:上述已经明确粘包的产生是因为接收数据时不知道数据的具体长度。所以我们应该先发送一段数据表明我们发送的数据长度,那么就不会产生数据没有发送或者没有收取完全的情况。

1.struct 模块(结构体)

struct模块的功能可以将python中的数据类型转换成C语言中的结构体(bytes类型)

复制代码
import struct
s = 123456789
res = struct.pack('i', s)
print(res)

res2 = struct.unpack('i', res)
print(res2)
print(res2[0])
复制代码

2.粘包的解决方案基本版

既然我们拿到了一个可以固定长度的办法,那么应用struct模块,可以固定长度了。

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

复制代码

服务器端

import socket
import subprocess
import struct
sever = socket.socket()
sever.bind(('127.0.0.1', 33520))
sever.listen()
while True:

client, address = sever.accept()
while True:
    try:
        cmd = client.recv(1024).decode('utf-8')
        #利用子进程模块启动程序
        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        #管道输出的信息有正确和错误的
        data = p.stdout.read()
        err_data = p.stderr.read()
        #先将数据的长度发送给客户端
        length = len(data)+len(err_data)
        #利用struct模块将数据的长度信息转化成固定的字节
        len_data = struct.pack('i', length)
        #以下将信息传输给客户端
        #1.数据的长度
        client.send(len_data)
        #2.正确的数据
        client.send(data)
        #2.错误管道的数据
        client.send(err_data)
    except Exception as e:
        client.close()
        print('连接中断。。。。')
        break
        

客户端

import socket
import struct

client = socket.socket()
client.connect(('127.0.0.1', 33520))
while True:

cmd = input('请输入指令>>:').strip().encode('utf-8')
client.send(cmd)
#1.先接收传过来数据的长度是多少,我们通过struct模块固定了字节长度为4
length = client.recv(4)
#将struct的字节再转回去整型数字
len_data = struct.unpack('i', length)
print(len_data)
len_data = len_data[0]
print('数据长度为%s:' % len_data)

all_data = b''
recv_size = 0
#2.接收真实的数据
#循环接收直到接收到数据的长度等于数据的真实长度(总长度)
while recv_size < len_data:
    data = client.recv(1024)
    recv_size += len(data)
    all_data += data

print('接收长度%s' % recv_size)
print(all_data.decode('gbk'))

复制代码
#总结:

服务器端:
1.在服务器端先收到命令,打开子进程,然后计算返回的数据的长度
2.先利用struct模块将数据长度转成固定4个字节传给客户端
3.再向客户端发送真实的数据。
客户端(两次接收):
1.第一次只接受4个字节,因为长度数据就是4个字节。这样防止了数据粘包。解码得到长度数据
2.第二次循环接收真实数据,拼接真实数据完成解码读取数据。

很显然,如果仅仅只是这样肯定无法满足在实际生产中一些需求。那么该怎么修改?

我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个字节足够用了)

我们可以将自定义的报头设置成这种这种格式。

发送时:

1先发报头长度

2再编码报头内容然后发送

3最后发真实内容

接收时:

1先收报头长度,用struct取出来

2根据取出的长度收取报头内容,然后解码,反序列化

3从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

复制代码

服务器端

import socket
import subprocess
import datetime
import json
import struct
sever = socket.socket()
sever.bind(('127.0.0.1', 33520))
sever.listen()
while True:

client, address = sever.accept()
while True:
    try:
        cmd = client.recv(1024).decode('utf-8')
        #启动子进程
        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        #得到子进程运行的数据
        data = p.stdout.read()   #子进程运行正确的输出管道数据,数据读出来后是字节
        err_data = p.stderr.read() #子进程运行错误的输出管道数据
        #计算数据的总长度
        length = len(data) + len(err_data)
        print('数据总长度:%s' % length)

        #先需要发送报头信息,以下为创建报头信息(至第一次发送)


        #需要添加时间信息
        time_info = datetime.datetime.now()
        #设置一个字典将一些额外的信息和长度信息放进去然后json序列化,报头字典
        masthead = {}
        #将时间数据放入报头字典中
        masthead['time'] = str(time_info)   #时间格式不能被json序列化,所以将其转化为字符串形式
        masthead['length'] = length

        #将报头字典json序列化
        json_masthead = json.dumps(masthead)            #得到json格式的报头
        # 将json格式的报头编码成字节形式
        masthead_data = json_masthead.encode('utf-8')
        #利用struct将报头编码的字节的长度转成固定的字节(4个字节)
        masthead_length = struct.pack('i', len(masthead_data))


        #1.发送报头的长度(第一次发送)
        client.send(masthead_length)
        #2.发送报头信息(第二次发送)
        client.send(masthead_data)
        #3.发送真实数据(第三次发送)
        client.send(data)
        client.send(err_data)
    except ConnectionResetError:
        print('客户端断开连接。。。')
        client.close()
        break
        
        
        

客户端

import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1', 33520))
while True:

cmd = input('请输入cmd指令(Q\q退出)>>:').strip()
if cmd == 'q':
    break

#发送CMD指令至服务器
client.send(cmd.encode('utf-8'))


#1.第一次接收,接收报头信息的长度,由于struct模块固定长度为4字节,括号内直接填4
len_masthead = client.recv(4)
#利用struct反解报头长度,由于是元组形式,取值得到整型数字masthead_length
masthead_length = struct.unpack('i', len_masthead)[0]


#2.第二次接收,接收报头信息,接收长度为报头长度masthead_length 被编码成字节形式的json格式的字典,
# 解字符编码得到json格式的字典masthead_data
masthead_data = client.recv(masthead_length).decode('utf-8')
#得到报头字典masthead
masthead = json.loads(masthead_data)
print('执行时间%s' % masthead['time'])
#通过报头字典得到数据长度
data_length = masthead['length']

#3.第三次接收,接收真实数据,真实数据长度为data_length
# data = client.recv(data_length)   #有可能真实数据长度太大会撑爆内存。
#所以循环读取数据
all_data = b''
length = 0
#循环直到长度大于等于数据长度
while length < data_length:
    data = client.recv(1024)
    length += len(data)
    all_data += data
print('数据的总长度:%s' % data_length)

#我的电脑是Windows系统,所以用gbk解码系统发出的信息
print(all_data.decode('gbk'))

复制代码


总结:

1.TCP协议中,会产生粘包现象。粘包现象产生本质就是读取数据长度未知。

2.解决粘包现象本质就是处理读取数据长度。

3.报头的作用就是解决数据传输过程中数据长度怎么计算传达和传输其他额外信息的。
原文地址https://www.cnblogs.com/5j421/p/10574390.html

相关文章
|
24天前
|
网络协议 网络安全 网络虚拟化
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算。通过这些术语的详细解释,帮助读者更好地理解和应用网络技术,应对数字化时代的挑战和机遇。
67 3
|
1月前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
49 13
|
1月前
|
网络协议 算法 网络性能优化
计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议
计算机网络常见面试题(一):TCP/IP五层模型、应用层常见的协议、TCP与UDP的区别,TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议、ARP协议
|
2月前
|
网络协议 算法 数据格式
【TCP/IP】UDP协议数据格式和报文格式
【TCP/IP】UDP协议数据格式和报文格式
148 3
|
2月前
|
XML JSON 网络协议
【TCP/IP】自定义应用层协议,常见端口号
【TCP/IP】自定义应用层协议,常见端口号
34 3
|
2月前
|
测试技术 网络安全 数据安全/隐私保护
Paramiko是一个用于处理SSHv2协议的Python库
Paramiko是一个用于处理SSHv2协议的Python库
35 3
|
3月前
|
数据采集 JSON API
🎓Python网络请求新手指南:requests库带你轻松玩转HTTP协议
本文介绍Python网络编程中不可或缺的HTTP协议基础,并以requests库为例,详细讲解如何执行GET与POST请求、处理响应及自定义请求头等操作。通过简洁易懂的代码示例,帮助初学者快速掌握网络爬虫与API开发所需的关键技能。无论是安装配置还是会话管理,requests库均提供了强大而直观的接口,助力读者轻松应对各类网络编程任务。
122 3
|
3月前
|
机器学习/深度学习 JSON API
HTTP协议实战演练场:Python requests库助你成为网络数据抓取大师
在数据驱动的时代,网络数据抓取对于数据分析、机器学习等至关重要。HTTP协议作为互联网通信的基石,其重要性不言而喻。Python的`requests`库凭借简洁的API和强大的功能,成为网络数据抓取的利器。本文将通过实战演练展示如何使用`requests`库进行数据抓取,包括发送GET/POST请求、处理JSON响应及添加自定义请求头等。首先,请确保已安装`requests`库,可通过`pip install requests`进行安装。接下来,我们将逐一介绍如何利用`requests`库探索网络世界,助你成为数据抓取大师。在实践过程中,务必遵守相关法律法规和网站使用条款,做到技术与道德并重。
55 2
|
3月前
|
数据采集 存储 JSON
从零到一构建网络爬虫帝国:HTTP协议+Python requests库深度解析
在网络数据的海洋中,网络爬虫遵循HTTP协议,穿梭于互联网各处,收集宝贵信息。本文将从零开始,使用Python的requests库,深入解析HTTP协议,助你构建自己的网络爬虫帝国。首先介绍HTTP协议基础,包括请求与响应结构;然后详细介绍requests库的安装与使用,演示如何发送GET和POST请求并处理响应;最后概述爬虫构建流程及挑战,帮助你逐步掌握核心技术,畅游数据海洋。
73 3
|
3月前
|
数据采集 网络协议 API
HTTP协议大揭秘!Python requests库实战,让网络请求变得简单高效
【9月更文挑战第13天】在数字化时代,互联网成为信息传输的核心平台,HTTP协议作为基石,定义了客户端与服务器间的数据传输规则。直接处理HTTP请求复杂繁琐,但Python的`requests`库提供了一个简洁强大的接口,简化了这一过程。HTTP协议采用请求与响应模式,无状态且结构化设计,使其能灵活处理各种数据交换。
86 8