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. 程序启动时,循环变量(local_var.server_on)初始化为True,使循环得以进行;
  4. 在接收到退出命令事件后,首先将上述循环变量置为False;
  5. 设置循环变量以后,调用一次 TCP 客户端函数(run_client),使服务端的 accept 退出阻塞;

       这种方法对阻塞或非阻塞方式都是有效的。如果不启动模仿接收停止命令事件的 control_timer 线程,它就是普通的永远运行的 TCP 服务端程序。

       下面的程序是一个可以实际运行的程序,用延时的方式仿真停止命令的处理,关键的处理方法见其中的 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
            else:
				print(s)
        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()

下面是测试用的驱动程序,一个简单的客户端程序。启动后,该程序连接服务器,接收一次回复后即退出。如果需要测试服务端的接收功能,可以把中间注释掉的发送部分恢复。

mport socket
import time
import sys

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

host = 'localhost'
port = 9999

s.connect((host, port))
message = s.recv(1024)

# for i in range(10):
#    s.sendall(b'hello')
#    time.sleep(2)

s.close()

print(message.decode('utf-8'))

下面是运行结果,开始运行 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

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值