参考博客:http://www.cnblogs.com/kex1n/p/6502002.html

一、粘包现象

  在上一篇的socket传输大数据文章中,我们可以顺利的接发数据,似乎做的不错,可以接收了。但是其实还隐藏着一个问题,请看下面的情况:

  socket--粘包-LMLPHP

  我们看到出错的位置,具体分析一下是什么情况

  socket--粘包-LMLPHP

  这个功能在上篇中介绍是用来判断数据量大小的,即服务端先将其要发送的数据量大小发给客户端,然后客户端再接收数据时,以此来判断是否接收了全部数据。

  我们在来看看服务端的代码: 

# 服务端先将数据大小发送给客户端,用于对比
conn.send(str(len('{}'.format(cmd_res).encode('utf-8'))).encode('utf-8'))
# 发送数据
conn.send('{}'.format(cmd_res).encode('utf-8'))

  第一句代码的作用是发送数据量大小,第二句则是发送数据本身。由错误我们可以判断出,第一次发送的数据当中,不仅仅是数据量大小,即数据长度,还包含了第二次发送的部分数据。所以str格式的字符当然没办法 int了,就报错了。但是为什么发送的数据会混淆呢,why?

二、Socket粘包

  同志们,这就是粘包了,也就是前后发送的数据在接收端那边是合在一起的。粘包现象不一定每次都出现,我试了二三十次才终于出现了。那么粘包现象是怎么来的呢?我目前知道的还少,只能有一点说一点了,先做个笔记,再慢慢了解了。

  从现象上来看,粘包就是后一个数据紧跟着前一个数据的末尾被接收了,这样的影响就是如果两个包的数据格式不一样,分别要用来处理不同问题,比如上例中一个是int 一个是 str,就会出错。即便是数据格式一致,但前后数据连在一起,可能接收方就不知道其本意了。

  2.1 考虑出现粘包

  1)如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题。关闭连接主要是要双方都发送close连接,双方在数据发送和接收完毕后不再继续发送和接收新数据,而是双方关闭连接,这样就不用考虑粘包问题。

  2)如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包

3)UDP发送数据不用考虑

  4)如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构,需要考虑粘包

  5)双方建立连接后,一段时间内不断发送数据,需要考虑粘包

  2.2 粘包原因

  1. 发送端需要等缓冲区满才发送出去,造成粘包
  2. 接收方不及时接收缓冲区的包,造成多个包接收 

  具体点:

  (1)发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

  (2)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

  粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。

三、如何解决粘包

  具体情况具体分析吧。我们还是以上例来处理

  3.1 利用超时

  socket将数据存入缓冲区后,TCP/IP协议会将数据发送给接收方,我们在两次连续发送数据时加入一个间隔时间,比如说: 

conn.send(str(len('{}'.format(cmd_res).encode('utf-8'))).encode('utf-8'))

time.sleep(0.5)   # 两次发送数据间隔一段时间
# 发送数据
conn.send('{}'.format(cmd_res).encode('utf-8', 'ignore'))

  这样确实能解决问题,但是显然有缺陷,你让老司机发车的时候,总是停停顿顿的,他还不砸了硬盘?哦不,是方向盘。

  3.2 接收端收到数据后回复一个确认

  利用socket中的recv()函数是阻塞的,在接收端不返回确认时,就不会继续发送数据。同样在两次发送数据之间添加这个功能,代码如下:

  服务端:

# -*- coding: UTF-8 -*-
import os
import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP/IP协议, tcp ,如果不填写就是默认这个 server.bind(('localhost', 9999)) server.listen() while True: # 可以接受多个客户端 conn, addr = server.accept() while True: data = conn.recv(1024)
if not data: # 防止当接受的客户端数据为空时,程序卡掉
print('client has lost...')
break
print('执行命令:', data.decode())
cmd_res = os.popen(data.decode()).read() if len(cmd_res) == 0:
print('command not found')
else:
# 服务端先将数据大小发送给客户端,用于对比
conn.send(str(len('{}'.format(cmd_res).encode('utf-8'))).encode('utf-8'))
# 这里等待接收到返回
clientACK = conn.recv(1024)
# 发送数据
conn.send('{}'.format(cmd_res).encode('utf-8', 'ignore'))
print('发送完成')

  客户端:

# -*- coding: UTF-8 -*-
import socket client = socket.socket() client.connect(('localhost', 9999)) while True:
cmd = input('>>:').strip()
# 判断是否发送空数据,如果是就重新发送
if len(cmd) == 0:
continue
else:
client.send(cmd.encode('utf-8'))
data_size = client.recv(1024) # 接收服务端发送的数据大小
print(data_size)
data_length = int(data_size.decode())
# 此处就是给服务端返回确认
client.send('返回确认收到数据大小,以解决粘包'.encode('utf-8')) print('返回数据大小:', data_length)
# 定义已接收数据大小为0
received_length = 0
# 定义已接收数据为0
received_data = b''
while received_length < data_length:
r_data = client.recv(1024) # 接受的数据是bytes类型
received_length += len(r_data)
received_data += r_data
else:
print('接收数据大小:', received_length)
print(received_data.decode('utf-8', 'ignore')) # 不加ignore在windows有时会报错
print('数据接收完毕!')

  其实改的不是很多,就添加了两行代码,但是效果杠杠的,方向盘是保住了。

  

05-25 10:39