粘包现象
Posted Sober--
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了粘包现象相关的知识,希望对你有一定的参考价值。
一、基于udp的套接字
udp是无链接的,先启动哪一端都不会报错
udp服务端:
ss = socket() #创建一个服务器的套接字 ss.bind() #绑定服务器套接字 while True : #服务器无限循环 cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送) ss.close() # 关闭服务器套接字
udp客户端:
cs = socket() # 创建客户套接字 while True : # 通讯循环 cs.sendto()/cs.recvfrom() # 对话(发送/接收) cs.close() # 关闭客户套接字
1、udp套接字简单实例
服务端:
from socket import * udp_ss=socket(AF_INET,SOCK_DGRAM) udp_ss.bind((\'127.0.0.1\',8080)) while True: msg,addr=udp_ss.recvfrom(1024) print(msg,addr) udp_ss.sendto(msg.upper(),addr)
客户端:
from socket import * udp_cs=socket(AF_INET,SOCK_DGRAM) while True: msg=input(\'>>: \').strip() if not msg:continue udp_cs.sendto(msg.encode(\'utf-8\'),(\'127.0.0.1\',8080)) msg,addr=udp_cs.recvfrom(1024) print(msg.decode(\'utf-8\'),addr)
2、模拟聊天(由于udp无连接,所以可以同时多个客户端去跟服务端通信)
服务端:
from socket import * udp_ss=socket(AF_INET,SOCK_DGRAM) udp_ss.bind((\'127.0.0.1\',8081)) while True: msg,addr=udp_ss.recvfrom(1024) print(\'来自[%s]的一条消息:%s\' %(addr,msg.decode(\'utf-8\'))) msg_b=input(\'回复消息: \').strip() udp_ss.sendto(msg_b.encode(\'utf-8\'),addr)
客户端1:
from socket import * udp_cs = socket(AF_INET,SOCK_DGRAM) while True : msg = input(\'请输入消息,回车发送: \').strip() if msg == \'quit\' : break if not msg : continue udp_cs.sendto(msg.encode(\'utf-8\'),(\'127.0.0.1\',8081)) back_msg,addr = udp_cs.recvfrom(1024) print(\'来自[%s]的一条消息:%s\' %(addr,back_msg.decode(\'utf-8\'))) udp_cs.close()
客户端2:
from socket import * udp_cs = socket(AF_INET,SOCK_DGRAM) while True : msg = input(\'请输入消息,回车发送: \').strip() if msg == \'quit\' : break if not msg : continue udp_cs.sendto(msg.encode(\'utf-8\'),(\'127.0.0.1\',8081)) back_msg,addr = udp_cs.recvfrom(1024) print(\'来自[%s]的一条消息:%s\' %(addr,back_msg.decode(\'utf-8\'))) udp_cs.close()
二、粘包现象
先做粘包现象:
服务端:
from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) phone.bind((\'127.0.0.1\',8080)) phone.listen(5) conn,client_addr=phone.accept() data1=conn.recv(1024) print(\'data1: \',data1) data2=conn.recv(1024) print(\'data2:\',data2)
客户端:
from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.connect((\'127.0.0.1\',8080)) phone.send(\'hello\'.encode(\'utf-8\')) phone.send(\'world\'.encode(\'utf-8\'))
我们再将上个随笔里的ssh例子拿出来(先执行 ipconfig /all 再执行 dir 看结果)
客户端:
from socket import * import subprocess cs=socket(AF_INET,SOCK_STREAM) cs.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) cs.bind((\'127.0.0.1\',8082)) cs.listen(5) print(\'starting...\') while True: conn,addr=cs.accept() print(\'-------->\',conn,addr) while True: try: cmd=conn.recv(1024) res = subprocess.Popen(cmd.decode(\'utf-8\'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=res.stdout.read() stderr=res.stderr.read() #发送命令的结果 conn.send(stdout+stderr) except Exception: break conn.close() #挂电话 cs.close() #关机
服务端:
from socket import * ss=socket(AF_INET,SOCK_STREAM) #买手机 ss.connect((\'127.0.0.1\',8082)) #绑定手机卡 #发,收消息 while True: cmd=input(\'>>: \').strip() if not cmd:continue ss.send(cmd.encode(\'utf-8\')) cmd_res=ss.recv(1024) print(cmd_res.decode(\'gbk\')) ss.close()
注意:
subprocess模块的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码
三、粘包
注意:只有TCP有粘包现象,UDP永远不会粘包,首先需要掌握一个socket收发消息的原理
应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
两种情况会粘包:
1、发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
2、接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
拆包的发生情况:
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
四、解决粘包方法
粘包现象中第一个现象解决:
解决一:(需要知道每次发过来的数据大小 不现实)
from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) phone.bind((\'127.0.0.1\',8080)) phone.listen(5) conn,client_addr=phone.accept() data1=conn.recv(10) print(\'data1: \',data1) data2=conn.recv(4) print(\'data2:\',data2)
from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.connect((\'127.0.0.1\',8080)) phone.send(\'helloworld\'.encode(\'utf-8\')) phone.send(\'egon\'.encode(\'utf-8\'))
解决二:
ssh例子问题解决:
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) phone.bind((\'127.0.0.1\',8080)) phone.listen(5) conn,client_addr=phone.accept() data1=conn.recv(1024) print(\'data1: \',data1) data2=conn.recv(1024) print(\'data2:\',data2)
from socket import * import time phone=socket(AF_INET,SOCK_STREAM) phone.connect((\'127.0.0.1\',8080)) phone.send(\'hello\'.encode(\'utf-8\')) time.sleep(5) phone.send(\'world\'.encode(\'utf-8\'))
五、struct模块(了解)
该模块可以把一个类型,如数字,转成固定长度的bytes
struct.pack(\'i\',11111111) #struct.error: \'i\' format requires -2147483648 <= number <= 2147483647 #这个是范围
struct.pack用于将Python的值根据格式符,转换为字符串(因为Python中没有字节(Byte)类型)。它的函数原型为:struct.unpack(fmt, string)。
struct.unpack做的工作刚好与struct.pack相反,用于将字节流转换成python数据类型。它的函数原型为:struct.unpack(fmt, string),该函数返回一个元组。
以上是关于粘包现象的主要内容,如果未能解决你的问题,请参考以下文章
详解啥是 TCP 粘包和拆包现象并演示 Netty 是如何解决的