原理概述
上图是我在学习python的socket编程中遇到的黏包问题所画,以实例来说明这个高大上的黏包问题。
我们知道socket()实例中sendall()方法是无论数据有多大,一次性提交写入缓冲区(应用层);再来看接收端,recv()方法有个参数为buffsize,没错buffsize就是套接口的发送缓冲区的大小了。所以数据大于SO_SNDBUF的就会被分块传输,问题就来了,当两次提交的数据都比较大,刚好第一次尾与第二次的首同一时间待在了SO_SNDBUF里,被接收到了,这就是黏包。
一句话:黏包最本质的原因就是接收方不知道接收的包有多大!
解决方法(应用层维护消息和消息边界):
- 定长包
- 包尾加上\r\n标记(FTP)
- 包头增加包体长度。
- 复杂的应用层协议。
实例
本实例多线程实例,实现的是客户端向服务端输入系统命令,服务器返回命令在本机上的执行结果。因为有些命令返回的结果是远大于1024的,所以可能出现黏包的问题。本实例的解决方案是第三条,server端每次向client端返回命令执行结果前,先发送包体大小并得到client端返回的确认信息,再发送数据,程序结构上避免了黏包问题。
TCPSocket服务端
#!/usr/bin/env python #-*- coding:utf-8 -*- import SocketServer import os class Myserver(SocketServer.BaseRequestHandler): def handle(self): conn = self.request print "Client from:",self.client_address conn.sendall("请输入您要查询的命令") flag = True while flag: data = conn.recv(1024) print "receive cmd: %s"%data if data == "exit": flag = False else: ret = os.popen(data).read().decode("gbk").encode("utf-8")
#发送包大小 conn.sendall(str(len(ret))) #收到客户端确认消息。 scOK = conn.recv(1024)
#发送包体内容 conn.sendall(ret) if __name__ == "__main__": server = SocketServer.ThreadingTCPServer(("localhost",8000),Myserver) server.serve_forever()
TCPSocket客户端
#!/usr/bin/env python #-*- coding:utf-8 -*- import socket sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk.connect(("127.0.0.1",8000)) sk.settimeout(5) data = sk.recv(1024) print "SocketServer: %s" % data while True: reSize = 0 msg = raw_input("Input:") sk.sendall(msg)
#接收包的大小 totleSize = int(sk.recv(1024))
#收到包的大小后,给server端发送确认信息。 sk.sendall("It is ok") while True: data = sk.recv(1024) reSize += len(data)
#当接收数据等于包的size后,跳出循环,停止 接收。 if reSize == totleSize: print data break print data if msg == "exit": break sk.close()