Python socket长连接、心跳、重连、自定义二进制数据报文

什么是socket短连接、长连接?

  短连接就是socket客户端与服务端建立一个连接,在收发完数据后就立刻关闭与服务端的连接,如果需要进行下一次请求,则需要重新连接服务端。socket短连接适用于客户端与服务端交互并不是很频繁的业务场景。

  长连接则是在建立socket连接后,一直保持连接,不关闭连接,可以持续收发数据包。socket长连接适用于客户端与服务端交互频繁、实时性很强的业务场景。

长连接和短连接的优缺点

  长连接的优势是服务端可以主动推送数据给客户端,数据实时性强。其缺点是:由于长连接一直要与服务器保持连接,需要持续消耗服务器资源,对服务器的压力比较大,服务器能承受的长连接数较低。

  短连接则是根据客户端需要连接服务器,用完即关闭连接,释放服务器资源,对服务器压力较小。但是缺点是服务端无法主动推送数据给客户端,而只能通过轮循的方式连接并请求服务器。客户端轮循请求服务器也是有弊端的,不管服务器是否有新数据要发送给客户端,客户端都要定时连接服务器,对服务器资源一样也是一种浪费。

  至于怎么选择,还是需要根据业务需求来确定。

心跳机制  

  心跳机制一般是用于socket长连接的,要保持长连接就需要引入心跳机制。由于网络或未知原因,客户端与服务端的连接可能会断开,这时候我们需要重新连接服务端。那么我们怎么知道连接什么时候断开了呢?这就需要使用心跳消息了,定时向对方发送消息,通过请求应答消息来确认连接是否正常。

  可能有的人会说,连接断开,程序会报错的,还需要定时检测吗?是的,物理网络连接正常的情况下,如果对方正常关闭,程序的确会抛出网络异常,我们捕捉到异常后尝试重新连接就可以。但是还有某些情况,程序是无法检测到网络断开的。如果某一端的网线被拔掉或程序崩溃死机,另一端是不会感知到连接已经断开的。另外在互联网中,客户端与服务端之间的通信可能是由多层路由转发的,如果中间某一链路断开,客户端是无法感知连接断开的,也不会报任何错误。这样客户端还以为自己连接正常,一直傻傻地等待服务器给它推送数据,其实与服务器的连接早就断开了。

  还有人说,TCP连接不是有keepalive机制吗?还需要我们自己在应用层上去实现心跳吗?TCP的keepalive机制是靠操作系统去实现的,在不同的系统上表现也有所不同。很多操作系统默认是2小时超时检测,这么长时间黄花菜都凉了。如果TCP连接使用了代理或NAT,也可能不会处理TCP的keepalive包。在应用层实现心跳更可靠,它不仅可以保证连接是活的,还可以确认应用是否正常工作。

  心跳分为单向心跳和双向心跳,而单向心跳又分为服务器端单向心跳和客户端单向心跳。服务器端使用单向心跳是服务器定时向客户端发送消息,以确认客户端的连接是否仍然有效,如果连接无效则释放掉相关的资源,避免浪费服务器资源。客户端使用单向心跳是客户端定时向服务端发送消息,以确认客户端与服务端连接是否正常,如果不正常则重新连接服务端或重新连接到另一台服务器。而双向心跳则是客户端和服务端都定时往对方发送心跳消息,互相检测连接是否正常。

  下面我贴一段代码,这段代码实现了长连接和客户端单向心跳以及重连功能。如果有需要可以进一步改成双向心跳。数据通信采用的是自定义二进制数据结构,可以传输任何类型的数据,包括文本、图像、文件等数据。也可以做为上位机代码使用,与硬件设备进行通信。

服务端代码:

import time
import socket
import Message
import threading
from struct import unpack


# 字节转字符串
def bytes2str(bytes):
    return bytes.decode('utf-8')


class Server:
    def __init__(self):
        # 服务器ip与端口
        self.server_address = ('127.0.0.1', 9089)

    def start(self):
        # 监听端口
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
            sock.bind(self.server_address)
            sock.listen(1024)
            print(f'Server started on {self.server_address}')

            while True:
                try:
                    client_socket, addr = sock.accept()
                    handler = RequestHandler(client_socket, addr)
                    thread = threading.Thread(target=handler.run, args=())
                    thread.start()
                except Exception as ex:
                    print(ex)


class RequestHandler:
    def __init__(self, sock, client_address):
        self.sock = sock
        self.client_address = client_address

    def run(self):
        try:
            while True:
                struct_bytes = self.sock.recv(4)  # 读取数据长度(4字节)
                data_size, = unpack('!I', struct_bytes)  # 解struct包,转换为数据长度

                data = b''  # 已接收的数据
                recv_size = 0  # 接收数据大小
                buff_size = 8192  # 接收缓冲区大小
                while recv_size < data_size:
                    remain_size = data_size - recv_size  # 计算剩余数据长度
                    if remain_size < buff_size:  # 如果剩余数据长度小于缓冲区长度时,设置缓冲区长度为剩余数据长度
                        buff_size = remain_size
                    recv_data = self.sock.recv(buff_size)  # 接收数据
                    data += recv_data  # 数据累加
                    recv_size = len(data)  # 计算已接收的数据长度

                if recv_size == data_size:  # 当数据接收完成时,对数据进行解析和处理
                    print(f'recv data from {self.client_address}, data_size:', len(data))
                    msgid, = unpack('!b', data[:1])  # 读取消息ID,根据消息ID区分处理
                    match msgid:
                        case Message.MSG_ID_REGISTER:
                            appid = bytes2str(data[1:37])
                            imei = bytes2str(data[37:52])
                            print(f'appid: {appid}, imei: {imei}')
                            error_code = 0  # 错误码 0正常 -1错误
                            response = Message.pack('!I2b', 2, msgid, error_code)
                            self.sock.sendall(response)
                        case Message.MSG_ID_HEARTBEAT:
                            error_code = 0  # 错误码 0正常 -1错误
                            response = Message.pack('!I2b', 2, msgid, error_code)
                            self.sock.sendall(response)
                        case Message.MSG_ID_DATETIME:
                            strtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
                            str_len = len(strtime)
                            error_code = 0  # 错误码 0正常 -1错误
                            response = Message.pack(f'!I2b{str_len}s', 2 + str_len, msgid, error_code, strtime)
                            self.sock.sendall(response)

        except Exception as ex:
            print(f"{ex} {self.client_address}")


if __name__ == '__main__':
    Server().start()

客户端代码:

import socket
import struct
import threading
import time
from multiprocessing import JoinableQueue

import Message


class Client:
    def __init__(self, host, port):
        self.sock = None
        self.writer = None
        self.reader = None
        self.heartbeat_thread = None
        self.server_address = (host, port)

    def connect(self):
        try:
            print(f'Connecting to server{self.server_address}.')
            # 与服务端建立socket连接
            self.sock = socket.create_connection(self.server_address)

            # 创建并启动网络数据读取线程
            self.reader = ReaderThread(self)
            self.reader.start()

            # 创建并启动网络数据写入线程
            self.writer = WriterThread(self)
            self.writer.start()

            # 启动心跳线程
            if not self.heartbeat_thread:
                self.heartbeat_thread = HeartbeatThread(self)
                self.heartbeat_thread.start()
            print('Connected.')
        except Exception as e:
            print(f'Connection refused. {e}')

    def disconnect(self):
        print('Disconnected from server.')
        if self.reader:
            self.reader.dispose()
            self.reader = None

        if self.writer:
            self.writer.dispose()
            self.writer = None

        self.sock.close()

    # 注册连接
    def register(self):
        appid = '996da38d-a7be-4947-aa4c-f8208b5f4ade'
        imei = '869858030720693'
        self.writer.send(Message.msg_register(appid, imei))

    # 请求日期时间
    def request_datetime(self):
        self.writer.send(Message.msg_datetime())


# 心跳线程
class HeartbeatThread(threading.Thread):
    def __init__(self, cli):
        threading.Thread.__init__(self)
        self.cli = cli

    def run(self) -> None:
        while True:
            if self.cli.writer:
                self.cli.writer.send(Message.msg_heartbeat())
            else:
                self.cli.connect()
            time.sleep(3)


# 数据读取线程
class ReaderThread(threading.Thread):
    def __init__(self, cli):
        threading.Thread.__init__(self)
        self.cli = cli
        self.interrupt = False

    def run(self) -> None:

        while not self.interrupt:
            try:
                struct_bytes = self.cli.sock.recv(4)  # 数据总长度
                data_size, = struct.unpack('!I', struct_bytes)  # 解struct包

                data = b''  # 已接收的数据
                recv_size = 0  # 接收数据大小
                buff_size = 8192  # 接收缓冲区大小
                while recv_size < data_size:
                    remain_size = data_size - recv_size
                    if remain_size < buff_size:
                        buff_size = remain_size
                    recv_data = self.cli.sock.recv(buff_size)
                    data += recv_data
                    recv_size = len(data)

                if recv_size == data_size:
                    print(f'recv data: {data}, size:{len(data)}')
                    msgid, = struct.unpack('!b', data[:1])
                    error_code, = struct.unpack('!b', data[1:2])
                    print(f'msgid:{msgid}, error_code:{error_code}')

                    match msgid:
                        case Message.MSG_ID_DATETIME:
                            date_time = data[2:].decode('utf-8')
                            print(date_time)

            except Exception as e:
                print(e)
                self.cli.disconnect()
                break

    def dispose(self):
        self.interrupt = True


# 数据写入线程
class WriterThread(threading.Thread):
    def __init__(self, cli):
        threading.Thread.__init__(self)
        self.cli = cli
        self.interrupt = False
        self.queue = JoinableQueue(1024)

    def run(self) -> None:
        while not self.interrupt:
            try:
                data = self.queue.get()
                self.cli.sock.sendall(data)
            except Exception as e:
                print(e)
                self.cli.disconnect()
                break

    # 数据长度(4字节,不包括本身) + 数据(不定长), 大端网络字节序
    def send(self, data):
        if self.queue:
            print(f'send data: {data}, size:{len(data)}')
            data_size = struct.pack('!I', len(data))
            self.queue.put(data_size + data)

    def dispose(self):
        self.interrupt = True
        self.queue = None


if __name__ == '__main__':
    client = Client('127.0.0.1', 9089)
    client.connect()
    client.register()
    client.request_datetime()

消息定义代码:

import struct

# 注册消息ID
MSG_ID_REGISTER = 0x00
# 心跳消息ID
MSG_ID_HEARTBEAT = 0x01
# 时间消息ID
MSG_ID_DATETIME = 0x02


def pack(fmt, *args):
    args_list = list(args)
    for i, arg in enumerate(args_list):
        if isinstance(arg, str):
            args_list[i] = arg.encode('utf-8')
    return struct.pack(fmt, *args_list)


# 组装注册消息
def msg_register(appid, imei):
    return pack('!b36s15s', MSG_ID_REGISTER, appid, imei)


# 组装心跳消息
def msg_heartbeat():
    return pack('!b', MSG_ID_HEARTBEAT)


# 组装时间消息
def msg_datetime():
    return pack('!b', MSG_ID_DATETIME)

  • 3
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值