非阻塞套接字与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详解直达](