粘包现象

粘包现象

粘包现象产生的原因

tcp中有一个negal算法,用途是这样的:通信两端有很多小的数据包要发送,虽然传送的数据很少,但是流程一点没少,也需要tcp的各种确认,校验。这样小的数据包如果很多,会造成网络资源很大的浪费,negal算法做了这样一件事,当来了一个很小的数据包,我不急于发送这个包,而是等来了更多的包,将这些小包组合成大包之后一并发送,不就提高了网络传输的效率的嘛。这个想法收到了很好的效果,但是我们想一下,如果是分属于两个不同页面的包,被合并在了一起,这就是粘包问题。
粘包问题只存在于TCP中,Not UDP
在这里插入图片描述

看上图,举个例子发送端可以是50K50K地发送数据,而接收端的应用程序可以100k,100k取走数据。应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

总结

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。

解决粘包问题的简单版本的服务端和客户端
服务端
import socket
import subprocess
import struct
import json

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

print('开始连接客户端')
while True:
    conn,client_adr=phone.accept()
    while True:
        try:
            #1.收命令
            cmd = conn.recv(1024)
            
            #2.拿命令的结果,并返回给客户端
            res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            stdout=res.stdout.read().decode('gbk')
            stderr=res.stderr.read().decode('gbk')

            #3.为了解决粘包现象,需要对传输的过程优化

            #1首先制作报头,报头中包含传输的文件信息
            header_dic={'filename':'a.txt',
                        'md5':'1236',
                        'total_size':len(stdout)+len(stderr)}
                        
            #字典不能直接使用send进行发送,因此导入json模块,
            # 对字典进行序列化,转化为字符串形式
            header_json=json.dumps(header_dic) 
            
            #header_json为字符串形式,也不能发送,先对其转换为bytes形式
            header_bytes=header_json.encode('utf-8')
            
            #不能直接发报头,由于不知道报头长度,也可能发生粘包现象
            #所以先发报头的长度,再发送报头,最后报文内容

            #先发报头长度
            
            conn.send(struct.pack('i',len(header_bytes)))
            
            #再发报头
            conn.send(header_bytes)
            
            #最后发报文数据
            conn.send(stdout.encode('utf-8'))
            conn.send(stderr.encode('utf-8'))
            
        except ConnectionResetError:
            break

    conn.close()
phone.close()
客户端
import socket
import struct
import json

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

while True :
    msg=input('请输入传送内容:').strip()
    if not msg:continue
    phone.send(msg.encode('utf-8'))
    
    #1.先收报头长度,反解报头长度
    l=phone.recv(4)
    header_len=struct.unpack('i',l)[0]#反解是元组形式,第一个数据为数字
    
    #2.收报头内容
    header_bytes=phone.recv(header_len)
    
    #3.对报头内容解析,获取所需要的数据
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    print(header_dic)
   
    #获取报文长度
    total_size=header_dic['total_size']
   
    #4.接受报文信息
    recv_size=0
    recv_date=b'' #接受的是内容是bytes
    while recv_size<total_size:
        res=phone.recv(1024)
        recv_size+=len(res)
        recv_date+=res

    print(recv_date.decode('utf-8'))


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值