非阻塞套接字与IO多路复用
非阻塞套接字与IO
多路复用
非阻塞套接字
# 【本机环境运行】 # 01-TCP非堵塞通信.py # 使用 TCP调试助手作为客户端 import socket tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_socket.bind(("", 9000)) tcp_socket.listen(128) # 设置监听套接字为非堵塞模式 tcp_socket.setblocking(False) client_socket_list = [] while True: try: client_socket, client_addr = tcp_socket.accept() except Exception: # print("没有客户端接入") pass else: print(f"新的客户端{client_addr}到来") # 设置服务套接字为非堵塞模式 client_socket.setblocking(False) # 添加客户端到服务列表 client_socket_list.append((client_socket, client_addr)) # 遍历服务套接字<轮询>接收数据 for client, addr in client_socket_list: try: recv_data = client.recv(1024) except Exception as ret: # print("非堵塞未收到数据异常") pass else: # 客户端调用close if recv_data: print(f"{addr}: {recv_data.decode()}") else: print(f"客户端{addr}关闭") client.close() client_socket_list.remove((client, addr))
IO
多路复用epoll
epoll
工作过程图解
# 【虚拟机环境运行】 # 02-epoll实现并发服务器.py """ 这里的epoll只能运行在linux ,在其他系统上有改版的工具包 """ from socket import * import select class FLServer: def __init__(self, bind_ip, port): self.tcp_socket = socket(AF_INET, SOCK_STREAM) # 重复利用端口 self.tcp_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 主机作为客户端访问虚拟机,网卡只能使用仅主机模式 self.tcp_socket.bind((bind_ip, port)) self.tcp_socket.listen(128) def run_server(self): # 创建一个epoll对象 (创建一个操作系统和应用程序的共享内存) epl = select.epoll() # 将监听套接字对应的fd注册到epoll中(添加到共享内存) epl.register(self.tcp_socket.fileno(), select.EPOLLIN) # 定义服务套接字对应文件描述符和套接字的映射关系{fd: socket} fd_socket_map = {} while True: # os监测数据到来, 通过事件通知方式通知程序(解堵塞); 这里默认堵塞 events_list = epl.poll() # events_list 数据格式 [(fd, event), (套接字的文件对应描述符, 对应事件)...] for fd, event in events_list: # 监听套接字有输入(有新客户端连接) if fd == self.tcp_socket.fileno(): tcp_server_socket, client_addr = self.tcp_socket.accept() print(f"【新的客户{client_addr}到来】") # 获取服务套接字的文件描述符并注册到epoll server_socket_fp = tcp_server_socket.fileno() epl.register(server_socket_fp, select.EPOLLIN) # 添加服务套接字与描述符的映射关系到字典 fd_socket_map[server_socket_fp] = tcp_server_socket # print(fd_socket_map) # 服务器套接字接收到数据 elif event == select.EPOLLIN: # print(fd_socket_map) client_socket = fd_socket_map[fd] recv_data = client_socket.recv(1024) if recv_data: print(f"接收到数据: {recv_data.decode('gbk')}") else: # 关闭套接字 client_socket.close() # 注销套接字对应的fd epl.unregister(fd) # 删除对应客户端的 事件描述与符套接字映射关系 fd_socket_map.pop(fd) self.tcp_socket.close def main(): ip = "192.168.56.101" port = 8888 print(f"http服务器已启动:\n服务器IP地址: {ip}\n服务器端口号: {port}") http_server = FLServer(ip, port) http_server.run_server() if __name__ == '__main__': main()
- 这里的
epoll
只能运行在linux
,在其他系统上有改版的工具包
-
epoll
能够高效运行的原因- 事件监听工作方式
- 与操作系统共享内存,减少文件拷贝耗时
-
I/O 多路复用的特点:
- 通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,
epoll()
函数就可以返回。 所以, IO多路复用,本质上不会有并发的功能,因为任何时候还是只有一个进程或线程进行工作,它之所以能提高效率是因为select\epoll
把进来的socket放到他们的 '监视' 列表里面,当任何socket有可读可写数据立马处理,那如果select\epoll
手里同时检测着很多socket, 一有动静马上返回给进程处理,总比一个一个socket过来,阻塞等待,处理高效率。 - 当然也可以多线程/多进程方式,一个连接过来开一个进程/线程处理,这样消耗的内存和进程切换页会耗掉更多的系统资源。 所以我们可以结合IO多路复用和多进程/多线程 来高性能并发,IO复用负责提高接受socket的通知效率,收到请求后,交给进程池/线程池来处理逻辑。
- 通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,
-
epoll
在Linux
中的实现过程可参考 [epoll详解直达](
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?