python socket编程
为什么写这篇文章
因为你现在网上搜索的很多demo代码是不可以在生产环境中用的, 先来说一下最常见的误解:
- conn.recv(1024) 并不是说一定会收到1024个字节,而是尝试获取1024个字节的数据,但是不保证一定拿到。 加入你client发送了一个完整的数据,正好是1024个字节,然后server调用conn.recv(1024), 你将获取的结果打印出来,你会发现卧槽,怎么只打印了一半。 因此要想完整收到指定数量字节必须用while True, 请仔细参考下方的read函数。
- 关于流边界
socket传输的是纯的二进制流,它并不知道取多少字节才算一个完整的消息,你在网上看到的client发送一个 string, 服务端conn.recv(1024)然后再打印大多数时候是没有问题的,因为这只是巧合,你发送的信息比较短, recv的时候正好一次性全部读出来了,我只能说这只是demo,生产环境无法用。 真正的socket通信,一条完整的消息如何传输与接收呢,这需要我们自己设计头协议的。头协议约定了client和server 如何进行流数据的解析,这个协议一般是我们自己设计的。 而不是网上随处可见的demo.
头协议设置
header={
"msg_type":'body' #
"data_length":123
}
msg_type=body 表示接下来的123个字节是数据包
msg_type=heart表示这是一个心跳头,此时data_length为0
client发送流程
- client 先发送 4个字节的一个数字
这个数字表示,接下来的 header的长度是多少字节
假设
header={
“msg_type”:‘body’
“data_length”:123
}
我们把header转成byte之后计算它的长度,这肯定是个数字,把这个数字发送到server - client 在把header发送到server端
- client把body 发送到server端
server解析数据
- server 先接收4字节的数字
假设这个数字为 133, 这个表示接下来socket传输的133个字节是header - server 再接收 133个字节
这次收到的就是header了, 然后取出header[‘msg_type’]if msg_type="body" 则取出:header["data_length"],,假设取出的数字为 10000 意思是,接下来的10000个字节是真正的数据体, 再继续接收 10000个字节 else msg_type="heart" 此时意味着这是一个心跳包, 则忽略就行了
心跳包的作用
有人可能会有疑问,tcp协不是长连接么,只要建立了连接在不出意外的情况下不是可以一直维持连接吗?为什么还需要心跳包。
在TCP/IP网络通信中,尽管TCP连接本身是面向连接的、可靠的传输层协议,并且TCP协议自身包含了许多机制来确保数据的可靠传输(如序列号、确认应答、超时重传等),但这并不意味着TCP连接就是“永远在线”或“长连接”就一定不会出现问题。实际上,长连接(也称为持久连接)指的是TCP连接在数据传输完成后不会被立即关闭,而是保持一段时间以便后续的数据传输,这样可以减少因为频繁建立和关闭连接所带来的开销。
然而,即使使用了长连接,也仍然可能遇到以下问题:
- 网络问题:网络中的设备(如路由器、交换机、防火墙等)可能会因为各种原因(如负载均衡、故障恢复、安全策略等)而中断或重置空闲的TCP连接。这些设备通常会有超时时间设置,用于清理长时间没有数据交换的连接。
- NAT(网络地址转换)超时:在使用NAT的私有网络中,NAT设备可能会因为超时而丢弃空闲的TCP连接。这是因为NAT表项资源有限,需要回收长时间未使用的表项以释放资源。
- 应用程序故障:应用程序本身也可能因为异常、崩溃或重启等原因而失去对TCP连接的跟踪,导致连接处于“挂起”状态。
- 负载均衡器或代理服务器:在客户端和服务器之间部署负载均衡器或代理服务器时,这些中间设备也可能因为超时或负载均衡策略而关闭空闲连接。
为了解决这些问题,心跳包(Heartbeat Packet)被引入。心跳包是一种定期发送的小数据包,用于在客户端和服务器之间保持连接的活跃状态,并通知对方自己仍然在线且愿意继续通信。通过发送心跳包,可以:
检测死连接:如果一方长时间没有收到对方的心跳包,则可以认为对方已经不可用,从而关闭连接并释放资源。
绕过NAT超时:定期的心跳包可以确保NAT表项不被超时删除,从而保持连接的活性。
保持负载均衡器或代理服务器的连接状态:类似地,心跳包也可以帮助维持负载均衡器或代理服务器中的连接状态,防止它们因为超时而关闭连接。
因此,尽管TCP连接本身是面向连接的,但在长连接场景中,心跳包仍然是保持连接活跃性和可靠性的重要手段之一。
基于以上知识的基础上我们来实现一个代码
socket server
代码说明:server代码中会为每个连接新建一个处理线程,可以达到异步执行的效果,对client意外断开的情况做了异常处理,可保证server不会退出。 在生产环境是可以使用的。 我们可以看到,每次client和服务端建立连接的时候,服务端都启动了一个线程进行处理,这在大部分非高并发的场景下已经足够用了。 但是缺点也因此很明显,那就是连接数过多的时候会无限制的创建线程,会对服务器造成很大的压力。当然这仅限于高并发场景才会出现这个问题。
import socket
import struct
import json
import time
import threading
import logging
logger = logging.getLogger("[SOCKET]")
logger.handlers.clear()
logger.setLevel(logging.DEBUG)
streamHandler = logging.StreamHandler()
streamHandler.setLevel(logging.DEBUG)
streamHandler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
logger.addHandler(streamHandler)
lock = threading.RLock
class SocketServer(object):
def __init__(self, host='127.0.0.1', port=9090):
self.host = host
self.port = port
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
def handle_client(self, client_socket, addr):
try:
while True:
time.sleep(0.01)
# 读取4字节头长
header_length = SocketServer._read_4(client_socket)
# 读取头
header = json.loads(SocketServer._read_header(client_socket, header_length).decode("utf-8"))
msg_type = header["msg_type"]
if msg_type == "heart":
logger.info(f"From {
addr}: header={
header}, 是心跳包")
elif msg_type == "data":
body_length = header["body_length"]
body = json.loads(SocketServer._read_body(client_socket, body_length).decode("utf-8"))
logger.info(f"From {
addr}: header={
header}, 数据包 data={
body}")
else:
logger.error(f"From {
addr}: Received Unknown msg_type")
except Exception as e:
logger.error(f"[SERVER] 客户端 {
addr} 出现异常: {
e}, 线程{
threading.current_thread().name} 退出 ")
finally:
client_socket.close()
def start(self):
self.server_socket.bind((self.host, self.port))
self.server_socket


282

被折叠的 条评论
为什么被折叠?



