基于tcp协议下粘包现象和解决方案
一、缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由tcp协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是tcp协议负责的事情。tcp协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。send()/recv()函数也是,都是从缓冲区拿数据,而不是直接从网络中拿数据。i/o缓冲区特性整理如下:
1,i/o缓冲区每个tcp套接字都是单独存在
2,i/o缓冲区在创建套接字时自动生成
3,即使关闭套接字也会继续向缓冲区发送输出缓冲区遗留的数据
4,关闭套接字将会丢失输入缓冲区中的数据
二、粘包现象
1,模拟第一种粘包现象:客户端发送发送ipconfig -all指令,让服务端接收指令,然后执行指令,把结果传输回来,但由于结果太大,客户端第一次没有接收完,然后客户再发送dir指令,但此时服务端发给客户端的并不是dir指令得到的结果,而是接着ipconfig-all没发完的指令,但这不是我想要的结果,这就是第一种粘包现象。
服务端
import subprocess import socket server=socket.socket() ip_port=('192.168.12.39',8888) server.bind(ip_port) server.listen() conn,adrr=server.accept() while 1: from_client_cmd=conn.recv(1024) sub_pbj=subprocess.popen( from_client_cmd.decode('utf-8'), shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe ) cmd_msg=sub_pbj.stdout.read() conn.send(cmd_msg)
客户端
import socket
client=socket.socket()
ip_port=('192.168.12.39',8888)
client.connect(ip_port)
while 1:
client_cmd=input('亲输入:')
client.send(client_cmd.encode('utf-8'))
from_server_msg=client.recv(1024)
print(from_server_msg.decode('gbk'))
d:\python3.6\python.exe d:/python3.6/程序/day24/粘包现象2客户端.py 亲输入:ipconfig -all #输入指令ipconfig-all得到的结果,但没接受完 windows ip 配置 主机名 . . . . . . . . . . . . . : desktop-ld9s9gg 主 dns 后缀 . . . . . . . . . . . : 节点类型 . . . . . . . . . . . . : 混合 ip 路由已启用 . . . . . . . . . . : 否 wins 代理已启用 . . . . . . . . . : 否 无线局域网适配器 本地连接* 3: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 dns 后缀 . . . . . . . : 描述. . . . . . . . . . . . . . . : microsoft wi-fi direct virtual adapter 物理地址. . . . . . . . . . . . . : 68-07-15-e4-b4-6c dhcp 已启用 . . . . . . . . . . . : 是 自动配置已启用. . . . . . . . . . : 是 无线局域网适配器 wlan 3: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 dns 后缀 . . . . . . . : 描述. . . . . . . . . . . . . . . : intel(r) dual band wireless-ac 3165 物理地址. . . . . . . . . . . . . : 68-07-15-e4-b4-6b dhcp 已启用 . . . . . . . . . . . : 是 自动配置已启用. . . . . . . . . . : 是 无线局域网适配器 本地连接* 12: 媒体状态 . . . . . . . . . . . . :
然后再输入指令dir
亲输入:dir #这是输入指令dir得到结果,但这根本不是执行dir指令后正确的结果,而是ipconfig -all没有接收完的结果
媒体已断开连接
连接特定的 dns 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : microsoft wi-fi direct virtual adapter #2
物理地址. . . . . . . . . . . . . : 6a-07-15-e4-b4-6b
dhcp 已启用 . . . . . . . . . . . : 是
自动配置已启用. . . . . . . . . . : 是
以太网适配器 以太网 3:
连接特定的 dns 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : realtek pcie gbe family controller #3
物理地址. . . . . . . . . . . . . : c8-5b-76-41-e6-c6
dhcp 已启用 . . . . . . . . . . . : 是
自动配置已启用. . . . . . . . . . : 是
本地链接 ipv6 地址. . . . . . . . : fe80::2058:e998:9fa9:4f8e%21(首选)
ipv4 地址 . . . . . . . . . . . . : 192.168.12.39(首选)
子网掩码 . . . . . . . . . . . . : 255.255.255.0
获得租约的时间 . . . . . . . . . : 2018年11月24日 8:33:56
租约过期的时间 . . . . . . . . . : 2018年11月25日 8:33:56
默认网关. . . . . . . . . . . . . : 192.168.12.254
dhcp 服务器 . . . . . . . . . . . : 192.168.12.254
dhcpv6 iaid . . . .
我们来看看ipconfig -all 和dir的正确结果
dir结果
驱动器 d 中的卷没有标签。
卷的序列号是 0007-53fa
d:\python3.6\程序\day24 的目录
2018/11/24 08:54 <dir> .
2018/11/24 08:54 <dir> ..
2018/11/23 21:32 142 aaa.txt
2018/11/23 21:36 80 ffff.log
2018/11/23 10:28 0 __init__.py
2018/11/24 08:54 1,607 作业文件客户端.py
2018/11/24 08:54 1,432 作业文件服务端.py
2018/11/23 15:03 146 粘包现象1客户端.py
2018/11/23 15:04 264 粘包现象1服务端.py
2018/11/23 10:35 268 粘包现象2客户端.py
2018/11/23 14:54 426 粘包现象2服务端.py
2018/11/23 16:13 456 粘包解决方案1大数据客户端.py
2018/11/23 16:13 655 粘包解决方案1大数据服务端.py
2018/11/23 15:23 363 粘包解决方案1客户端.py
2018/11/23 15:23 532 粘包解决方案1服务端.py
2018/11/23 16:33 345 粘包解决方案2客户端.py
2018/11/23 16:25 471 粘包解决方案2服务端.py
2018/11/23 16:56 314 验证合法性客户端.py
2018/11/23 16:56 315 验证合法性服务端.py
17 个文件 7,816 字节
2 个目录 328,564,199,424 可用字节
来看看ipconfig-all的结果
windows ip 配置
主机名 . . . . . . . . . . . . . : desktop-ld9s9gg
主 dns 后缀 . . . . . . . . . . . :
节点类型 . . . . . . . . . . . . : 混合
ip 路由已启用 . . . . . . . . . . : 否
wins 代理已启用 . . . . . . . . . : 否
无线局域网适配器 本地连接* 3:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 dns 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : microsoft wi-fi direct virtual adapter
物理地址. . . . . . . . . . . . . : 68-07-15-e4-b4-6c
dhcp 已启用 . . . . . . . . . . . : 是
自动配置已启用. . . . . . . . . . : 是
无线局域网适配器 wlan 3:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 dns 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : intel(r) dual band wireless-ac 3165
物理地址. . . . . . . . . . . . . : 68-07-15-e4-b4-6b
dhcp 已启用 . . . . . . . . . . . : 是
自动配置已启用. . . . . . . . . . : 是
无线局域网适配器 本地连接* 12:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 dns 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : microsoft wi-fi direct virtual adapter #2
物理地址. . . . . . . . . . . . . : 6a-07-15-e4-b4-6b
dhcp 已启用 . . . . . . . . . . . : 是
自动配置已启用. . . . . . . . . . : 是
以太网适配器 以太网 3:
连接特定的 dns 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : realtek pcie gbe family controller #3
物理地址. . . . . . . . . . . . . : c8-5b-76-41-e6-c6
dhcp 已启用 . . . . . . . . . . . : 是
自动配置已启用. . . . . . . . . . : 是
本地链接 ipv6 地址. . . . . . . . : fe80::2058:e998:9fa9:4f8e%21(首选)
ipv4 地址 . . . . . . . . . . . . : 192.168.12.39(首选)
子网掩码 . . . . . . . . . . . . : 255.255.255.0
获得租约的时间 . . . . . . . . . : 2018年11月24日 8:33:56
租约过期的时间 . . . . . . . . . : 2018年11月25日 8:33:56
默认网关. . . . . . . . . . . . . : 192.168.12.254
dhcp 服务器 . . . . . . . . . . . : 192.168.12.254
dhcpv6 iaid . . . . . . . . . . . : 298343286
dhcpv6 客户端 duid . . . . . . . : 00-01-00-01-23-40-72-81-c8-5b-76-41-e6-c6
dns 服务器 . . . . . . . . . . . : 202.96.134.33
202.96.128.86
tcpip 上的 netbios . . . . . . . : 已启用
以太网适配器 蓝牙网络连接 2:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 dns 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : bluetooth device (personal area network) #2
物理地址. . . . . . . . . . . . . : 68-07-15-e4-b4-6f
dhcp 已启用 . . . . . . . . . . . : 是
自动配置已启用. . . . . . . . . . : 是
2,第二种粘包现象:客户端连续给服务端发送信息,服务端连续接收信息,此时就会把两个信息拼到一起,这就是第二种粘包现象
服务端
import socket server=socket.socket() ip_port=('192.168.12.39',8888) server.bind(ip_port) server.listen() conn,adrr=server.accept() msg1=conn.recv(1024) msg2=conn.recv(1024) msg3=conn.recv(1024) print('msg1:',msg1) print('msg2:',msg2) print('msg3:',msg3)
客户端
import socket
client=socket.socket()
ip_port=('192.168.12.39',8888)
client.connect(ip_port)
client.send(b'nihaoa')
client.send(b'woxihuanni')
运行后得到的结果
d:\python3.6\python.exe d:/python3.6/程序/day24/粘包现象1服务端.py
msg1: b'nihaoawoxihuanni' #此时把我发送的三个消息拼接到一起,得到错误结果
msg2: b''
msg3: b''
总结:发生粘包现象的最根本原因是(不管是服务端还是客户端)每次接收数据的后recv()括号里面总写的1024,但这个数字根本对不上具体要接收数据大小,当数据大于1024时,接受不完;当数据小于1024时,会把连续发的消息拼到一起。所以要解决粘包问题,首先就是要让接收端知道将要接收数据大小,然后根据数据大小来接收
三、解决粘包问题方案一
第一种方案就是把发送端先把要发送的数据大小发给接收端,接收端根据数据大小来具体接收
服务端 import subprocess import socket server=socket.socket() ip_port=('192.168.12.39',8888) server.bind(ip_port) server.listen() conn,adrr=server.accept() while 1: from_client_cmd=conn.recv(1024) obj=subprocess.popen( from_client_cmd.decode('utf-8'), shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe ) data=obj.stdout.read() print(len(data)) conn.send(str(len(data)).encode('utf-8')) msg1=conn.recv(1024) if msg1==b'ok': conn.send(data)
客户端
import socket
client=socket.socket()
ip_port=('192.168.12.39',8888)
client.connect(ip_port)
while 1:
cmd=input('请输入指令:')
client.send(cmd.encode('utf-8'))
data_len=client.recv(1024)
print(int(data_len.decode('utf-8')))
client.send(b'ok')
data=client.recv(int(data_len.decode('utf-8')))
print(data.decode('gbk'))
基于第一种解决方案中,当发送数据大小大于缓冲区大小时,就会自动报错,意思就是上面的解决方案发送不了大数据,对于大数据来说,我们也是先发送数据大小,然后在把数据进行循环的发过去
服务端 import subprocess import socket server=socket.socket() ip_port=('192.168.12.39',8888) server.bind(ip_port) server.listen() conn,adrr=server.accept() while 1: send_data_len = 0 from_client_cmd=conn.recv(1024) obj=subprocess.popen( from_client_cmd.decode('utf-8'), shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe ) data=obj.stdout.read() conn.send(str(len(data)).encode('utf-8')) while send_data_len < len(data): conn.send(data[send_data_len:send_data_len+1024]) send_data_len +=len(data[send_data_len:send_data_len+1024]) print(send_data_len)
客户端
import socket
client=socket.socket()
ip_port=('192.168.12.39',8888)
client.connect(ip_port)
while 1:
data = b''
recv_data_len = 0
cmd=input('请输入指令:')
client.send(cmd.encode('utf-8'))
data_len=client.recv(1024)
while recv_data_len<int(data_len.decode('utf-8')):
data1=client.recv(1024)
recv_data_len += len(data1)
data += data1
print(recv_data_len)
print(data.decode('gbk'))
四、解决粘包问题方案二
第二种解决方案也是要让接收端知道数据大小,但我们可以把数据大小转成四个字节的bytes类型,然后和数据拼接到一起发过去,接收端先只接受4个字节,然后把四个字节转换成int类型,此时接收端接获得了数据大小,然后根据数据大小来接收数据
服务端 import struct import subprocess import socket server=socket.socket() ip_port=('192.168.12.39',8888) server.bind(ip_port) server.listen() conn,adrr=server.accept() while 1: from_client_cmd=conn.recv(1024) obj=subprocess.popen( from_client_cmd.decode('utf-8'), shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe ) data=obj.stdout.read() data1=struct.pack('i',len(data)) conn.send(data1+data)
客户端
import struct
import socket
client=socket.socket()
ip_port=('192.168.12.39',8888)
client.connect(ip_port)
while 1:
cmd=input('请输入指令:')
client.send(cmd.encode('utf-8'))
data_len=client.recv(4)
real_len=struct.unpack('i',data_len)[0]
data=client.recv(real_len)
print(data.decode('gbk'))
五、解决粘包问题方案三
这属于我自己的想法,不知道是不是粘包解决方案,但我认为是。通过发送端发送一条消息,接收端接收一条消息,然后接收端发送一条确认消息,发送端接收一条确认消息,然后发送端再发送第二条消息。下面就用文件上传和下载的程序来证明一下。
服务端 import socket import os import json server=socket.socket() ip_port=('192.168.12.39',8888) server.bind(ip_port) server.listen() #服务端验证客户端信息是否正确 def fun1(conn): info=conn.recv(1024) d1=json.loads(info.decode('utf-8')) if d1['name']=='alex' and d1['password']=='123': conn.send(b'200') return b'200' else: conn.send(b'100') return b'100' def shangchuan(conn): #服务端接收客服端上传的文件 name=conn.recv(1024) conn.send(b'2222') f1=open(r'c:\users\admin\desktop\%s'%name.decode('utf-8'),mode='wb') while 1: data = conn.recv(1024) conn.send(b'2222') #此处为发送确认信息 if data == b'over': break f1.write(data) f1.close() l1=os.listdir(r'c:\users\admin\desktop') conn.send(str(l1).encode('utf-8')) def xiazai(conn): #服务端发送客户端下载的文件 name=conn.recv(1024) conn.send(b'200') #此处为发送确认信息 f1=open(r'c:\users\admin\desktop\%s'%name.decode('utf-8'),mode='rb') for line in f1: conn.send(line) conn.recv(1024) #此处为接收确认信息 else: conn.send(b'over') conn.recv(1024) 此处为接收确认信息 f1.close() while 1: #服务端程序入口 conn,adrr=server.accept() result=fun1(conn) if result==b'200': num=conn.recv(1024) if num.decode('utf-8')=='1': shangchuan(conn) elif num.decode('utf-8')=='2': l1 = os.listdir(r'c:\users\admin\desktop') conn.send(str(l1).encode('utf-8')) xiazai(conn)
客户端
import socket
import json
import os
client=socket.socket()
ip_port=('192.168.12.39',8888)
client.connect(ip_port)
def fun1(client): #客户端发送用户信息到服务端进行验证
d1={}
name=input('请输入你的用户名:')
password=input('请输入你的密码:')
d1['name']=name
d1['password']=password
info=json.dumps(d1)
client.send(info.encode('utf-8'))
nn=client.recv(1024)
return nn
def shangchuan(clinet): #这是客户端向服务端上传文件
url=input('请输入上传文件的绝对路径:')
name=os.path.split(url)[1]
client.send(name.encode('utf-8'))
client.recv(1024) #此处为接收确认信息
f1=open(url,mode='rb')
for line in f1:
client.send(line)
client.recv(1024) #此处为接收确认信息
else:
client.send(b'over')
client.recv(1024) #此处为接收确认信息
print('上传成功')
f1.close()
data1=client.recv(1024)
print(data1.decode('utf-8'))
def xiazai(client): #这是客户端从服务端下载文件
url=input('请输入想下载的文件名(加上后缀):')
client.send(url.encode('utf-8'))
client.recv(1024) #此处为接收确认信息
f1=open(r'e:\%s'%url,mode='wb')
while 1:
data = client.recv(1024)
client.send(b'2222') #此处为接收确认信息
if data == b'over':
print('下载成功')
break
f1.write(data)
f1.close()
while 1: 客户端程序入口
send_data_len=0
result=fun1(client)
if result==b'200':
num=input('上传输1,下载输2')
client.send(num.encode('utf-8'))
if num=='1':
shangchuan(client)
elif num=='2':
data1 = client.recv(1024)
print(data1.decode('utf-8'))
xiazai(client)
在每次发送消息之后,要接收一个确认消息;在每次接收消息之后,要发送一个确认消息。至于发送的确认消息内容随便写,在真实程序中,确认消息内容是没有实际意义的,只是在每次发送消息后接收一个确认消息来形成阻塞,这样就避免了粘包的问题。
下一篇: 名著要熟读精思,就能孵出冷笑话