施洪宝
一. 基础
nginx 源码采用 1.15.5
后续部分仅讨论 http 中的 listen 配置解析以及优化流程
1.1 概述
假设 nginx http 模块的配置如下
http{
server {
listen 127.0.0.1:8000;
server_name www.baidu.com;
root html;
location /{
index index.html;
}
}
server {
listen 10.0.1.1:8000;
server_name www.news.baidu.com;
root html;
location /{
index index.html;
}
}
server {
listen 8000; #相当于 0.0.0.0:8000
server_name www.tieba.baidu.com;
root html;
location /{
index index.html;
}
}
server {
listen 127.0.0.1:8000;
server_name www.zhidao.baidu.com;
location / {
root html;
index index.html;
}
}
}
端口, 地址, server 的关系
端口是指一个端口号, 例如上面的 8000 端口
地址是 ip+port, 例如 127.0.0.1:8000, 10.0.1.1:8000, 0.0.0.0:8000, listen 后配置的是一个地址。
每个地址可以放到多个 server 中, 例如上面的 127.0.0.1:8000
总而言之, 一个端口可以有多个地址, 每个地址可以有多个 server
1.2 存在的问题
是否需要在读取完 http 块中所有的 server 才能建立监听套接字, 绑定监听地址?
是的, 因为允许配置通配地址, 故而必须将 http 块中的 server 全部读取完后, 才能知道如何建立监听套接字。
一个端口可以对应多个地址, 如何建立监听套接字, 如何绑定地址?
通常情况下, 每个地址只能绑定一次(只考虑 tcp 协议), 这种情况下, 我们只能选择部分地址创建监听套接字, 绑定监听地址。
当配置中存在通配地址 (0.0.0.0:port) 时, 只需要创建一个监听套接字, 绑定这个通配地址即可, 但需要能够依据该监听套接字找到该端口配置的其他地址, 这样当客户端发送请求时, 可以根据客户端请求的地址, 找到对应地址下的相关配置。
当配置中不存在通配地址时, 需要对每个地址都创建一个监听套接字, 绑定监听地址。
一个地址多个 server 的情况下, 如何快速找到客户端请求的 server?
比较合适的方案是通过 hash 表。
为了快速找到客户端请求的 server, nginx 以 server_name 为 key, 每个 server 块的配置 (可以理解为一个指针, 该指针指向整个 server 块的配置) 为 value, 放入到哈希表。
由于 server_name 中可以出现正则匹配等情况, nginx 将 server_name 具体分为 4 类进行分别处理(www.baidu.com, *baidu.com, www.baidu*, ~*baidu)。
1.3 nginx listen 解析的流程
总体而言分为 2 步,
将所有 http 模块内的配置解析完成, 将 listen 的相关配置暂存(主要存储监听端口以及监听地址)。
根据上一步暂存的监听端口以及监听地址, 创建监听套接字, 绑定监听地址
二. 配置解析
nginx http 块解析完成后, 会存储配置文件中配置的监听端口以及监听地址, 其核心结构图如下, 总体而言, 结构可以分为 3 级, 端口 -> 地址 ->server
2.1 源码
listen 的处理流程:
ngx_http_core_listen: 读取配置文件配置
ngx_http_add_listen: 查看之前是否出现过当前监听的端口, 没有则新建, 否则追加
ngx_http_add_address: 查看之前该端口下是否监听过该地址, 没有则新建, 否则追加。
ngx_http_add_server: 查看 server 之前是否出现过, 没有则新建, 否则报错(重复定义)。
三. 创建监听套接字
nginx 最终创建的监听套接字及其相关的结构图如下,
每个 ngx_listening_t 结构对应一个监听套接字, 绑定一个监听地址
每个 ngx_listening_t 结构后面需要存储地址信息, 地址可能不止一个, 因为这个监听套接字可能绑定的是通配地址, 这个端口下的其他地址都会放在这个监听套接字下。例如, 1.1 节的配置中, 只会创建一个 ngx_listening_t 结构, 其他地址的配置都会放到这个通配地址下。
每个监听地址可能对应多个域名(配置文件中的 server_name), 需要将这些域名放到哈希表中, 以供后续使用
总体而言, 结构分为 3 级, 监听套接字 -> 监听地址 ->server
3.1 源码
读取完 http 块后, 需要创建监听套接字绑定监听地址, 处理函数 ngx_http_optimize_servers, 该函数的处理流程:
遍历所有监听端口, 针对每个监听端口, 执行以下 3 步
对该端口下所有监听地址排序(listen 后配置 bind 的放在前面, 通配地址放在后面)
遍历该端口下的所有地址, 将每个地址配置的所有 server, 放到该地址的哈希表中。
为该端口建立监听套接字, 绑定监听地址。
四. 监听套接字的使用
假设此处我们使用 epoll 作为事件处理模块
epoll 在增加事件时, 用户可以使用 epoll_event 中的 data 字段, 当事件发生时, 该字段也会带回。
nginx 中的 epoll_event 指向的是 ngx_connection_t 结构, 事件发生时, 调用 ngx_connection_t 结构中的读写事件, 负责具体处理事件, 参见下图。
//c is ngx_connection_t
rev = c->read;
rev->hadler(rev);
wev = c->write;
wev->handler(wev);
每个监听套接字对应一个 ngx_connection_t, 该结构的读事件回调函数为 ngx_event_accept, 当用户发起 tcp 握手时, 通过 ngx_event_accept 接受客户端的连接请求。
ngx_event_accept 会接受客户端请求, 初始化一个新的 ngx_connection_t 结构, 并将其加入到 epoll 中进行监听, 最后会调用 ngx_connection_t 对应的 ngx_listening_t 的处理函数(http 块对应 ngx_http_init_connection, mail 块 ngx_mail_init_connection, stream 块对应 ngx_stream_init_connection)
五. 总结
nginx 在读取 listen 相关的配置时, 将结构分为 3 级, 端口 -> 地址 ->server, 各级都是一对多的关系。
nginx 在创建监听套接字时, 将结构分为 3 级, 监听套接字 -> 地址 ->server, 各级都是一对多的关系。