首页 > 编程语言 >网络编程之粘包问题

网络编程之粘包问题

时间:2024-01-16 21:12:13浏览次数:32  
标签:字节 struct 编程 网络 粘包 socket import recv 客户端

粘包问题

只有TCP有粘包现象,UDP永远不会粘包

什么是粘包

存在于客户端接收数据时,不能一次性收取全部缓冲区中的数据.当下一次再有数据来时,缓冲区中剩余的数据会和新的数据'粘连'在一起.这就是粘包现象。

## 什么是粘包?
存在于TCP/IP协议中数据粘连在一起。
 
## socket中造成粘包现象的原因是什么?
客户端不能一次性接收完缓冲区里的所有数据,服务端接受客户端发送数据时,由于缓冲区没有存满,回先把数据'聚合'在客户端的缓冲区中,当客户端缓存区存满之后,在发给服务端。
 
## 哪些情况会发生粘包现象?
1. 客户端多次send少量数据, 会在客户端send输出缓冲区堆积, 当缓冲区堆积到一定程度 .会一次性把缓冲区的数据发给服务端造成粘包现象
2. 客户端接收服务端发送的数据,会在客户端的recv输入缓存堆积,由于每次接收数据有限,当第二次服务端把数据发送过来时数据会堆积在客户端的输入缓冲区.造成粘包
3. 发送频率过快:发送方发送数据的速度过快,接收方可能无法及时接收数据,从而导致多个数据包合并在一起。

## 解决粘包问题的方案有哪些?
1. 消息长度标识:发送方在每个数据包前添加消息长度信息,接收方根据消息长度来分割数据包,这样可以确保接收方正确地解析每个数据包。
2. 分隔符:发送方在数据包之间添加一种特殊的分隔符,接收方通过分隔符来切分数据包。
3. 应用层协议:通过在应用层定义协议,如固定长度的数据包格式、以JSON或XML格式打包数据等,可以避免粘包问题。

产生粘包现象:

### 客户端 
import  socket
 
client=socket.socket()
 
client.connect(('127.0.0.1',7777))
 
 
while 1:
    ui=input('请输入有命令:>>').strip()
    client.send(ui.encode('utf-8'))  # 向服务端发送指令
    
    ###***重点
      # 客户端 send完之后,  就会执行recv() 等待接收数据
      # 第一次 输入:dir指令        能够一次性接收完服务端返回的数据
      # 第二次 输入:ipconfig指令   不能一次性接收完服务端返回的数据,部分残余数据放在缓冲区中
      # 第三次 输入:dir指令时       由于上一次的返回的数据还没全部取完,新的指令数据还未到达缓冲区.     
      # 总结: 如果缓冲区承载的数据过大时,每次只接收1024字节,当新的指令执行完,返回回来的数据会'怼'到缓冲区,造成数据粘连现象. 这就是粘包
    
    
    ser_data=client.recv(1024)  # 接收到服务端发送的字节码  
 
    print(ser_data.decode('utf-8'))   # 解码 输出
 
client.close()
 
 
 
 
 
 
### 服务端
import socket
import subprocess # 执行 系统cmd指令
server=socket.socket()
 
server.bind(("127.0.0.1",7777)) # 绑定IP和端口
server.listen(5)
 
 
while 1:
 
    conn, addr=server.accept() # 等待接收连接
    print(conn,addr)
 
    while 1:
        try:
            cmd=conn.recv(1024)     # 接收客户端发送过来的数据
            cmd=cmd.decode('utf-8') # 将客户端发送过来的字节进行解码
 
            # 执行客户端发来的系统命令
            obj=subprocess.Popen(cmd,
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
            # 这是执行系统命令之后产生正确结果或错误结果,
            # 字节形式
            result=(obj.stdout.read()+obj.stderr.read()).decode('gbk')
 
            conn.send(result.encode('utf-8') ) # 向客户端发送结果,字节形式
            
        except Exception :
            break
    conn.close()
server.close()

而基于UDP的命令执行程序是不存在粘包问题的:

服务端

import socket
import subprocess

ip_port = ('127.0.0.1', 9003)
bufsize = 1024

udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server.bind(ip_port)

while True:
    # 收消息
    cmd, addr = udp_server.recvfrom(bufsize)
    print('用户命令----->', cmd,addr)

    # 逻辑处理
    res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdin=subprocess.PIPE,
                           stdout=subprocess.PIPE)
    stderr = res.stderr.read()
    stdout = res.stdout.read()

    # 发消息
    udp_server.sendto(stdout + stderr, addr)

udp_server.close()

客户端

from socket import *

import time

ip_port = ('127.0.0.1', 9003)
bufsize = 1024

udp_client = socket(AF_INET, SOCK_DGRAM)

while True:
    msg = input('>>: ').strip()
    if len(msg) == 0:
        continue

    udp_client.sendto(msg.encode('utf-8'), ip_port)
    data, addr = udp_client.recvfrom(bufsize)
    print(data.decode('utf-8'), end='')
  • 当我们启动udp服务端后,由udp客户端向服务端发送两条数据
  • 但是在udp服务端只接收到了一条数据
  • 这是因为 udp 是报式协议,传送数据过程中会将数据打包直接发走,不会对数据进行拼接操作(没有Nagle算法)

解决粘包问题

方式一(recv工作原理):

recv 工作原理:

  1. 能够接收来自socket缓冲区的字节数据;
  2. 当缓冲区没有数据可以读取时,recv会一直处于阻塞状态,知道缓冲区至少有一个字节数据可取,或者客户端关闭;
  3. 关闭远程端并读取所有数据后,再recv会返回字符串。

简易版解决粘包:

### 客户端
import  socket
 
client=socket.socket()
 
client.connect(('127.0.0.1',9989))
 
while 1:
    ui=input('请输入指令:>>>').strip()
    if len(ui)<0:continue
    if ui.upper()=='Q':break
 
    client.send(ui.encode('utf-8'))
 
    message_size=int(client.recv(1024).decode('utf-8'))
    client.send(b'recv_ready')
    recv_size=0
    data=b''
    while recv_size<message_size:
        data+=client.recv(1024)
        recv_size+=len(data)
 
    print(data.decode('utf-8'))
    
    
### 服务端
import socket
import subprocess
server=socket.socket()
 
server.bind(('127.0.0.1',9989))
 
server.listen(5)
 
while 1:
    conn,addr=server.accept()
    print(conn,addr)
 
    while 1:
        cmd=conn.recv(1024).decode('utf-8')
        obj=subprocess.Popen(cmd,
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        result=(obj.stdout.read()+obj.stderr.read()).decode('gbk').encode('utf-8')
        conn.send(str(len(result)).encode('utf-8'))
 
        data=conn.recv(1024).decode('utf-8')
        if data=='recv_ready':
            conn.sendall(result)
    conn.close()
server.close()

方式二(使用struct模块):

利用 struct 模块将传输过去的数据的总长度 打包 + 到头部进行发送

工作原理:

import struct
#  'i' 的取值范围  -2147483648 <= number <= 2147483647
#   i 模式 等长四位 , 将一个数字转化成等长度的bytes类型。
res=struct.pack('i',123456)
print(res,len(res))
 
###  通过unpack反解回来
data=struct.unpack('i',res)
print(data)
 
### 但是通过struct 处理不能处理太大
ret = struct.pack('q', 4323241232132324)
print(ret, type(ret), len(ret))  # 报错

lowB版解决粘包问题:

### 服务端
import socket
import subprocess
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
phone.bind(('127.0.0.1', 8080))
 
phone.listen(5)
 
while 1:
    conn, client_addr = phone.accept()
    print(client_addr)
    
    while 1:
        try:
            cmd = conn.recv(1024)
            ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            correct_msg = ret.stdout.read()
            error_msg = ret.stderr.read()
            
            # 1 制作固定报头
            total_size = len(correct_msg) + len(error_msg)
            header = struct.pack('i', total_size)
            
            # 2 发送报头
            conn.send(header)
            
            # 发送真实数据:
            conn.send(correct_msg)
            conn.send(error_msg)
        except ConnectionResetError:
            break
 
conn.close()
phone.close()
 
 
# 但是low版本有问题:
# 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
# 2,通过struct模块直接数据处理,不能处理太大。


### 客户端
import socket
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 
phone.connect(('127.0.0.1',8080))
 
 
while 1:
    cmd = input('>>>').strip()
    if not cmd: continue
    phone.send(cmd.encode('utf-8'))
    
    # 1,接收固定报头
    header = phone.recv(4)
    
    # 2,解析报头
    print(struct.unpack('i', header)) # 这是一个元组 (xxx,)
    total_size = struct.unpack('i', header)[0] # 取第一个元素
    
    # 3,根据报头信息,接收真实数据
    recv_size = 0
    res = b''
    
    while recv_size < total_size:
        
        recv_data = phone.recv(1024)
        res += recv_data
        recv_size += len(recv_data)
 
    print(res.decode('gbk'))
 
phone.close()

方式三(再加上json):

工作原理:

### 步骤解释
# 报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节
 
#发送时:
    1.先发报头长度
    2.再编码报头内容然后发送
    3.最后发真实内容
 
 
#接收时:
    1.先手报头长度,用struct取出来
    2.根据取出的长度收取报头内容,然后解码,反序列化
    3.从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

懂哥版解决粘包问题:

### 客户端
import socket
import struct
import json
 
client = socket.socket()    # 实例化 socket对象
 
client.connect(('127.0.0.1', 8898)) # socket 连接IP和端口
 
while 1:
        ui = input('请输入指令:>>').strip()
        #  1. 发送 用户输入 指令
        client.send(ui.encode('utf-8'))
 
        #2.  接收 struct 封装的头 字节形式 ,  接收4个字节 head
        head = client.recv(4)
        # 3. struct反解 ,获得自定义报头的长度
        dic_length = struct.unpack('i', head)[0]
 
        #4.  接收自定义字典报头内容 字节形式
        head_dic = client.recv(int(dic_length))
 
        #5.  反序列化自定义字典,先解码在反序列化
        dic = json.loads(head_dic.decode('utf-8'))
        #6. 得到 真实内容的长度
        content_length = dic['size']
 
        # 7 设置一个字节变量 用于接收真实数据
        content = b''
        # 8 设置一个客户端接收长度
        recv_size = 0
 
        # 9 当客户端接收长度 小于 源数据长度,一直接收
        while recv_size < content_length:
            # 累加 真实数据,以字节形式
            content += client.recv(1024)
            # 累加 客户端接收的长度
            recv_size += len(content)
            
        # 接收完毕,解码内容
        print(content.decode('utf-8'))
client.close()



### 服务端
import socket
import struct
import json
import time
import subprocess
 
server = socket.socket()            # 实例化 socket对象
 
server.bind(('127.0.0.1', 8898))    # 绑定ip和端口
 
server.listen(5)                    # 邦迪监听连接
 
while 1:
    conn, addr = server.accept()    # 等待连接
    print(conn, addr)               #  打印连接,和连接ip
 
    while 1:
        try:
            # 1. 接收客户端发送的 指令 ,字节形式
            cmd = conn.recv(1024)
            # 2. 执行指令,得到结果 , 指令是字符形式(需要解码)
            obj = subprocess.Popen(cmd.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
 
            #3.得到结果字节 ,转换成utf-8的字节格式
            result = (obj.stdout.read() + obj.stderr.read()).decode('gbk').encode('utf-8')
 
            #4. 获得真实数据的字节长度
            total_res_bytes = len(result)
 
            #5. 自定制字典报头
            head_dic = {
                'time': time.localtime(time.time()),
                'size': total_res_bytes, # 字节长度
                'MD5': 'XXXXX',
                'file_name': '婚前视频',
            }
 
            # 6. 序列化字典 ,并将其转换成字节形式
            head_dic_bytes = json.dumps(head_dic).encode('utf-8')
 
            #7.  使用 struct 封装报头字典head_dic_bytes ,固定长度(4个字节)
            #   封装成字节,发送给客户端,还是按照字节取出来.
            head = struct.pack('i', len(head_dic_bytes))
 
            # 8 , 先将固定头发送给客户端
            conn.send(head)
            # 9 . 再将自定制报头发送给客户端
            conn.send(head_dic_bytes)
            # 10. 最后将真实结果发送给客户端
            conn.send(result)
            ### 这里就是拼接字节
                # 格式: 固定头 + 自定义报头 +真实数据
        except Exception:
            break
    conn.close()
server.close()

【补充】struct模块

  • struct.pack()是Python内置模块struct中的一个函数
    • 它的作用是将指定的数据按照指定的格式进行打包,并将打包后的结果转换成一个字节序列(byte string)
    • 可以用于在网络上传输或者储存于文件中。
  • struct.pack(fmt, v1, v2, ...)
    • 其中,fmt为格式字符串,指定了需要打包的数据的格式,后面的v1,v2,...则是需要打包的数据。
    • 这些数据会按照fmt的格式被编码成二进制的字节串,并返回这个字节串。
  • fmt的常用格式符如下:
    • x --- 填充字节
    • c --- char类型,占1字节
    • b --- signed char类型,占1字节
    • B --- unsigned char类型,占1字节
    • h --- short类型,占2字节
    • H --- unsigned short类型,占2字节
    • i --- int类型,占4字节
    • I --- unsigned int类型,占4字节
    • l --- long类型,占4字节(32位机器上)或者8字节(64位机器上)
    • L --- unsigned long类型,占4字节(32位机器上)或者8字节(64位机器上)
    • q --- long long类型,占8字节
    • Q --- unsigned long long类型,占8字节
    • f --- float类型,占4字节
    • d --- double类型,占8字节
    • s --- char[]类型,占指定字节个数,需要用数字指定长度
    • p --- char[]类型,跟s一样,但通常用来表示字符串
    • ? --- bool类型,占1字节
  • 具体的格式化规则可以在Python文档中查看(链接)。

img

import json
import struct

# 为避免粘包,必须自定制报头
# 1T数据,文件路径和md5值
header = {'file_size': 1073741824000, 'file_name': '/a/b/c/d/e/a.txt',
          'md5': '8f6fbf8347faa4924a76856701edb0f3'}

# 为了该报头能传送,需要序列化并且转为bytes
# 序列化并转成bytes,用于传输
head_bytes = bytes(json.dumps(header), encoding='utf-8')

# 为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
# 这4个字节里只包含了一个数字,该数字是报头的长度
head_len_bytes = struct.pack('i', len(head_bytes))

print(f"这是原本的数据 :>>>> {header}")
print(f"这是json序列化后的数据 :>>>> {head_bytes}")
print(f"这是压缩后的数据 :>>>> {head_len_bytes}")

# 这是原本的数据 :>>>> {'file_size': 1073741824000, 'file_name': '/a/b/c/d/e/a.txt', 'md5': '8f6fbf8347faa4924a76856701edb0f3'}
# 这是json序列化后的数据 :>>>> b'{"file_size": 1073741824000, "file_name": "/a/b/c/d/e/a.txt", "md5": "8f6fbf8347faa4924a76856701edb0f3"}'
# 这是压缩后的数据 :>>>> b'h\x00\x00\x00'

标签:字节,struct,编程,网络,粘包,socket,import,recv,客户端
From: https://www.cnblogs.com/xiao01/p/17968531

相关文章

  • 网络编程之网络架构及其趋势
    一、网络结构模型C/S和B/S都是互联网中常见的网络结构模型。引言刚开始的时候用户去取数据,直接就去主机拿,从这里开始就分出了客户端和服务端。客户端:用户安装的软件;服务端:统一管理数据库的主机中的软件就叫做服务端,再后来服务端不只是管理数据,外加处理业务逻辑。1.1什么......
  • 粘包问题
    粘包问题(1)粘包问题介绍粘包问题是在计算机网络中,特别是在使用面向流的传输协议(例如TCP)时经常遇到的一种情况。它主要涉及到数据在传输过程中的组织和接收问题。当使用TCP协议进行数据传输时,发送方往往会将要传输的数据切分成小的数据块,并通过网络发送给接收方。然而,底层的......
  • 网络编程TCP UDP
    网络编程(1)什么是网络编程网络编程是指通过编程语言在计算机之间建立通信的一种方式。它是在互联网上进行数据传输的关键组成部分,使计算机能够相互通信、交换信息和共享资源。网络编程涉及许多不同的技术和协议,包括TCP/IP(传输控制协议/因特网协议),HTTP(超文本传输协议),FTP(文件传......
  • springBoot通过AOP(面向切面编程)实现自动保存请求日志
    1.定义注解importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;@Target(ElementType.METHOD)//指定该注解只能应用于方法上@Retention(RetentionPolicy.......
  • 网络连通性测试 【Connectivity】
    CFD简介CFD(ConnectivityFaultDetection,连通错误检测)是一种二层网络中的端到端OAM(Operation,Administration,andMaintenance,操作、管理和维护)技术,主要用于在二层网络中检测链路连通性,以及在故障发生时进行定位。适用的二层网络包括基于VLAN的以太网网络和基于MPLS的二层V**。......
  • 【论文笔记#2】Farseg++:用于高空间分辨率遥感图像地理空间对象分割的前景感知关系网络
    论文来源IEEETransactionsonPatternAnalysisandMachineIntelligence作者ZhuoZheng;YanfeiZhong;JunjueWang等发表年代2023使用方法多分支金字塔编码、前景-场景关系、前景感知解码、前景感知优化期刊层次CCFA;计算机科学1区;IF23.6原文链接......
  • 网络编程
    【一】CS架构与BS架构C/S和B/S都是互联网中常见的网络结构模型。【1】什么是C/S模型C是英文单词“Client”的首字母,即客户端的意思C/S就是“Client/Server”的缩写,即“客户端/服务器”模式。例如:拼多多APP、PC上的有道云笔记等等【2】什么是B/S模型B是英文单词“B......
  • 【python网络编程相关】 ----操作系统相关了解
    title:【python网络编程相关】----操作系统相关了解date:2024-01-1615:54:06updated:2024-01-1616:20:00description:【python网络编程相关】----操作系统相关了解cover: https://www.cnblogs.com/YZL2333/p/10444200.htmlhttps://home.cnblogs.com/u/......
  • 网络库
    网络库在学习爬虫过程中,我们需要了解三种基本的网络库。urlliburllib3requests一、urllib网络库urllib包含四个模块1、request:最基本的HTTP请求模块,可以用来发送HTTP请求,并接受服务端的响应数据。2、error:异常处理模块。3、parse:工具模块,提供了很多处理URL的API,如拆分......
  • 粘包
    粘包【一】什么是粘包须知:只有TCP有粘包现象,UDP永远不会粘包【二】什么是粘包问题客户端发送需要执行的代码服务端接收到客户端传过来的代码服务端调用方法执行代码并拿到执行后的结果服务端将执行后的结果进行返回客户端接收到服务端返回的结果并做打印输出【1】......