本文主要讲解,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。