粘包问题

一.什么是粘包?

粘包指的是数据与数据之间没有明确的分界线,导致不能正确读取数据!

TCP、UDP协议传输数据时的具体流程,TCP协议也称之为流式协议,UDP称之为数据报协议

 

应用程序无法直接操作硬件,应用程序想要发送数据则必须将数据交给操作系统,而操作系统需要需要同时为所有应用程序提供数据传输服务,也就意味着,操作系统不可能立马就能将应用程序的数据发送出去,就需要为应用程序提供一个缓冲区,用于临时存放数据,具体流程如下:

发送方:

当应用程序调用send函数时,应用程序会将数据从应用程序拷贝到操作系统的缓存区,再由操作系统从缓存区读取数据并发送出去.

接收方:

对方计算机操作系统先收到数据,先将数据存放到操作系统的缓冲区中,当应用程序调用recv时,实际上是从操作系统缓冲区将数据拷贝到应用程序的过程.

上述过程对于TCP和UDP都是相同的,不同之处在于:

UDP:

UDP在收发数据时是基于数据包的,即一个包一个包的发送,包与包之间有着明确的分界,到达操作系统缓存区后也是一个一个独立的数据包,接收方从操作系统缓冲区中将数据包拷贝到应用程序.

这种方式存在的问题:

1.发送方发送的数据长度每个操作系统会有不同的限制,数据超过限制则无法发送

2.接收方接受数据时如果应用程序的提供的缓存容量小于数据包的长度将造成数据丢失,而缓存区大小不可能无限放大.

TCP:

当我们需要传输较大的数据,或需要保证数据的完整性时 ,最简单的方式就是使用TCP协议

与UDP不同TCP会增加一套校验的规则来保证数据的完整性,会将超过TCP最大长度的数据拆分成多个TCP包,并在传输数据时为每一个TCP数据包指定一个顺序号,接收方在收到TCP数据包后按照顺序将数据包进行重组,重组后的数据全都是二进制数据,且每次收到的二进制数据之间没有明显的分界.

基于这种工作机制TCP在三种情况下会发送粘包问题

1.当单个数据包较小时接收方可能一次性读取多个包的数据

2.当整体数据较大时接收方可能一次仅读取一个包的一部分内容

3.TCP为了提高效率,会将数据较小且发送间隔较短的数据合并发送,该机制也会导致发送方将两个数据包粘在一起发送.

二.粘包的解决方案

1.基础解决方案

服务器:
import socket,subprocess
server = socket.socket()
server.bind(('192.168.12.207',4396))
server.listen()
while True:
  client,addr = server.accept()
  while True:
    try:
      cmd = client.recv(1024).decode('utf-8')
      p = subprocess.Popen(cmd,shell=True,\
      stdout=subprocess.PIPE,stderr=subprocess.PIPE)
      data = p.stdout.read()
      err_data = p.stderr.read()
      print('数据长度:%s'%(len(data) + len(err_data)))
      length = len(data) + len(err_data)
      len_str = str(length).encode('utf-8')
      #先发送长度数据再发送真实数据,长度数据可能和真实数据粘在一起,而接收方不知道长度数据的字节数,导致粘包
      client.send(len_str)
      client.send(data)
      client.send(err_data)
    except ConnectionResetError:
      print('连接异常')
      client.close()
      break
客户端:
import socket
client = socket.socket()
client.connect(('192.168.12.207',4396))
while True:
  cmd = input('>>:').strip()
  client.send(cmd.encode('utf-8'))
  length = client.recv(1024)
  len_data =int(length.decode('utf-8'))
  print('数据长度为%s'%len_data)
  data_all= b''
  data_size = 0
  while data_size < len_data:
    data = client.recv(len_data)
    data_size += len(data)
    data_all += data
  print('接受长度为%s'%data_size)
  print(data_all.decode('GBK'))

为了解决长度数据和真实数据的粘包问题,我们要用到:struct结构体 可以将python中的数据类型转换成C中的结构体(转换成bytes)

import struct
num = 100
#该函数 将一个python中的数据类型转换成bytes 第一个参数通常是i 其能转换的数据范围是C语言中的int范围
#如果int不够,那就使用q 表示的是long long型
res = struct.pack('i',num)
print(res)
print(len(res))

#该函数将bytes类型转换
res2 = struct.unpack('i',res)
print(res2) #得到一个元组(100,)
print(res2[0]) #得到整型 100

修正版本:

服务器:
import socket,subprocess,struct
server = socket.socket()
server.bind(('192.168.12.207',4396))
server.listen()
while True:
  client,addr = server.accept()
  while True:
    try:
      cmd = client.recv(1024).decode('utf-8')
      p = subprocess.Popen(cmd,shell=True,\
      stdout=subprocess.PIPE,stderr=subprocess.PIPE)
      data = p.stdout.read()
      err_data = p.stderr.read()
      print('数据长度:%s'%(len(data) + len(err_data)))
      length = len(data) + len(err_data)
      len_data = struct.pack('i',length)
      client.send(len_data)
      client.send(data)
      client.send(err_data)
    except ConnectionResetError:
      print('连接异常')
      client.close()
      break
客户端:
import socket,struct
client = socket.socket()
client.connect(('192.168.12.207',4396))
while True:
  cmd = input('>>:').strip()
  client.send(cmd.encode('utf-8'))
  #接受一个长度为4的固定字节
  length = client.recv(4)
  len_data = struct.unpack('i',length)[0]
  print('数据长度为%s'%len_data)
  data_all= b''
  data_size = 0
  while data_size < len_data:
    data = client.recv(1024)
    data_size += len(data)
    data_all += data
  print('接受长度为%s'%data_size)
  print(data_all.decode('GBK'))

2.自定义报头解决粘包

上述方案已经完美的解决了粘包的问题,但是扩展性不高,例如我们要实现文件上传下载,不光要传输文件的数据,还需要传输文件名字,md5值等等,如何实现?

具体思路:

发送端:

1.先将所有的额外信息打包到一个头中

2.然后先发送头部数据

3.最后发送真实数据

接收端:

1.接受固定长度的头部数据长度

2.根据长度数据获取头部数据

3,根据头部数据获取真实数据

服务器:
import socket,subprocess,struct,datetime,json
server = socket.socket()
server.bind(('192.168.12.207',4396))
server.listen()
while True:
  client,addr = server.accept()
  while True:
    try:
      cmd = client.recv(1024).decode('utf-8')
      p = subprocess.Popen(cmd,shell=True,\

stdout=subprocess.PIPE,stderr=subprocess.PIPE)

      data = p.stdout.read()
      err_data = p.stderr.read()
      print('数据长度:%s'%(len(data) + len(err_data)))
      length = len(data) + len(err_data)
      #先发送额外数据,将要发送的真实数据长度先存到字典中
      t = {}
      t['time'] = str(datetime.datetime.now())
      t['size'] = length
      #将字典转换成json格式
      t_json = json.dumps(t)
      #将字典转换成字节
      t_data = t_json.encode('utf-8')
      t_length = struct.pack('i',len(t_data))
      #1.先发送额外数据长度
      client.send(t_length)
      #2.发送额外信息
      client.send(t_data)
      #3.发送真实数据
      client.send(data)
      client.send(err_data)
    except ConnectionResetError:
      print('连接异常')
      client.close()
      break
客户端:
import socket,struct,json
client = socket.socket()
client.connect(('192.168.12.207',4396))
while True:
  cmd = input('>>:').strip()
  if not cmd:
    print('命令不能为空!')
    continue
  client.send(cmd.encode('utf-8'))
  #1.先接受额外信息长度
  length = client.recv(4)
  len_data = struct.unpack('i',length)[0]
  #2.接受额外信息
  t_data = client.recv(len_data)
  print(t_data.decode('utf-8'))
  json_dic = json.loads(t_data.decode('utf-8'))
  print('执行时间为%s'%json_dic['time'])
  data_size = json_dic['size']
  #3,接受真实信息
  data_all= b''
  rcv_size = 0
  while rcv_size < data_size:
    data = client.recv(1024)
    rcv_size += len(data)
    data_all += data
  print('接受长度为%s'%rcv_size)
  print(data_all.decode('GBK'))

 

posted @ 2018-12-28 17:34  Zhuang_Z  阅读(164)  评论(0)    收藏  举报