032.粘包问题及解决方案

一、什么是粘包问题

​ 前提:只有TCP会发生粘包现象,UDP永远不会粘包。

​ 粘包问题本质上就是接收方不知道消息的边界,不知道一次性该提取多少字节流用于解析消息,造成的消息解析错误问题。

二、为何么会有粘包问题

(一)socket收发消息的原理之流式协议

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bVt9t6Jq-1597461624663)(D:\Onedrive\文档\01学习资料\全栈15博客笔记\python学习博客及作业\day032.粘包问题及解决方案\001收发消息原理.png)]

​ 发送端可以是1K1K的发送数据,而接收端的应用程序可以是两K两K地提取数据,也可以一次性全部提走,或者一次只提取几个字节地数据,也就是说,应用程序所看到的数据是一个整体,或者说是一个流(stream),一条消息有多少个字节对应用程序时不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP协议是面向消息的协议,每个UDP字段都是一条消息,应用程序必须以消息为单位提取数据,不能一次性提取任意字节的数据,这和TCP很不相同。TCP协议下,一条消息的发送,无论底层如何分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

​ 例如:基于TCP的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看来,根部不知道该文件的字节流是从何处开始,在何处结束。

所谓的粘包问题,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节流的数据造成的。

​ 此外,发送放引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要手机足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据整合成一个TCP段后一次性发出,这样接收方就受到了粘包数据。

(二)TCP与UDP的消息边界

1.TCP(transport control protocol,传输控制协议)下的消息边界

​ 该协议是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务端)都要有一一成对的socket,因此,发送端为了将多个法网接收端的包,更有效的发到对方,使用了优化算法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。

2.UDP(user datagram protocal,用户数据报协议)下的消息边界

​ 该协议是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中都有消息头(消息来源地址,端口等消息),这样,对于接收端来说,就容易进行区分处理了。即面向消息的通信是有消息保护边界的。

3.总结

​ 由于TCP协议是基于数据流的,于是收发消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即使是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。

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

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

(三)两种发生粘包的情况

​ 1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据流很小,会河道一起,产生粘包)。

​ 2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再接收的时候,还是从缓冲区拿上次一六的数据,产生粘包)。

(四)拆包发生的情况

​ 当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

(五)补充知识两则

1.为何tcp是可靠传输,udp是不可靠传输

​ tcp在传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发送往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的。

​ 而udp发送数据,对端是不会返回确认信息的,因此不可靠。

2.send(字节流) 和recv(1024) 及sendall

​ recv里指定的1024意思是从缓存里一次拿出了1024个字节的数据。

​ send的字节流是先存放入己端缓存,然后由协议控制将缓存内容法网对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。

三、如何解决粘包问题

(一)解决方法之low版本

​ 发送字节之前,先发送一段该字节流的长度,然后接收字节流长度的数据。

​ 缺陷:由于程序的运行速度远快于网络传输速度,会因网络延迟造成性能损耗。

(二)正确的解决方法

​ 为字节流加入自定义固定长度报头,报头中包含字节流长度,然后send一次到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。使用struct模块,可以辅助实现此功能。

1.struct模块

​ 该模块可以把一个类型,如数字,转成固定长度的bytes。

struct.pack(fmt,v1,v2,)
返回的是一个字符串,是参数按照fmt数据格式组合而成


struct.unpack(fmt,string)
按照给定数据格式解开(通常都是由struct.pack进行打包)数据,返回值是一个tuple

在这里插入图片描述

2.远程执行命令程序解决粘包问题

​ 服务端:

##——————————————————————————————————————server端远程执行指令解决粘包问题

import subprocess
import struct
from socket import *

server = socket(AF_INET, SOCK_STREAM)

server.setsockopt(SOL_SOCKET, SO_REUSEADDR)

server.bind(('127.0.0.1', 8080))

server.listen(5)

while True:
    conn, client_addr = server.accept()

    while True:
        try:
            cmd = conn.recv(1024)
            obj = subprocess.Popen(cmd.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            total_size = len(stdout) + len(stderr)

            # 先发送数据大小
            conn.send(struct.pack('i', total_size))

            # 再发送真正的数据
            conn.send(stdout)
            conn.send(stderr)

        except Exception:
            break
    conn.close()

server.close()

​ 客户端:

##——————————————————————————————————————client端远程执行指令解决粘包问题

import struct
from socket import *

client = socket(AF_INET, SOCK_STREAM)

client.connect(('127.0.0.1', 8080))  # connect__连接

while True:
    cmd = input('>>>:').strip()
    if len(cmd) == 0:  # 禁止发送空,规避可能的粘包问题
        continue
    client.send(cmd.encode('utf-8'))

    # 先接受数据长度(接收固定字节的数据)
    n = 0
    header = b''
    while n < 4:
        data = client.recv(1)
        header += data
        n += 1

    total_size = struct.unpack('i', header)[0]  # unpack出是一个元组,取第一个数据

    # 收真正的数据
    recv_size = 0
    res = b''
    while recv_size < total_size:
        data = client.recv(1024)
        res += data
        recv_size += len(data)

    print(res.decode('gbk'))  # windows下的系统命令,要以gbkg格式解码

client.close()

3.定制复杂的报头版本

客户端:

##——————————————————————————————————————server端————定制复杂的报头
import os
import struct
import json
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080))

server.listen(5)
while True:
    conn, client_addr = server.accept()
    print(conn)
    print(client_addr)

    while True:
        try:
            msg = conn.recv(1024).decode('utf-8')
            cmd, file_path = msg.split()
            print(cmd, type(cmd), file_path)
            if cmd == 'get':
                # 一、制作报头
                print(os.path.getsize(file_path))
                print(os.path.basename(file_path))
                header_dic = {
                    'total_size': os.path.getsize(file_path),
                    'filename': os.path.basename(file_path),
                    'md5': '123123123123'}
                print(header_dic)
                header_json = json.dumps(header_dic)  # 报头字典使用json序列化
                header_json_bytes = header_json.encode('utf-8')  # 序列化的字符串,转为bytes类型

                # 二、发送数据
                # 1、先发送报头长度
                header_size = len(header_json_bytes)
                conn.send(struct.pack('i', header_size))
                # 2、再发送报头
                conn.send(header_json_bytes)
                # 3、最后发送真是的数据
                with open(r'%s' % file_path, mode='rb') as f:
                    for line in f:
                        conn.send(line)

        except Exception:
            break

    conn.close()

server.close()

服务端:

##——————————————————————————————————————client端----定制复杂的报头

import struct
import json
from socket import *

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

while True:
    cmd = input('>>>:').strip()  # get 文件路径
    if len(cmd) == 0:
        continue
    client.send(cmd.encode('utf-8'))

    # 1.先接收报头的长度
    res = client.recv(4)  # 我们已知报头长度定长为4
    header_size = struct.unpack('i', res)[0]

    # 2.再接收报头
    header_json_bytes = client.recv(header_size)
    header_json = header_json_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    print(header_dic)

    # 3.最后接收真实的数据
    total_size = header_dic['total_size']
    filename = header_dic['filename']
    recv_size = 0
    with open(r'D:\%s' % filename, mode='wb') as f:
        while recv_size < total_size:
            data = client.recv(1024)
            f.write(data)
            recv_size += len(data)

client.close()
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值