欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于tcp协议下粘包现象和解决方案

程序员文章站 2022-06-12 08:09:47
一、缓冲区 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被 ......

一、缓冲区

  每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由tcp协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是tcp协议负责的事情。tcp协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。send()/recv()函数也是,都是从缓冲区拿数据,而不是直接从网络中拿数据。i/o缓冲区特性整理如下:

  1,i/o缓冲区每个tcp套接字都是单独存在

  2,i/o缓冲区在创建套接字时自动生成

  3,即使关闭套接字也会继续向缓冲区发送输出缓冲区遗留的数据

  4,关闭套接字将会丢失输入缓冲区中的数据

二、粘包现象

基于tcp协议下粘包现象和解决方案

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)

   在每次发送消息之后,要接收一个确认消息;在每次接收消息之后,要发送一个确认消息。至于发送的确认消息内容随便写,在真实程序中,确认消息内容是没有实际意义的,只是在每次发送消息后接收一个确认消息来形成阻塞,这样就避免了粘包的问题。