Python socket 编程中 accept 阻塞问题的一种解决方法
在进行 Python socket TCP server 端编程时,需要在其运行时接收停止命令事件,停止整个服务程序。虽然这是不常见的需求,但实现起来颇有些周折,其中 accept 执行时的阻塞问题是关键所在。
一般情况下,Python Socket 的 accept 是阻塞执行的,它的阻塞能够屏蔽程序对CTRL-C的接收,也会阻止程序的退出。虽然可以用 settimeout 方法使所有操作进入超时或非阻塞模式(根据官方文档,这与操作系统的相关特性还有关系),但超时时间的选择也是比较两难的问题,时间短了不仅会影响其他操作(如recv),还会使程序在一定程度上变得复杂,处理量增加;时间长了又会使退出操作费时过长。CSDN有文章 给出了一种方法:建立一个主线程,生成一个 Socket 接受连接的子线程,主线程接收CTRL-C以后退出,子线程也随之退出,但经过测试,该方法对于我们接收命令事件退出的方式并不起作用。
为了解决这一问题,我们采用了一种方法,要点如下:
- 设置一个全局循环变量(下面程序中的 local_var.server_on),控制服务端的 accept 循环,当它为 False 时,退出循环;
- 在程序中定义一个 TCP Socket 客户端的函数(下面程序中的 run_client() 函数),该函数连接本服务端一次,然后关闭连接;
- 程序启动时,该变量初始化为True,使循环得以进行;
- 在接收到退出命令事件后,首先将上述循环变量置为False;
- 设置循环变量以后,调用一次上述 run_client 函数,作用是使服务端的 accept 退出阻塞;
下面的程序是一个可以实际运行的程序,用延时替代停止命令,关键的处理方法见其中的 control_timer() 函数,它作为一个线程运行,程序启动后,延时 20 秒后进入上述的 TCP 服务端停止处理。最后处理的是接收线程(receive_threading() 线程实例),服务端 run_tcp_server() 每接受一个连接都会生成响应的接收线程,退出时也一并清理。
其中 local_var.py (import local_var)只是定义了一些全局变量,为避免冗余,本文未给出其源代码。
import socket
import local_var
import threading
import time
# import sys
def create_socket():
local_var.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = '0.0.0.0'
port = 9999
local_var.server_socket.bind((host, port))
print('timeout time:', local_var.server_socket.gettimeout())
local_var.server_socket.listen(10)
# 这个函数在退出时运行一次,以防止 run_tcp_server 因 accept 阻塞而无法退出
def run_client():
local_var.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = 'localhost'
print(host)
port = 9999
try:
local_var.client_socket.connect((host, port))
local_var.client_socket.close()
except Exception as e:
print('except at run client: ', e)
def run_tcp_server():
create_socket()
while local_var.server_on:
try:
client_socket, addr = local_var.server_socket.accept()
except socket.timeout:
print('socket time out!')
continue
local_var.connect_list[addr] = {'socket': client_socket, 'in_listen': True}
t = threading.Thread(target=receive_threading, args=(addr,))
try:
t.start()
except Exception as e:
print('except on run_tcp_server 2: ', e)
client_socket.close()
print(f'Client address: {addr}')
msg = 'Hello client!' + '\r\n'
try:
client_socket.send(msg.encode('utf-8'))
except Exception as e:
print('except on run_tcp_server 2: ', e)
# client_socket.close()
def receive_threading(the_addr):
in_listen = local_var.connect_list[the_addr]['in_listen']
the_socket = local_var.connect_list[the_addr]['socket']
while in_listen:
try:
s = the_socket.recv(1000)
print(s)
if len(s) == 0:
the_socket.close()
local_var.connect_list.pop(the_addr)
in_listen = False
except Exception as e:
print(f'except on receive_threading: address{the_addr}', e)
the_socket.close()
local_var.connect_list.pop(the_addr)
in_listen = False
def control_timer():
# 设置定时,模仿退出命令事件的输入
time.sleep(20)
print('time over!')
# 改变循环变量,使得 run_tcp_server() 的循环退出
local_var.server_on = False
# 运行一下client端连接,保证 run_tcp_server() 的 accept 退出阻塞
run_client()
# 停止所有接收子线程(receive_threading),这时不怕 recv 函数阻塞,因为服务端将退出,从而使 recv 产生异常
for i in local_var.connect_list.keys():
local_var.connect_list[i]['in_listen'] = False
# local_var.control_queue.put('stop')
# print(local_var.control_queue.qsize())
if __name__ == '__main__':
local_var.server_on = True
t_outside = threading.Thread(target=run_tcp_server)
t_outside.start()
t_timer = threading.Thread(target=control_timer)
t_timer.start()
t_outside.join()
t_timer.join()
下面是运行结果,开始运行 20 秒后退出,控制台打印出的 ”timeout time:None“ 表示 socket 运行在阻塞模式,其间接受过两次外进程的连接,最后打出的一个 “Client address:…" 字符串是 run_client() 函数执行的结果:
C:\Users\A\AppData\Local\Programs\Python\Python37\python.exe C:/Users/A/OneDrive/文档/Python/try_tcp_stream/socket_server.py
timeout time: None
Client address: ('127.0.0.1', 50874)
b''
Client address: ('127.0.0.1', 50880)
b''
time over!
localhost
b''Client address: ('127.0.0.1', 50898)
Process finished with exit code 0