Nginx高并发服务器底层逻辑分析-Epoll事件通知

Nginx高并发服务器底层逻辑分析-Epoll事件通知

  • 这里的epoll只能运行在linux,在其他系统上有改版的工具包
  • epoll能够高效运行的原因

    1. 事件监听工作方式
    2. 与操作系统共享内存,减少文件拷贝耗时
  • I/O 多路复用的特点:

    • 通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,epoll()函数就可以返回。 所以, IO多路复用,本质上不会有并发的功能,因为任何时候还是只有一个进程或线程进行工作,它之所以能提高效率是因为select\epoll 把进来的socket放到他们的 ‘监视’ 列表里面,当任何socket有可读可写数据立马处理,那如果select\epoll 手里同时检测着很多socket, 一有动静马上返回给进程处理,总比一个一个socket过来,阻塞等待,处理高效率。
    • 当然也可以多线程/多进程方式,一个连接过来开一个进程/线程处理,这样消耗的内存和进程切换页会耗掉更多的系统资源。 所以我们可以结合IO多路复用和多进程/多线程 来高性能并发,IO复用负责提高接受socket的通知效率,收到请求后,交给进程池/线程池来处理逻辑。

epoll工作过程图解

在这里插入图片描述

import sys
from socket import *
import select


class FLServer:

    def __init__(self, bind_ip, port, root_path):
        self.root_path = root_path
        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:
                        self.handle_request(client_socket, recv_data)
                    else:
                        # 关闭套接字
                        client_socket.close()
                        # 注销套接字对应的fd
                        epl.unregister(fd)
                        # 删除对应客户端的 事件描述与符套接字映射关系
                        fd_socket_map.pop(fd)

        self.tcp_socket.close

    def handle_request(self, server_socket, recv_data):
        lines = recv_data.decode().splitlines()
        file_path = lines[0].split(" ")[1]

        if file_path == "/":
            file_path = "/index.html"
        file_path = self.root_path + file_path
        print(f"资源路径为: {file_path}")

        try:
            rf = open(file_path, "rb")
        except IOError:
            response_header = "HTTP/1.1 404 not found\r\n"
            response_blank = "\r\n"
            response_body = "sorry, page not found".encode()
        else:
            response_body = rf.read()
            rf.close()

            response_header = "HTTP/1.1 200 OK\r\n"
            # 在请求头中添加body 长度,客户端收到指定长度数据后调用close, 关闭连接;
            # 服务器再关闭服务套接字实现长连接(使用一个套接字多次为客户端服务,由客户端先断开连接)服务
            response_header += f"Content-length: {len(response_body)}\r\n"
            response_blank = "\r\n"
        finally:
            response = (response_header + response_blank).encode() + response_body
            server_socket.send(response)


DOCUMENT_ROOT = "./html"


def main():
    script_params = sys.argv

    if len(script_params) == 3:
        ip, port = script_params[1:]
        if port.isdigit():
            port = int(port)
    else:
        print("请按如下方式运行服务器: python3 xxx.py 127.0.0.1 7890")
        return

    print(f"http服务器已启动:\n服务器IP地址: {ip}\n服务器端口号: {port}")
    http_server = FLServer(ip, port, DOCUMENT_ROOT)
    http_server.run_server()


if __name__ == '__main__':
    main()

### Nginxepoll 的工作原理 Nginx 使用 Linux 内核提供的 `epoll` 接口来实现高效的 I/O 多路复用机制。这种模型允许单个线程管理大量并发连接,从而显著提高性能[^1]。 #### epoll 工作流程概述 当 Nginx 启动时,会初始化并注册文件描述符到内核中的 `epoll` 实例上。每当有新的网络请求到达时,操作系统会通知 Nginx 哪些套接字已准备好读取或写入数据。这种方式避免了传统轮询方式带来的高 CPU 占用率问题[^2]。 具体来说,在 Nginx 源码中有一个名为 `ngx_event_epoll_module` 的模块专门用于处理基于 `epoll` 的事件驱动逻辑[^3]。该模块内部维护了一个红黑树结构的数据表,记录着所有正在被监控的 socket 连接状态变化情况。 ```c // 示例代码片段展示如何创建监听端口 ngx_listening_t * ngx_create_listening(ngx_conf_t *cf, struct sockaddr *sockaddr, socklen_t socklen) { ... ls = ngx_array_push(&cf->cycle->listening); ... } ``` 这段 C 语言函数展示了 Nginx 如何设置服务器监听端口的过程的一部分[^4]。 ### 配置 Nginx 使用 Epoll 实际上,默认情况下如果是在支持的操作系统环境下(如Linux),Nginx 就已经自动选择了最优的事件处理器——即对于大多数现代 Linux 发行版而言就是 `epoll` 。因此通常不需要特别指定配置项启用此功能。 不过为了确保使用的是 `epoll` ,可以在编译安装 Nginx 之前确认 configure 脚本选项里包含了 `--with-select_module --with-poll_module` 参数;尽管如此做主要是为了让那些不支持更先进技术的老平台也能正常工作而不是强制切换回旧有的 select 或 poll 方法。 另外值得注意的一点是,虽然可以通过调整 worker_processes 和 worker_connections 等参数间接影响到 `epoll` 表现,但这并不改变底层采用哪种多路复用机制的事实。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值