码迷,mamicode.com
首页 > 其他好文 > 详细

粘包问题及解决方案

时间:2021-06-25 17:27:19      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:ast   多个   取数据   read   字段   产生   recv   字节   完成   

粘包问题及解决方案

一 什么是粘包问题

	前提:只有TCP会发生粘包现象,UDP永远不会粘包。
	粘包问题本质上就是接收方不知道消息的边界,不知道一次性该提取多少字节流用于解析消息,造成的消息解析错误问题。

二 为何么会有粘包问题

1 socket收发消息的原理之流式协议

技术图片

?

	发送端可以是1K1K的发送数据,而接收端的应用程序可以是两K两K地提取数据,也可以一次性全部提走,或者一次只提取几个字节地数据,也就是说,应用程序所看到的数据是一个整体,或者说是一个流(stream) ,一条消息有多少个字节对应用程序时不可见的,因此TCP协议是"""**面向流的协议**""",这也是容易出现粘包问题的原因。而UDP协议是面向消息的协议,每个UDP字段都是一条消息,应用程序必须以消息为单位提取数据,不能一次性提取任意字节的数据,这和TCP很不相同。TCP协议下,一条消息的发送,无论底层如何分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
	例如:基于TCP的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看来,根部不知道该文件的字节流是从何处开始,在何处结束。
	# **所谓的粘包问题,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节流的数据造成的。**
	此外,发送放引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要手机足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据整合成一个TCP段后一次性发出,这样接收方就收到了粘包数据。

2 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时才会清除缓冲区内容。数据是可靠的,但是会粘包。

3 两种发生粘包的情况

	1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据流很小,会河道一起,产生粘包) 。
	2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再接收的时候,还是从缓冲区拿上次一六的数据,产生粘包) 。

4 拆包发生的情况

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

5 补充知识两则

### 1.为何tcp是可靠传输,udp是不可靠传输
	tcp在传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发送往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的。
	而udp发送数据,对端是不会返回确认信息的,因此不可靠。

### 2.send(字节流) 和recv(1024) 及sendall
	recv里指定的1024意思是从缓存里一次拿出了1024个字节的数据。
	send的字节流是先存放入己端缓存,然后由协议控制将缓存内容法网对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。

三 如何解决粘包问题

1 解决方法之low版本

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

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

2 正确的解决方法

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

2.1 struct模块

# 	该模块可以把一个类型,如数字,转成固定长度的bytes。
struct.pack(fmt,v1,v2,…)
返回的是一个字符串,是参数按照fmt数据格式组合而成

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

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

2.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 structimport jsonfrom 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()

粘包问题及解决方案

标签:ast   多个   取数据   read   字段   产生   recv   字节   完成   

原文地址:https://www.cnblogs.com/chaochaofan/p/14930262.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!