一、缓冲区

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

  1,I/O缓冲区每个TCP套接字都是单独存在

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

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

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

二、粘包现象

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

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)

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

 





















11-26 04:28