33.python socket

为什么写这篇文章

因为你现在网上搜索的很多demo代码是不可以在生产环境中用的, 先来说一下最常见的误解:

  1. conn.recv(1024) 并不是说一定会收到1024个字节,而是尝试获取1024个字节的数据,但是不保证一定拿到。 加入你client发送了一个完整的数据,正好是1024个字节,然后server调用conn.recv(1024), 你将获取的结果打印出来,你会发现卧槽,怎么只打印了一半。 因此要想完整收到指定数量字节必须用while True, 请仔细参考下方的read函数。
  2. 关于流边界
    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发送流程

  1. client 先发送 4个字节的一个数字
    这个数字表示,接下来的 header的长度是多少字节
    假设
    header={
    “msg_type”:‘body’
    “data_length”:123
    }
    我们把header转成byte之后计算它的长度,这肯定是个数字,把这个数字发送到server
  2. client 在把header发送到server端
  3. client把body 发送到server端

server解析数据

  1. server 先接收4字节的数字
    假设这个数字为 133, 这个表示接下来socket传输的133个字节是header
  2. 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我先森

鼓励一个吧,哈哈

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值