博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
网络编程-----黏包问题
阅读量:6243 次
发布时间:2019-06-22

本文共 6977 字,大约阅读时间需要 23 分钟。

一,黏包现象

我们通过一段简单程序来看看黏包现象:

import socketsk=socket.socket()sk.bind(('127.0.0.1',8090))sk.listen()conn,addr=sk.accept()while True:    cmd=input(">>>")    if cmd=='q':        conn.send(b'q')        break    conn.send(cmd.encode('gbk'))    res=conn.recv(1024).decode('gbk')    print(res)conn.close()sk.close()
server端
imsocketimport subprocesssk=socket.socket()sk.connect(('127.0.0.1',8090))while True:    cmd=sk.recv(1024).decode('gbk')    if cmd=='q':        break    res=subprocess.Popen(cmd,shell=True,                         stdout=subprocess.PIPE,                         stderr=subprocess.PIPE)    sk.send(res.stdout.read())    sk.send(res.stderr.read())sk.close()
client端

同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令时又会接到之前执行的另外一部分结果,这种就是黏包。

只有tcp有黏包现象,udp不会黏包。

二,黏包成因

tcp协议中的数据传递

tcp协议的拆包机制

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。 MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。 如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。

面向流的通信特点和Nagle(优化)算法

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
View Code
基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始, 在何处结束此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少, 通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

三,会发生黏包的两种情况

1,发送方的缓存机制:发送端需要等缓冲区满才发送出去,造成黏包(发送数据时间间隔很短,数据很小,会合到一起,产生黏包)

例:连续send两次且数据很小

import socketsk = socket.socket()sk.connect(('127.0.0.1',8090))sk.send(b'hello')sk.send(b'egg')sk.close()
发送端
import socketsk = socket.socket()sk.bind(('127.0.0.1',8090))sk.listen()conn,addr = sk.accept()ret2 = conn.recv(10)print(ret2)conn.close()sk.close()
接收端

2,接收方的缓存机制:接收不及时接收缓冲区的包,造成多个包接收(客户端发送一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿走上次剩余的数据,产生黏包。)

例:连续recv两次且第一个recv接收的数据小

import socketsk = socket.socket()sk.connect(('127.0.0.1',8090))sk.send(b'hello,egg')sk.close()
发送端
import socketsk = socket.socket()sk.bind(('127.0.0.1',8090))sk.listen()conn,addr = sk.accept()ret = conn.recv(2)ret2 = conn.recv(10)print(ret)print(ret2)conn.close()sk.close()
接收端

总结:

1,表面上看,黏包问题主要是因为发送端和接收端的缓存机制、tcp协议面向流通信的特点。

2,实际上,主要还是因为接收端不知道消息之间的界限,不知道一次性提取多少次字节的数据所造成的。

四,黏包的解决

1,将要发送的字节流总大小发给接收端,然后接收端来一个循环接收完所有数据。

import socketsk = socket.socket()sk.bind(('127.0.0.1',8080))sk.listen()conn,addr = sk.accept()while True:    cmd = input('>>>')    if cmd == 'q':        conn.send(b'q')        break    conn.send(cmd.encode('gbk'))    num = conn.recv(1024).decode('utf-8')  # 2048    conn.send(b'ok')    res = conn.recv(int(num)).decode('gbk')    print(res)conn.close()sk.close()
服务端
import socketimport subprocesssk = socket.socket()sk.connect(('127.0.0.1',8080))while True:    cmd = sk.recv(1024).decode('gbk')    if cmd == 'q':        break    res = subprocess.Popen(cmd,shell=True,                     stdout=subprocess.PIPE,                     stderr=subprocess.PIPE)    std_out = res.stdout.read()    std_err = res.stderr.read()    sk.send(str(len(std_out)+len(std_err)).encode('utf-8'))   #2000    sk.recv(1024)   # ok    sk.send(std_out)    sk.send(std_err)sk.close()
客户端

问题:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网路延迟带来的性能损耗。send sendto在超过一定范围的时候会报错

2,进阶方案

struck模块,该模块可以把一个类型,例,数字,转换成固定的长度bytes类型

import structs=struct.pack('i',23)    # i ,整数类型,不管数字有多大 ,都转换成4个字节长度print(s)                         #b'\x84\x1a\x00\x00's1=struct.unpack('i',s) #取出数字print(s1)            #元组(23,)
import json,struct#假设通过客户端上传1T:1073741824000的文件a.txt#为避免粘包,必须自定制报头header={
'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值#为了该报头能传送,需要序列化并且转为byteshead_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度#客户端开始发送conn.send(head_len_bytes) #先发报头的长度,4个bytesconn.send(head_bytes) #再发报头的字节格式conn.sendall(文件内容) #然后发真实内容的字节格式#服务端开始接收head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式header=json.loads(json.dumps(header)) #提取报头#最后根据报头的内容提取真实的数据,比如real_data_len=s.recv(header['file_size'])s.recv(real_data_len)
View Code

使用struck解决黏包:

把报头做成字典,字典里要包含将要发送真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。

发送时:先发报头长度,再编码报头内容然后发送,最后发真实内容。

接收时:先收报头长度,用struct取出来,根据取出的长度收取报头内容,解码,反序列化,从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

# 发送端import osimport jsonimport structimport socketsk = socket.socket()sk.connect(('127.0.0.1',8090))buffer = 1024# 发送文件head = {
'filepath':r'F:\Program Files\feiq\Recv Files\day23', 'filename':r'04 python fullstack s9day23 组合2.mp4', 'filesize':None}file_path = os.path.join(head['filepath'],head['filename'])filesize = os.path.getsize(file_path)head['filesize'] = filesizejson_head = json.dumps(head) # 字典转成了字符串bytes_head = json_head.encode('utf-8') # 字符串转bytes# 计算head的长度head_len = len(bytes_head) # 报头的长度pack_len = struct.pack('i',head_len)sk.send(pack_len) # 先发报头的长度sk.send(bytes_head) # 再发送bytes类型的报头with open(file_path,'rb') as f: while filesize: print(filesize) if filesize >= buffer: content = f.read(buffer) # 每次读出来的内容 sk.send(content) filesize -= buffer else: content = f.read(filesize) sk.send(content) breaksk.close()
客户端
import jsonimport socketimport structsk = socket.socket()sk.bind(('127.0.0.1',8090))sk.listen()buffer = 1024conn,addr = sk.accept()# 接收head_len = conn.recv(4)head_len = struct.unpack('i',head_len)[0]json_head = conn.recv(head_len).decode('utf-8')head = json.loads(json_head)filesize = head['filesize']with open(head['filename'],'wb') as f:    while filesize:        print(filesize)        if filesize >= buffer:            content = conn.recv(buffer)            f.write(content)            filesize -= buffer        else:            content = conn.recv(filesize)            f.write(content)            breakconn.close()sk.close()
服务端

五,socket的更多方法

服务端套接字函数s.bind()    绑定(主机,端口号)到套接字s.listen()  开始TCP监听s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来客户端套接字函数s.connect()     主动初始化TCP服务器连接s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常公共用途的套接字函数s.recv()            接收TCP数据s.send()            发送TCP数据s.sendall()         发送TCP数据s.recvfrom()        接收UDP数据s.sendto()          发送UDP数据s.getpeername()     连接到当前套接字的远端的地址s.getsockname()     当前套接字的地址s.getsockopt()      返回指定套接字的参数s.setsockopt()      设置指定套接字的参数s.close()           关闭套接字面向锁的套接字方法s.setblocking()     设置套接字的阻塞与非阻塞模式s.settimeout()      设置阻塞套接字操作的超时时间s.gettimeout()      得到阻塞套接字操作的超时时间面向文件的套接字的函数s.fileno()          套接字的文件描述符s.makefile()        创建一个与该套接字相关的文件
View Code

 

转载于:https://www.cnblogs.com/glf1160/p/8378850.html

你可能感兴趣的文章
Laravel 教程 - 实战 iBrand 开源电商 API 系统
查看>>
vue-cli的坑,loader重复的锅 Invalid CSS after "...load the styles"
查看>>
手写Spring之IOC基于xml动态创建对象
查看>>
聊聊reactive streams的tranform操作
查看>>
箭头函数与this
查看>>
Angular4学习笔记之DOM属性绑定
查看>>
java构造list,合并重复的数组
查看>>
紫书第四章例4-4信息解码Message Decoding (处理二进制字符的一种典型例题)
查看>>
一些杂感杂想(三)打理业余时间的一点个人心得
查看>>
Apache Kylin安装部署
查看>>
JavaScript的async 的学习
查看>>
SpringCloud(第 021 篇)Zuul 的过滤器 ZuulFilter 的使用
查看>>
JavaScript笔记——闭包
查看>>
gRPC 初探
查看>>
SpringBoot非官方教程 | 第六篇:SpringBoot整合mybatis
查看>>
dataguard备库出现GAP修复
查看>>
OOD、DIP、IOC、DI、依赖注入容器(即 控制反转容器,IOC Container)
查看>>
Linux常用命令
查看>>
Grub4Dos 手动引导指令
查看>>
C# 有道API翻译 查询单词详细信息
查看>>