在进行 Python socket TCP server 端编程时,需要在其运行时接收停止命令事件,停止整个服务程序。虽然这是不常见的需求,但实现起来颇有些周折,其中 accept 执行时的阻塞问题是关键所在。
一般情况下,Python Socket 的 accept 是阻塞执行的,它的阻塞能够屏蔽程序对CTRL-C的接收,也会阻止程序的退出。虽然可以用 settimeout 方法使所有操作进入超时或非阻塞模式(根据官方文档,这与操作系统的相关特性还有关系),但超时时间的选择也是比较两难的问题,时间短了不仅会影响其他操作(如recv),还会使程序在一定程度上变得复杂,处理量增加;时间长了又会使退出操作费时过长。CSDN有文章 给出了一种方法:建立一个主线程,生成一个 Socket 接受连接的子线程,主线程接收CTRL-C以后退出,子线程也随之退出,但经过测试,该方法对于我们接收命令事件退出的方式并不起作用。
为了解决这一问题,我们采用了一种方法,要点如下:
下面的程序是一个可以实际运行的程序,用延时替代停止命令,关键的处理方法见其中的 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