基于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:www.mingcheng178.com /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(www.chuangshi88.cn)
  
  conn,adrr=server.accept(www.fengshen157.com)
  
  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(www.dasheng178.com)
  
  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.www.gcyl152.com/ recv(1024)
  
  print(int(data_len.decode('utf-8')))
  
  client.send(b'www.michenggw.com ok')
  
  data=client.recv(int(data_len.decode('utf-8')))
  
  print(data.decode(www.dfgjyl.cn '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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值