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

转载自:https://www.cnblogs.com/12345huangchun/p/10011324.html

 

一、缓冲区

  每个 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)

复制代码

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值