Python socket 编程中 accept 阻塞问题的一种解决方法

Python socket 编程中 accept 阻塞问题的一种解决方法

        在进行 Python socket TCP server 端编程时,需要在其运行时接收停止命令事件,停止整个服务程序。虽然这是不常见的需求,但实现起来颇有些周折,其中 accept 执行时的阻塞问题是关键所在。

        一般情况下,Python Socket 的 accept 是阻塞执行的,它的阻塞能够屏蔽程序对CTRL-C的接收,也会阻止程序的退出。虽然可以用 settimeout 方法使所有操作进入超时或非阻塞模式(根据官方文档,这与操作系统的相关特性还有关系),但超时时间的选择也是比较两难的问题,时间短了不仅会影响其他操作(如recv),还会使程序在一定程度上变得复杂,处理量增加;时间长了又会使退出操作费时过长。CSDN有文章 给出了一种方法:建立一个主线程,生成一个 Socket 接受连接的子线程,主线程接收CTRL-C以后退出,子线程也随之退出,但经过测试,该方法对于我们接收命令事件退出的方式并不起作用。

       为了解决这一问题,我们采用了一种方法,要点如下:

  1. 设置一个全局循环变量(下面程序中的 local_var.server_on),控制服务端的 accept 循环,当它为 False 时,退出循环;
  2. 在程序中定义一个 TCP Socket 客户端的函数(下面程序中的 run_client() 函数),该函数连接本服务端一次,然后关闭连接;
  3. 程序启动时,该变量初始化为True,使循环得以进行;
  4. 在接收到退出命令事件后,首先将上述循环变量置为False;
  5. 设置循环变量以后,调用一次上述 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

上一篇:Http中Content-Type与Accept的区别


下一篇:Tomcat入门及http简介