本文主要讲解,ST 提供的示例程序 server ,make 编译之后,会在 obj 目录生成 server 可执行文件,如下:

server 是一个 简单的 http 服务器,访问之后输出一个 简单的 html 页面,使用命令如下:
./obj/server -l ./ -b 192.168.0.122:8888
在刚开始调试的时候,推荐加上 -i 选项,可以防止 server 在后台运行。
server 流程图如下:

上面流程几个比较简单的函数,我简单过一遍:
1,parse_arguments(),解析命令行参数到 变量。
2,set_thread_throttling(),就根据一些规则设置 3 个变量,max_threads,min_wait_threads,max_wait_threads
3,create_listeners(),创建套接字,然后 listen,这里可以绑定多个 ip 端口 。
4,open_log_files(),创建 pid 文件,这里 pid 文件好像没有限制两个程序同时允许。最重要的是打开 ERRORS_FILE 错误文件。
5,load_configs() ,什么都没做,留给用户实现的。
先讲解一下 server 程序 中的一个重点结构体 struct socket_info。
struct socket_info {
st_netfd_t nfd; /* Listening socket */
char *addr; /* Bind address */
unsigned int port; /* Port */
int wait_threads; /* Number of threads waiting to accept */
int busy_threads; /* Number of threads processing request */
int rqst_count; /* Total number of processed requests */
} srv_socket[MAX_BIND_ADDRS]; /* Array of listening sockets
上面的代码定义了一个 全局数据srv_socket ,这个数组一般只有 [0] 用到,只绑定一个 ip跟端口。这个结构体的每个字段都比较重要,下面详细讲解一些:
1, st_netfd_t nfd; 这个是 服务器 listen 的 tcp 套接字,ST 自己封装了一下。
2, char *addr; IP 地址
3,unsigned int port; 端口。
4,int wait_threads; 代表有多少个协程 阻塞在 st_accept() 函数 。
5,int busy_threads 代表有多少个协程已经 从 st_accept() 拿到 fd,开始处理 http 请求。
6,int rqst_count;代表处理了多少个 http 请求。
server 程序有3个重点函数。
1,start_processes() ,开启多进程。多个进程同时 select() 监听 服务端套接字(srv_socket[0].nfd)。
2,install_sighandlers() ,注册信号 SIGTERM,SIGHUP,SIGUSR1, 用 pipe() 管道把 信号事件 转成 I/O 事件,这样就能用 select 监听 管道的 fd ,跟其他的网络套接字同一个 select 监听。
Only two types of external events are handled by the library's scheduler, because only these events can be detected by
select(2)orpoll(2): I/O events (a file descriptor is ready for I/O) and time events (some timeout has expired). However, other types of events (such as a signal sent to a process) can also be handled by converting them to I/O events. For example, a signal handling function can perform a write to a pipe (write(2)is reentrant/asynchronous-safe), thus converting a signal event to an I/O event
3,start_threads() ,开启 max_wait_threads 数量的协程,全部阻塞在 st_accept() 函数 等待客户端请求。
下面仔细分析重点函数 start_processes() ,流程图参考上面的,重点如下。
1,start_processes() 会 fork() 很多子进程,所有子进程都会返回 main() 函数执行后续的逻辑。但是 父进程不返回 main() 函数,父进程一直阻塞在 start_processes() 函数里面,等待某个子进程结束或者意外终结,然后打印子进程退出信息,在 fork() 一个子进程。这样,父进程 就是一个 watchdog,看门人,子进程如果有代码问题,奔溃了,父进程就会被激活,收集子进程的退出信息,方便排查错误,然后父进程再生一个子进程出来处理业务。
这种 watchdog的机制特别好,因为写代码总有一些意外的情况,不是经常发生。偶尔奔溃,就子进程奔溃,那就由父进程再生一个子进程出来就行。
在 Linux 环境,有一个软件也可以实现这种 看门人功能,就是 supervisor。
父进程除了等待子进程结束,还会处理信号,如下面代码,wait() 函数在等待子进程结束的时候,会被信号中断,errno 等于 EINTR 就会继续等待。
if ((pid = wait(&status)) < 0) {
if (errno == EINTR)
continue;
err_sys_quit(errfd, "ERROR: watchdog: wait");
}
父进程是用 wdog_sighandler() 函数来处理信号的,所有的信号都会传递一遍给子进程,然后自己再处理信号。演示一下,我用以下命令 发送一个 SIGUSR1 信号给父进程 ,13287 是我的父进程ID。
kill -s SIGUSR1 13287
上面的命令执行之后,所有的进程都会打印一遍信息,如下:

server 程序有处理 3 种信号:
SIGTERM,终结进程。SIGHUP,虽然注释写的 restart,但是实际上只是重新加载了配置文件。SIGUSR1,打印信息。
主进程不返回 main() , 子进程全部返回 main,然后 阻塞在 process_signals() 处理信号。
相当于 始祖协程 阻塞在 process_signals() 处理信号。 有多少个进程就有多少个始祖协程。
接下来分析最后一个重点函数 start_threads(),这个是子进程的开始协程的函数,用来处理http请求的,流程图参考上面的:
start_threads() 用 st_thread_create() 创建了很多个协程函数 handle_connections()。这些协程函数全部阻塞在 st_accept() 那里,等待客户端请求。
相关阅读:
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。QQ:2338195090。

