socket--粘包
参考博客:http://www.cnblogs.com/kex1n/p/6502002.html
一、粘包现象
在上一篇的socket传输大数据文章中,我们可以顺利的接发数据,似乎做的不错,可以接收了。但是其实还隐藏着一个问题,请看下面的情况:
我们看到出错的位置,具体分析一下是什么情况
这个功能在上篇中介绍是用来判断数据量大小的,即服务端先将其要发送的数据量大小发给客户端,然后客户端再接收数据时,以此来判断是否接收了全部数据。
我们在来看看服务端的代码:
# 服务端先将数据大小发送给客户端,用于对比 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)发送方引起的粘包是由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('数据接收完毕!')
其实改的不是很多,就添加了两行代码,但是效果杠杠的,方向盘是保住了。