HTTP服务器server架构分析—StateThreads示例程序介绍

HTTP服务器 server 程序 的主要流程如下:

server 程序其实有多个模块的,如下:

1,create_listeners() 创建 listening sockets 列表

server 程序其实是可以监听多个 地址的,如下:

bash 复制代码
./server -l ./log -p 5 -b 192.168.0.109:8888 -b 127.0.0.1:1234

上面就监听了两个地址,所以 create_listeners() 函数内部会创建 2 个 listening sockets,这是存储在数组 srv_socket[] 里面的,sk_copunt 就是这个数组的长度,也就是 2。

监听 2 个地址 比 监听 1 个地址的协程数量多一倍,如下:


2,start_processes() 开启子进程处理 HTTP 请求

start_processes() 内部会开启多个子进程来处理 HTTP 请求,如下:

fork() 函数的逻辑是有点复杂的,fork() 其实会产生一个新的逻辑分支。你可以理解 fork() 返回了两次,当返回值 pid 等于 0 ,代表这是子进程的逻辑分支。当 返回值 pid 大于 0,代表这是原来 父进程的逻辑分支,pid 就是 子进程的进程ID。

-p 等于 5 的时候,父进程的逻辑分支一直在 for 循环里面循环了 5 次,而 创建出来的子进程 立即就退出了 start_process() 函数了,子进程会跑到 install_sighandlers() 的流水线继续去执行。

当父进程 循环完 for(), 开启完 5 个子进程去处理 HTTP 请求之后,父级进程自己会变成一个 Watch Dog 进程,负责监控各个子进程的运作状态。

他使用的是 wait() 函数,这个函数一开始会阻塞,一直等到有子进程退出,wait() 就会返回退出的子进程的进程ID 以及 退出状态。而 父进程 看到有子进程退出了,就会重新创建一个子进程来处理 HTTP 请求。

我的服务器开了 5 个 server 的 HTTP 进程,现在我把其中一个 26883 进程 kill 掉。会发现,会再有一个新的进程 26701 重新创建处理,如下:

bash 复制代码
kill -9 26683

这就是父级进程的 Watch Dog 功能。在 Linux 环境,有一个软件也可以实现这种 看门人 功能,就是 supervisor


3,start_threads() 开启多协程处理 HTTP 请求

start_threads() 其实已经处于 子进程的流水线,start_threads() 内部会创建多个 handle_connections() 协程,全部阻塞在 st_accept() 里面等待浏览器的 HTTP 请求。

上图是单进程的流程,一个进程里面多个 handle_connections() 协程,内部全部都是 st_accept() 同一个 listening socket 的 ,当浏览器请求来的时候,只有一个 handle_connections() 协程被激活,然后去处理请求。

这里剧透一下 st_accept() 函数的一些内部原理,虽然是多个 协程全部 st_accept() 同一个 socket,但是其实内部只有一次 select/epoll_wait 调用。因为多个协程是共用一个 全局的 Socket 集合 的。

select/epoll_wait 的调用如下:

ini 复制代码
// event.c 第 331 行
nfd = select(_ST_SELECT_MAX_OSFD + 1, rp, wp, ep, tvp);
rust 复制代码
// event.c 第 1268 行
nfd = epoll_wait(_st_epoll_data->epfd, _st_epoll_data->evtlist, _st_epoll_data->evtlist_size, timeout);

select/epoll_wait 被激活的时候,就会选取最先执行 st_accept() 的那个协程来处理这次的请求。


上面单进程的情况,但是 server 程序本身是有 多个进程的,前面的 create_listeners() 创建 listening sockets ,然后再执行 fork() 创建子进程的,进程本身是内存隔离的,所以 fork() 会复制多块父级进程的内存。fork() 的内存复制是写时复制。

但是 select/epoll_wait 系统调用 本身是支持多进程同时 select/epoll_wait 同一个 listening sockets 的,也是只会激活其中一个进程来处理这个请求。


整体的逻辑就是 5 个 server 进程同时 select() 监听同一个 listening sockets,当浏览器请求来的时候,就激活其中一个进程来处理,假设是进程 26683。

然后进程 26883 会激活最先执行 st_accept() 的那个协程来处理这次的请求


4,max_threads 控制协程数量

server 会通过 max_threadsmax_wait_threadsmin_wait_threads 控制每一个 listening socket 的协程数量,以下面的命令为例。

bash 复制代码
./server -l ./log -t 20:50 -p 5 -b 192.168.0.109:8888 -b 127.0.0.1:1234

上面监听了两个地址,所有会有两个 listening socket,这时候 sk_count 等于 2 ,srv_socket[] 数组的长度就是 2

同时使用了 -t 选项,-t 是用来指定 max_wait_threadsmax_threads 的。上面的命令,我设置了 max_wait_threads 等于 20,max_threads 等于 50。

max_threads 不是指单个进程里面的最大协程,而是指所有进程加起来的最大协程,所以上面是每个进程最多开 10 个协程。

max_wait_threads 也是指所有进程的最大空闲协程的数量,所以上面每个进程最多有 4 个协程是空闲的。


控制协程数量的策略有点复杂的,如下:

上面这段代码的意思是,当 start_threads() 函数执行完之后 ,就会有创建 4 个空闲协程出来阻塞在 st_accept() 里。这时候一个进程里面就有 4 个协程,而且全部都是空闲的。假设这些协程的编号位 1 2 3 4

然后当浏览器发送一个 HTTP 请求来的时候,协程 1 就会激活,协程 1 负责调 handle_session() 处理这个 HTTP 请求。但是在此之前,协程1 会再次创建一个 空闲协程5 来处理,协程5 会阻塞在 st_accept() 负责接受新的请求。

此刻 协程1 不是空闲协程了,而是在忙碌处理 HTTP 请求。空闲协程是 2~5。所以,他的策略是尽量保证有 4 个空闲协程在等待 HTTP 请求到来。

再假设另一个例子,浏览器一下子发送 4 个 HTTP 请求,那协程 14 都会变成 busy 协程,但是也会重新创建 5 8 的空闲协程处理,所以此刻总协程数量 TOTAL_THREADS 就是 8 个协程。

TOTAL_THREADS 不是无限的,当达到 我们前面设置的 10 个的时候,就不会再创建空闲协程。

所以,如果浏览器一下子发送 12 个 HTTP 请求,假设只有一个 server 进程,max_threads 等于 10,那 10 个协程都会变成 busy,空闲协程的数量是 0 个。

而且最后两个 HTTP 请求要后面才能处理。这就是 server 程序的协程数量控制策略。


上面我们用 -t 选项指定了 max_wait_threadsmax_threads ,这样其实不是太好。通常我们不需要使用 -t ,让他根据服务器的性能自动选择合适的协程数量即可。

他默认的协程数量策略如下:

ini 复制代码
if (max_wait_threads == 0)
    max_wait_threads = MAX_WAIT_THREADS_DEFAULT * vp_count;
/* Assuming that each client session needs FD_PER_THREAD file descriptors */
if (max_threads == 0)
    max_threads = (st_getfdlimit() * vp_count) / FD_PER_THREAD / sk_count;

MAX_WAIT_THREADS_DEFAULT 等于 0 ,所以他默认是每个进程里面最多有 8 个空闲协程。

st_getfdlimit() 获取的是事件驱动能支持的最大 socket fd,select() 函数支持的是 1024 个 fd。而 FD_PER_THREAD 等于 2,就是每个协程负责处理 2 个请求。vp_count 是 server 进程的数量,sk_count 是监听地址的数量

所以每个进程默认最多开 1280 个协程,如下:

ini 复制代码
max_threads = (1024 * 5) / 2 / 2 = 1280

本文是《 SRS原理 》一书中的文章,如需观看更多内容,请购买本书。

相关推荐
x007xyz1 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
音视频牛哥1 个月前
Android摄像头采集选Camera1还是Camera2?
音视频开发·视频编码·直播
九酒2 个月前
【harmonyOS NEXT 下的前端开发者】WAV音频编码实现
前端·harmonyos·音视频开发
音视频牛哥2 个月前
结合GB/T28181规范探讨Android平台设备接入模块心跳实现
音视频开发·视频编码·直播
哔哩哔哩技术2 个月前
自研点直播转码核心
音视频开发
音视频牛哥2 个月前
Android平台轻量级RTSP服务模块二次封装版调用说明
音视频开发·视频编码·直播
音视频牛哥2 个月前
Android平台RTSP|RTMP直播播放器技术接入说明
音视频开发·视频编码·直播
山雨楼2 个月前
ExoPlayer架构详解与源码分析(15)——Renderer
android·架构·音视频开发
音视频牛哥2 个月前
Windows平台如何实现多路RTSP|RTMP流合成后录像或转发RTMP服务
音视频开发·视频编码·直播
音视频牛哥2 个月前
GB28181设备接入模块和轻量级RTSP服务有什么区别?
音视频开发·视频编码·直播