import os
import socket
import sys
import time
import threading
from loguru import logger
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures._base import Future
import multiprocessing

default_encoding: str = 'utf-8'

pool = ThreadPoolExecutor(
    max_workers=20,
    thread_name_prefix='simple-work-thread-pool'
)


def init_serversocket() -> socket.socket:
    serversocket = socket.socket(
        family=socket.AF_INET,
        type=socket.SOCK_STREAM
    )

    # 获取本地主机名
    host = socket.gethostname()

    logger.debug(f'host {host}')

    port = 6001

    # 绑定端口号
    serversocket.bind(('0.0.0.0', port))

    # 设置最大连接数,超过后排队
    serversocket.listen(2048)

    return serversocket


def send_response(clientsocket: socket.socket, addr: tuple, response_body: bytes) -> int:
    send_len: int = clientsocket.send(response_body)
    clientsocket.close()
    return send_len


def start_request(clientsocket: socket.socket, addr: tuple) -> int:
    try:
        pid = os.getpid()
        # logger.debug(f'pid: {pid}, get message from {addr}')
        request_body: bytes = clientsocket.recv(2048)
        request_text: str = request_body.decode(encoding=default_encoding)

        response_text: str = f'server get message: {request_text}'

        response_body: bytes = response_text.encode(default_encoding)
        # time.sleep(1)
        send_len = send_response(
            clientsocket=clientsocket, addr=addr, response_body=response_body)
        # logger.debug(f'发送了响应')
        return send_len
    except Exception as error:
        logger.exception(error)


def start_request_callback(future: Future) -> None:
    send_len: int = future.result()
    logger.debug(
        f'{threading.current_thread().name}, send payload len is {send_len}')


if __name__ == "__main__":
    serversocket = init_serversocket()

    pool = multiprocessing.Pool(processes=16)

    try:
        while True:
            clientsocket, addr = serversocket.accept()

            clientsocket: socket.socket
            addr: tuple

            # future: Future = pool.submit(start_request, clientsocket, addr)
            # future.add_done_callback(start_request_callback)

            pool.apply_async(start_request, (clientsocket, addr))
    finally:
        pool.close()
        pool.join()

写了上面的代码,用于实现一个master-slave 架构的 TCP 服务,但是遇到了一个,就是我 CTRL+C 关了和这个进程组之后,进程没有全部关闭,并且还一直占用这端口 6001

─➤  ps jfax | grep 002
  42535  120264  120263   42535 pts/13    120263 S+    1000   0:00  |                       |   |   \_ grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox 002
  23700   27002   27002   27002 pts/4      27002 Ss+   1000   0:00  |       \_ /usr/bin/zsh --login
      1  116522  102068   41388 pts/12     41388 S     1000   0:00 python 002_socket_tcp_server_process.py
      1  117004  116633   41388 pts/12     41388 S     1000   0:00 python 002_socket_tcp_client_process.py
      1  117005  116633   41388 pts/12     41388 S     1000   0:00 python 002_socket_tcp_client_process.py
      1  118556  117249   42535 pts/13    120263 S     1000   0:00 python 002_socket_tcp_client_process.py
(http_wsgi_asgi_tutorial) ╭─pon@amd5700g ~/Desktop/code/me/http_wsgi_asgi_tutorial  ‹master*› 
╰─➤  sudo lsof -i :6001                     
[sudo] pon 的密码: 
COMMAND    PID USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
python  116522  pon    3u  IPv4  8483065      0t0  TCP *:x11-1 (LISTEN)
python  116522  pon   48u  IPv4 12515823      0t0  TCP localhost:x11-1->localhost:60056 (CLOSE_WAIT)
python  116522  pon   53u  IPv4 12514260      0t0  TCP localhost:x11-1->localhost:60072 (CLOSE_WAIT)

这就很让人讨厌了

问了一下 chatGPT 解决方案:

Q:如何在TCP 服务端程序退出的时候,关闭全部TCP连接,释放端口资源?

A:在 TCP 服务端程序退出时,需要将所有的连接断开,释放占用的端口资源,以免产生端口占用问题。

具体的实现方式有多种,以下是一种常用的实现方式:

  1. 通过 socket.setsockopt() 函数设置 socket 选项 SO_REUSEADDR 和 SO_REUSEPORT,以允许端口重用。

    import socket
    
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    server_socket.bind(('localhost', 8080))
  2. 在 TCP 服务端程序退出时,先关闭监听套接字,然后循环遍历所有连接套接字,关闭连接并释放相关资源。

    server_socket.listen()
    
    # TCP 服务端程序退出时执行的清理代码
    def cleanup():
        # 关闭监听套接字
        server_socket.close()
    
        # 关闭所有连接
        for client_socket in clients:
            client_socket.close()
    
    try:
        while True:
            # 接受客户端连接
            client_socket, client_address = server_socket.accept()
            clients.append(client_socket)
            # 处理客户端请求
            handle_client(client_socket)
    except KeyboardInterrupt:
        # 当用户按下 Ctrl-C 时执行清理代码
        cleanup()
    except:
        # 发生异常时执行清理代码
        cleanup()
        raise

注意:在使用 SO_REUSEADDR 和 SO_REUSEPORT 选项时,应该在调用 bind() 函数之前设置这些选项。如果在 bind() 函数之后设置这些选项,可能会导致 bind() 函数失败。


上诉文章内容错误, 真实原因参考:https://segmentfault.com/q/1010000043750446


universe_king
3.4k 声望680 粉丝