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 服务端程序退出时,须要将所有的连贯断开,开释占用的端口资源,免得产生端口占用问题。
具体的实现形式有多种,以下是一种罕用的实现形式:
-
通过 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))
-
在 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() 函数失败。