网络与并发(四)
Python-网络与并发
第四章 黏包
在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。
而对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。所以UDP不会出现粘包问题。
这里要介绍两个概念长连接和短连接
长连接,指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。
短连接是相对于长连接而言的概念,指在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。
好的,我们先看一段代码,
服务器端
# coding=utf-8
# 文件名:spackag_server.py
# 导入socket模块
from socket import *
# 创建服务器套接字
server_socket = socket(AF_INET, SOCK_STREAM)
# 设定ip与,端口并绑定
server_socket.bind(('127.0.0.1', 8080))
# 端口监听事件,等待客户端链接
server_socket.listen()
# accept()接受一个客户端的连接请求,并返回一个新的套接字
# 每个连接进来的客户端,都会通过accept函数返回一个不同的客户端的socket对象和属于客户端的套接字
conn, addr = server_socket.accept()
# 循环
while True:
# 输入信息
cmd = input('>>>')
# 数据转换字节编码,并发送
conn.send((cmd.encode('utf-8')))
# 接收信息并解码为原数据类型
ret = conn.recv(1024).decode('utf-8')
# 打印接受的数据
print(ret)
# 服务器端
conn.close()
sk.close()
客户端代码
# coding=utf-8
# 文件名:spackag_server.py
# 导入socket, subprocess模块
from socket import *
import subprocess
# 创建客户端套接字对象
client_socket = socket(AF_INET, SOCK_STREAM)
# 链接务器
client_socket.connect(('127.0.0.1', 8080))
# 循环链接事件
while True:
# 接收数据缓冲区的最大1024字节数据 ,并解码
cmd = client_socket.recv(1024).decode('utf-8')
# 创建一个线程,将接受到的数据字符串使用Popen命令
ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 输出信息,stdout属性读取并解码gbk
std_out = 'stdout :' + (ret.stdout.read()).decode('gbk')
# 错误信息,stdout属性读取并解码gbk
std_err = 'stderr :' + (ret.stderr.read()).decode('gbk')
# 打印命令信息执行信息
print(std_out)
# 打印命令执行错误
print(std_err)
# 将输出信息回显返回发送
client_socket.send(std_out.encode('utf-8'))
# 将错误信息回显返回发送
client_socket.send(std_err.encode('utf-8'))
# 关闭客户端
sk.close()
直接上我们的运行结果,线面两张图是第一次命令和第二次命令
图1
图2
如果是单条线程,每次都有信息不全,若是多条线程,同时执行多条命令之后,得到的结果很可能只有一部分,信息在极大的损失,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。
1、黏包成因
TCP协议中的数据传递
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。
大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,
这样会产生很多数据包碎片,增加丢包率,降低网络速度。
而TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一个成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是输入的是空内容(直接回车),也可以被发送,udp协议会封装消息头部发送过去。
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。
也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
那如何去定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
2、UDP不会发生黏包
UDP是无连接的,面向消息的,提供高效率服务。
不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
对于空消息:注意tcp是需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,但是而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
3、黏包原因
那时候需要考虑黏包问题
其中主要的不可控,在流传输中出现,UDP不会出现粘包,因为它有消息边界
1发送端需要等缓冲区满才发送出去,造成黏包
2接收方不及时接收缓冲区的包,造成多个包接收
具体点:
(1)发送方引起的黏包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。
(2)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致黏包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
黏包情况有两种,一种是黏在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。
不是所有的黏包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把黏连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。
在处理定长结构数据的黏包问题时,分包算法比较简单;在处理不定长结构数据的黏包问题时,分包算法就比较复杂。特别是黏在一起的包有不完整的包的粘包情况,由于一包数据内容被分在了两个连续的接收包中,处理起来难度较大。实际工程应用中应尽量避免出现黏包现象。
1如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现黏包问题(因为只有一种包结构,类似于http协议)。
关闭连接主要是要双方都发送close连接。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样黏包问题不用考虑到,因为大家都知道是发送一段字符。
2如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑黏包3如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
1)“hellogive me sth abour yourself”
2)“Don’tgive me sth abour yourself”
那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hellogive me sth abour yourselfDon’t give me sth abour yourself"这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。
会发生黏包的两种情况
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
黏包的解决方案
解决方案一
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
客户端代码
# coding=utf-8
# 文件名:spackag1_client.py
import socket, time
# 创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 链接服务器
res = server_socket.connect_ex(('127.0.0.1', 12345))
while True:
# 输入命令去除两边空格
msg = input('>>: ').strip()
# 如果命令空就跳过
if len(msg) == 0:
continue
# 命令为quit直接退出
if msg == 'quit':
break
# 编码并发送msg消息
server_socket.send(msg.encode('utf-8'))
# 接收消息,最大1024字节的消息,转码后转为int
length = int(server_socket.recv(1024).decode('utf-8'))
# 发送"recv_ready"消息
server_socket.send('recv_ready'.encode('utf-8'))
# 发送大小
send_size = 0
# 接收大小
recv_size = 0
# 空数据
data = b''
# 嵌套循环,如果接收的数据 小于 接收消息,最大1024字节的消息
while recv_size < length:
data += server_socket.recv(1024)
recv_size += len(data)
print(data.decode('gbk'))
服务器代码
# coding=utf-8
# 文件名:spackag1_server.py
import socket, subprocess
# 创建端口
ip_port = ('127.0.0.1', 12345)
#创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取套接字关联选项SOL_SOCKET操作其它层 SO_REUSEADDR允许重用本地地址和端口
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定端口
server_socket.bind(ip_port)
# 等待链接,监听
server_socket.listen(5)
while True:
# 返回一个不同的客户端的socket对象和属于客户端的套接字
conn, addr = server_socket.accept()
print('客户端', addr)
while True:
# 接收数据
msg = conn.recv(1024)
# 如果没有数据中断
if not msg:
break
# 创建新的线程准备运行命令
res = subprocess.Popen(msg.decode('utf-8'), shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
# stderr属性读取
err = res.stderr.read()
# 如果存在错误报异常,不存在,就将读取stdout属性给到变量ret
if err:
ret = err
else:
ret = res.stdout.read()
# 计算长度
data_length = len(ret)
# 转换成字符串并编码发送
conn.send(str(data_length).encode('utf-8'))
# 接收数据
data = conn.recv(1024).decode('utf-8')
# 如果数据为ecv_ready将数据尝试发送所有数据,成功则返回None,失败则抛出异常
if data == 'recv_ready':
conn.sendall(ret)
conn.close()
看看运行的结果
存在的问题
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
解决方案进阶
刚刚的方法,问题在于我们我们在发送
我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
4、Struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct
print(struct.pack('i',2147483647 ))
#这个是范围-2147483648 <= number <= 2147483647
# 输出:b'\xff\xff\xff\x7f'
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import struct
import binascii
import ctypes
values1 = (1, 'abc'.encode('utf-8'), 2.7)
values2 = ('defg'.encode('utf-8'),101)
s1 = struct.Struct('I3sf')
s2 = struct.Struct('4sI')
print(s1.size,s2.size)
# 12 8
prebuffer=ctypes.create_string_buffer(s1.size+s2.size)
print('Before : ',binascii.hexlify(prebuffer))
# Before : b'0000000000000000000000000000000000000000'
# t=binascii.hexlify('asdfaf'.encode('utf-8'))
# print(t)
s1.pack_into(prebuffer,0,*values1)
s2.pack_into(prebuffer,s1.size,*values2)
print('After pack',binascii.hexlify(prebuffer))
# After pack b'0100000061626300cdcc2c406465666765000000'
print(s1.unpack_from(prebuffer,0))
# (1, b'abc', 2.700000047683716)
print(s2.unpack_from(prebuffer,s1.size))
# (b'defg', 101)
s3=struct.Struct('ii')
s3.pack_into(prebuffer,0,123,123)
print('After pack',binascii.hexlify(prebuffer))
# After pack b'7b0000007b000000cdcc2c406465666765000000'
print(s3.unpack_from(prebuffer,0))
# (123, 123)
以上是关于Struct模块的一些详细使用,
那我们使用Struct解决我们的黏包问题,如何处理的尼
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构