一、问题原由
最近看了其他的一些python实现的socket通信的相关样例或demo代码,一直想找一个较为不错的样例作为编写socket通信的一个样板,固定下编写风格,但是苦于找到的相关样例都是非常简易的一个demo,甚至针对于超过buf_size需要多次recv数据然后进行数据拼接都没有做。这样就导致demo在演示传输较大数据或者是传输文件二进制流,或是其他编码流的方面产生无法解析或无法解码的情况出现。或是在聊天场景下发送超过buf_size的信息会出现收取断层的情况。基于以上问题,设计了这么一个实现思路。
思路
由于recv一次收取的信息量为提前设置好的buf_size的大小,所以需要我们多次收取数据,直到数据收取完毕。
可能涉及到的问题:
一、如何判断一个数据是否已经收取完毕?
思路1:先传提前传一个即将传输的数据长度,当收取数据的长度到达提前传来的长度的时候判定为收取完成。弊端:传输一个数据需要传两次,一次数据长度,一次数据本体。如果传输的数据长度出现异常,server端可能出现奇怪的异常错误。
思路2:根据client端的close关闭socket连接的形式来判定数据传输完成。即当与服务端的socket断开连接的时候会传输一个空字符串,可以以收到空字符串来作为判断数据结束的标识。弊端:这种情况需要发送一次数据建立一次连接然后断开一次连接,适合短连接场景,但是对于持久通信来说并不太合适。
思路3:通过协议数据中的负载长度值来确定有效报文长度。弊端:协议数据中需要包含负载协议字段,并且需要一边收数据的情况下一遍解析数据,相对来说比较适合有负载协议字段情况下的数据传输。
扩展思路4:从前三个常见的思路来看,思路二实现起来较容易,并且出错的可能性最小,同时如果我们能够再尝试适配长连接的情况下,就能让思路二适用于socket简单通信的绝大多数场景。因此我们选择从思路二入手,由思路二我们能想到,当close的时候会发送空字符串并且我们将空字符串作为了判断数据结束的标志,如果我们把空字符串换成我们自定义的一个字符,即可对数据结尾做一个标识判断。比如说我们用@符号作为数据的结尾,当收取到@符号的时候我们判定为数据收取完成。弊端:采用自定义符号会随之带来一个弊端,如果我们传输的数据中本身就带有我们自定义的符号,就会导致数据的提前收取结束,从而获取到的数据不完整。
扩展思路5:针对于扩展思路4,我们需要解决的是如何让我们设置的数据结尾标志唯一,不被数据本身干扰。由此联想到了数据编码,选择编码的时候我们需要关注的是:
1、编码后的数据符号需要是可控的有限的(让数据结尾自定义的标志唯一)
2、编码需要是可逆的(不可逆的话没啥用了)
3、编码可以针对ascii字符但是不限于ascii字符进行编码,即也可以对不可见字符进行编码如二进制数据。(覆盖数据范围大)
4、编码以及解码的效率和速度要高(避免拖慢传输速度)
5、编码后的数据大小尽可能的小(避免拖慢传输速度)
综上条件我们可以针对大文件采用压缩编码如Huffman编码,针对普通数据或字符串采用base64编码,针对状态信息如返回成功的状态码或者成功的状态信息等可采用Hex编码。(大文件的其实也可以先压缩后再base64编码其二进制数据,或者是直接传输二进制数据(较长)也可以,普通信息或状态码等都可用base64编码,Hex编码后数据长度较长)
base64在编码数据后数据长度与原数据长度相比,稍微会大点,约为原数据的4/3 [原因是:采用8位字符来表示信息中的6个位],但是要比Hex编码后的长度要小的多,同时base64的基础数据集合为64个可打印字符,这64个字符中包括大小写字母、数字、+ 和 / ,还有用来补缺的特殊字符 = 。因此最终我选择base64编码。
最终思路:
自定义一个结束标志,符号需要在base64基础数据集合之外的符号,如#、@等,我选择#。首先对数据进行base64编码,然后拼接#号作为数据结尾,再采用思路二的循环收取数据,只不过把思路二的close结束标志空字符串换成了我们自定义的#号,这样就可以保证长连接了。
功能实现代码:
接收服务端数据功能函数实现:
def _recv_data(self):
"""
接收服务端数据
:return: 接收服务端数据-ERROR=None
"""
data = b''
while True:
try:
self._tcpCliSock.settimeout(10) # 设置超时时间为10s超过10s判定为服务端没有返回状态,未收到数据
recv_data = self._tcpCliSock.recv(self._BUFSIZ)
if len(recv_data) > 0:
if recv_data[-1:] == b'#':
data += recv_data[:-1]
break
else:
data += recv_data
else:
break
except Exception as e:
print("Socket receiving data error! | "+str(e))
return None # 出现异常返回None
if len(data) != 0:
return self._de_data(data)
else:
return "" # 非异常无数据,返回空字符串
短连接实现:
def sent_data(self, data=None):
"""
发送数据,短连接
:param data: 要发送的数据 [str]
:return: 收到的服务端发来的数据或状态 [str]
"""
if data:
self._tcpCliSock.sendall(self._en_data(data))
recv = self._recv_data()
self.close()
return recv
else:
print("The sent data is empty or not sent!")
长连接实现:
def sent_data_pending_input(self):
"""
发送数据,长连接
"""
lastdata = ''
while lastdata != "quit":
lastdata = input("请输入要发送的数据:")
if len(lastdata) > 0:
self._tcpCliSock.sendall(self._en_data(lastdata))
recv = self._recv_data()
print(recv)
else:
print("The sent data is empty or not sent!")
print("Client端已退出!")
服务端handle实现:
def handle(self):
while True:
data = b''
while True:
try:
recv_data = self.request.recv(BUF_SIZE)
if len(recv_data) > 0:
if recv_data[-1:] == b'#':
data += recv_data[:-1]
break
else:
data += recv_data
else:
break
except Exception as e:
print("Socket receiving data error! | "+str(e))
break
if len(data) != 0:
recv = self.de_data(data)
print('收到数据:', recv)
self.request.sendall(self.en_data("服务端已收到数据: "+str(recv)))
else:
print("Client Close!")
break
写文章不容易,感觉还不错的点个赞,点个收藏吧~,有问题可以评论区留言。
以上代码为功能函数实现,可参考,由于写的时候太晚了我不想把完整代码再拷贝一份了,如果需要完整功能代码(搬砖/演示/测试)请去我另一个blog获取。https://blog.gcoperation.top/posts/c961f1fb/
Python实现socket通信样例 | Guf's Bloghttps://blog.gcoperation.top/posts/c961f1fb/