作者:施洪宝
一. 基础知识
1.1 swoole
swoole 是面向生产环境的 php 异步网络通信引擎, php 开发人员可以利用 swoole 开发出高性能的 server 服务。swoole 的 server 部分, 内容很多, 也涉及很多的知识点, 本文仅对其 server 进行简单的概述, 具体的实现细节在后续的文章中再进行详细介绍。
1.2 网络编程
- 网络通信是指在一台 (或者多台) 机器上启动一个 (或者多个) 进程, 监听一个 (或者多个) 端口, 按照某种协议 (可以是标准协议 http, dns; 也可以是自行定义的协议) 与客户端交换信息。
- 目前的网络编程多是在 tcp, udp 或者更上层的协议之上进行编程。swoole 的 server 部分是基于 tcp 以及 udp 协议的。
- 利用 udp 进行编程较为简单, 本文主要介绍 tcp 协议之上的网络编程
- TCP 网络编程主要涉及 4 种事件,
- 连接建立: 主要是指客户端发起连接 (connect) 以及服务端接受连接(accept)
- 消息到达: 服务端接受到客户端发送的数据, 该事件是 TCP 网络编程最重要的事件, 服务端对于该类事件进行处理时, 可以采用阻塞式或者非阻塞式, 除此之外, 服务端还需要考虑分包, 应用层缓冲区等问题
- 消息发送成功: 发送成功是指应用层将数据成功发送到内核的套接字发送缓冲区中, 并不是指客户端成功接受数据。对于低流量的服务而言, 数据通常一次性即可发送完, 并不需要关心此类事件。如果一次性不能将全部数据发送到内核缓冲区, 则需要关心消息是否成功发送 (阻塞式编程在系统调用(write, writev, send 等) 返回后即是发送成功, 非阻塞式编程则需要考虑实际写入的数据是否与预期一致)
- 连接断开: 需要考虑客户端断开连接 (read 返回 0) 以及服务端断开连接(close, shutdown)
1.3 进程间通信
- 进程之间的通信有无名管道(pipe), 有名管道(fifo), 信号, 信号量, 套接字, 共享内存等方式
- swoole 中采用 unix 域套接字用于多进程之间的通信(指 swoole 内部进程之间)
1.4 socketpair
- socketpair 用于创建一个套接字对, 类似于 pipe, 不同的是 pipe 是单向通信, 双向通信需要创建两次, socketpair 调用一次即可实现双向通信, 除此之外, 由于使用的是套接字, 还可以定义数据交换的方式
- socketpair 系统调用
int socketpair(int domain, int type, int protocol, int sv[2]);
//domain 表示协议簇
//type 表示类型
//protocol 表示协议, SOCK_STREAM 表示流协议(类似 tcp), SOCK_DGRAM 表示数据报协议(类似 udp)
//sv 用于存储建立的套接字对, 也就是两个套接字文件描述符
// 成功返回 0, 否则返回 -1, 可以从 errno 获取错误信息
- 调用成功后 sv[0], sv[1]分别存储一个文件描述符
- 向 sv[0]中写入, 可以从 sv[1]中读取
- 向 sv[1]中写入, 可以从 sv[0]中读取
- 进程调用 socketpair 后, fork 子进程, 子进程会默认继承 sv[0], sv[1]这两个文件描述符, 进而可以实现父子进程间通信。例如, 父进程向 sv[0]中写入, 子进程从 sv[1]中读取; 子进程向 sv[1]中写入, 父进程从 sv[0]中读取。
1.5 守护进程(daemon)
- 守护进程是一种特殊的后台进程, 它脱离于终端, 用于周期性的执行某种任务
- 进程组
- 每个进程都属于一个进程组
- 每个进程组都有一个进程组号, 也就是该组组长的进程号(PID)
- 一个进程只能为自己或者其子进程设置进程组号
- 会话
- 一个会话可以包含多个进程组, 这些进程组中最多只能有一个前台进程组(也可以没有)
- setsid 可以创建一个新的会话, 该进程不能是进程组的组长。setsid 调用完成后, 该进程成为这个会话的首进程(领头进程), 同时变成一个新的进程组的组长, 如果该进程之前有控制终端, 则该进程与终端的联系被断开
- 用户通过终端登录或者网络登录, 会创建一个新的会话
- 一个会话最多只能有一个控制终端
- 创建守护进程的方式
- fork 子进程后, 父进程退出, 子进程执行 setsid 即可成为守护进程。这种方式下, 子进程是会话的领头进程, 可以重新打开终端, 此时可以再次 fork, fork 产生的子进程无法再打开终端。第二次 fork 并不是必须的, 只是为了防止子进程再次打开终端
- linux 提供了 daemon 函数用于创建守护进程
1.6 swoole tcp server 示例
<?php
// 创建 server
$serv = new Swoole\Server('0.0.0.0', 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP);
// 设置 server 的参数
$serv->set(array(
'reactor_num' => 2, //reactor thread num
'worker_num' => 3, //worker process num
));
// 设置事件回调
$serv->on('connect', function ($serv, $fd){echo "Client:Connect.\n";});
$serv->on('receive', function ($serv, $fd, $reactor_id, $data) {$serv->send($fd, 'Swoole:'.$data);
$serv->close($fd);
});
$serv->on('close', function ($serv, $fd) {echo "Client: Close.\n";});
// 启动 server
$serv->start();
- 上述代码在 cli 模式下执行时, 经过词法分析, 语法分析生成 opcode, 进而交由 zend 虚拟机执行
- zend 虚拟机在执行到 $serv->start()时, 启动 swoole server
- 上述代码中设置的事件回调是在 worker 进程中执行, 后文会详细介绍 swoole server 模型
二. swoole server
2.1 base 模式
- 说明
- base 模式采用多进程模型, 这种模型与 nginx 一致, 每个进程只有一个线程, 主进程负责管理工作进程, 工作进程负责监听端口, 接受连接, 处理请求以及关闭连接
- 多个进程同时监听端口, 会有惊群问题, 目前 swoole 并没有解决
- linux 内核 3.9 及其后续版本提供了新的套接字参数 SO_REUSEPORT, 该参数允许多个进程绑定到同一个端口, 内核在接受到新的连接请求时, 会唤醒其中一个进行处理, 内核层面也会做负载均衡, 可以解决上述的惊群问题
- base 模式下, reactor_number 参数并没有作用, 因为每个进程只有一个线程
- 如果 worker 进程数设置为 1, 则不会 fork 出 worker 进程, 主进程直接处理请求
- 启动过程
- php 代码执行到 $serv->start()时, 主进程进入 int swServer_start(swServer *serv)函数, 该函数负责启动 server
- 在函数 swServer_start 中会调用 swReactorProcess_start, 这个函数会 fork 出多个 worker 进程
- 主进程和 worker 进程各自进入自己的事件循环, 处理各类事件
2.2 process 模式
- 说明
- 这种模式为多进程多线程, 有主进程, manager 进程, worker 进程, task_worker 进程
- 主进程下有多个线程, 主线程负责接受连接, 之后交给 react 线程处理请求。react 线程负责接收数据包, 并将数据转发给 worker 进程进行处理, 之后处理 worker 进程返回的数据
- manager 进程, 该进程为单线程, 主要负责管理 worker 进程, 类似于 nginx 中的主进程, 当 worker 进程异常退出时, manager 进程负责重新 fork 出一个 worker 进程
- worker 进程, 该进程为单线程, 负责具体处理请求
- task_worker 进程, 用于处理比较耗时的任务, 默认不开启
- worker 进程与主进程中的 react 线程使用域套接字进行通信, worker 进程之间不进行通信
- 启动过程
- swoole server 启动入口: swServer_start 函数,
//php 代码中 $serv->start(); 会调用函数, 进行 server start
int swServer_start(swServer *serv);
// 该函数首先进行必要的参数检查
static int swServer_start_check(swServer *serv);
// 其中有,
if (serv->worker_num < serv->reactor_num)
{serv->reactor_num = serv->worker_num;}// 也就是说 reactor_num <= worker_num
// 之后执行 factory start, 也就是 swFactoryProcess_start 函数, 该函数会 fork 出 manager 进程, manager 进程进而 fork 出 worker 进程以及 task_worker 进程
if (factory->start(factory) < 0)
{return SW_ERR;}
// 然后主进程的主线程生成 reactor 线程
if (serv->factory_mode == SW_MODE_BASE)
{ret = swReactorProcess_start(serv);
}
else
{ret = swReactorThread_start(serv);
}
- 如果设置了 daemon 模式, 在必要的参数检查完后, 先将自己变为守护进程再 fork manager 进程, 进而创建 reactor 线程
- 主进程先 fork 出 manager 进程, manager 进程负责 fork 出 worker 进程以及 task_worker 进程。worker 进程之后进入 int swWorker_loop(swServer *serv, int worker_id), 也就是进入自己的事件循环, task_worker 也是一样, 进入自己的事件循环。
static int swFactoryProcess_start(swFactory *factory);
//swFactoryProcess_start 会调用 swManager_start 生成 manager 进程
int swManager_start(swServer *serv);
// manager 进程会 fork 出 worker 进程以及 task_worker 进程
- 主进程 pthread_create 出 react 线程, 主线程和 react 线程各自进入自己的事件循环, reactor 线程执行 static int swReactorThread_loop(swThreadParam *param), 等待处理事件
// 主线程执行 swReactorThread_start, 创建出 reactor 线程
int swReactorThread_start(swServer *serv);
- 结构图
swoole process 模式结构如下图所示,
- 上图并没有考虑 task_worker 进程, 在默认情况下, task_worker 进程数为 0
三. 请求处理流程(process 模式)
3.1 reactor 线程与 worker 进程之间的通信
- swoole master 进程与 worker 进程之间的通信如下图所示,
- swoole 使用 SOCK_DGRAM, 而不是 SOCK_STREAM, 这里是因为每个 reactor 线程负责处理多个请求, reactor 接收到请求后会将信息转发给 worker 进程, 由 worker 进程负责处理, 如果使用 SOCK_STREAM, worker 进程无法对 tcp 进行分包, 进而处理请求
- swFactoryProcess_start 函数中会根据 worker 进程数创建对应个数的套接字对, 用于 reactor 线程与 worker 进程通信(swPipeUnsock_create 函数)
- 假设 reactor 线程有 2 个, worker 进程有 3 个, 则 reactor 与 worker 之间的通信如下图所示,
- 每个 reactor 线程负责监听几个 worker 进程, 每个 worker 进程只有一个 reactor 线程监听(reactor_num<=worker_num)。swoole 默认使用 worker_process_id % reactor_num 对 worker 进程进行分配, 交给对应的 reactor 线程进行监听
- reactor 线程收到某个 worker 进程的数据后会进行处理, 值得注意的是, 这个 reactor 线程可能并不是发送请求的那个 reactor 线程。
- reactor 线程与 worker 进程通信的数据包
// 包头
typedef struct _swDataHead
{
int fd;
uint32_t len;
int16_t from_id;
uint8_t type;
uint8_t flags;
uint16_t from_fd;
#ifdef SW_BUFFER_RECV_TIME
double time;
#endif
} swDataHead;
//reactor 线程向 worker 进程发送的数据, 也就是 worker 进程收到的数据包
typedef struct
{
swDataHead info;
char data[SW_IPC_BUFFER_SIZE];
} swEventData;
//worker 进程向 reactor 线程发送的数据, 也就是 reactor 线程收到的数据包
typedef struct
{
swDataHead info;
char data[0];
} swPipeBuffer;
3.2 请求处理
- master 进程中的主线程负责监听端口(listen), 接受连接(accept, 产生一个 fd), 接受连接后将请求分配给 reactor 线程, 默认通过 fd % reactor_num 进行分配, 之后通过 epoll_ctl 将 fd 加入到对应 reactor 线程中(如果对应的 reactor 线程正在执行 epoll_wait, 主线程会阻塞), 刚加入时监听写事件, 如果直接监听读事件, 可能会立刻被触发, 而监听写事件可以允许 reactor 线程进行一些初始化操作
// 主线程执行 epoll_ctl 将 fd(新接受的连接)加入到 reactor 线程的监听队列中
epoll_ctl(epfd, fd, ...);
// 对应的 reactor 线程如果正在执行
epoll_wait(epfd, ...);
- 这种情况主线程会被阻塞(两个线程同时操作 epfd)
- 如果 reactor 线程没有正在执行 epoll_wait, 主线程则不会被阻塞, 执行成功后直接返回
- reactor 线程中 fd 的写事件被触发, reactor 线程负责处理, 发现是首次加入, 没有数据可写, 则开启读事件监听
- reactor 线程读取到用户的请求数据, 一个请求的数据接收完后, 将数据转发给 worker 进程, 默认是通过 fd % worker_num 进行分配
- reactor 发送给 worker 进程的数据包, 会包含一个头部, 头部中记录了 reactor 的信息
- 如果发送的数据过大, 则需要将数据进行分片, 限于篇幅, reactor 的分片, 后续再进行详细讲述
- 可能存在多个 reactor 线程同时向同一个 worker 进程发送数据的情况, 故而 swoole 采用 SOCK_DGRAM 模式与 worker 进程进行通信, 通过每个数据包的包头, worker 进程可以区分出是由哪个 reactor 线程发送的数据
- worker 进程收到 reactor 发送的数据包后, 进行处理, 处理完成后, 将数据发送给主进程
- worker 进程发送给主进程的数据包, 也会包含一个头部, 当 reactor 线程收到数据包后, 能够知道对应的 reactor 线程, 请求的 fd 等信息
- 主进程收到 worker 进程发送的数据包, 这个会触发某个 reactor 线程进行处理
- 这个 reactor 线程并不一定是之前发送请求给 worker 进程的那个 reactor 线程
- 主进程的每个 reactor 线程都负责监听 worker 进程发送的数据包, 每个 worker 发送的数据包只会由一个 reactor 线程进行监听, 故而只会触发一个 reactor 线程
- reactor 线程处理 worker 进程发送的数据包, 如果是直接发送数据给客户端, 则可以直接发送, 如果需要改变这个这个连接的监听状态(例如 close), 则需要先找到监听这个连接的 reactor, 进而改变这个连接的监听状态
- reactor 处理线程与 reactor 监听线程可能并不是同一个线程
- reactor 监听线程负责监听客户端发送的数据, 进而转发给 worker 进程
- reactor 处理线程负责监听 worker 进程发送给主进程的数据, 进而将数据发送给客户端
四. gdb 调试
4.1 process 模式启动
//fork manager 进程
#0 0x00007ffff67dae64 in fork () from /lib64/libc.so.6
#1 0x00007ffff553888a in swoole_fork () at /root/code/swoole-src/src/core/base.c:186
#2 0x00007ffff556afb8 in swManager_start (serv=serv@entry=0x1353f60) at /root/code/swoole-src/src/server/manager.cc:164
#3 0x00007ffff5571dde in swFactoryProcess_start (factory=0x1353ff8) at /root/code/swoole-src/src/server/process.c:198
#4 0x00007ffff556ef8b in swServer_start (serv=0x1353f60) at /root/code/swoole-src/src/server/master.cc:651
#5 0x00007ffff55dc808 in zim_swoole_server_start (execute_data=<optimized out>, return_value=0x7fffffffac50)
at /root/code/swoole-src/swoole_server.cc:2946
#6 0x00000000007bb068 in ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER () at /root/php-7.3.3/Zend/zend_vm_execute.h:980
#7 execute_ex (ex=0x7ffff7f850a8) at /root/php-7.3.3/Zend/zend_vm_execute.h:55485
#8 0x00000000007bbf58 in zend_execute (op_array=op_array@entry=0x7ffff5e7b340, return_value=return_value@entry=0x7ffff5e1d030)
at /root/php-7.3.3/Zend/zend_vm_execute.h:60881
#9 0x0000000000737554 in zend_execute_scripts (type=type@entry=8, retval=0x7ffff5e1d030, retval@entry=0x0,
file_count=file_count@entry=3) at /root/php-7.3.3/Zend/zend.c:1568
#10 0x00000000006db4d0 in php_execute_script (primary_file=primary_file@entry=0x7fffffffd050) at /root/php-7.3.3/main/main.c:2630
#11 0x00000000007be2f5 in do_cli (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:997
#12 0x000000000043fc1f in main (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:1389
// pthread_create reactor 线程
#0 0x00007ffff552e960 in pthread_create@plt () from /usr/local/lib/php/extensions/no-debug-non-zts-20180731/swoole.so
#1 0x00007ffff5576959 in swReactorThread_start (serv=0x1353f60) at /root/code/swoole-src/src/server/reactor_thread.c:883
#2 0x00007ffff556f006 in swServer_start (serv=0x1353f60) at /root/code/swoole-src/src/server/master.cc:670
#3 0x00007ffff55dc808 in zim_swoole_server_start (execute_data=<optimized out>, return_value=0x7fffffffac50)
at /root/code/swoole-src/swoole_server.cc:2946
#4 0x00000000007bb068 in ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER () at /root/php-7.3.3/Zend/zend_vm_execute.h:980
#5 execute_ex (ex=0x7fffffffab10) at /root/php-7.3.3/Zend/zend_vm_execute.h:55485
#6 0x00000000007bbf58 in zend_execute (op_array=op_array@entry=0x7ffff5e7b340, return_value=return_value@entry=0x7ffff5e1d030)
at /root/php-7.3.3/Zend/zend_vm_execute.h:60881
#7 0x0000000000737554 in zend_execute_scripts (type=type@entry=8, retval=0x7ffff5e1d030, retval@entry=0x0,
file_count=file_count@entry=3) at /root/php-7.3.3/Zend/zend.c:1568
#8 0x00000000006db4d0 in php_execute_script (primary_file=primary_file@entry=0x7fffffffd050) at /root/php-7.3.3/main/main.c:2630
#9 0x00000000007be2f5 in do_cli (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:997
#10 0x000000000043fc1f in main (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:1389
4.2 base 模式启动
//base 模式下的启动
#0 0x00007ffff67dae64 in fork () from /lib64/libc.so.6
#1 0x00007ffff553888a in swoole_fork () at /root/code/swoole-src/src/core/base.c:186
#2 0x00007ffff5558557 in swProcessPool_spawn (pool=pool@entry=0x7ffff2d2a308, worker=0x7ffff2d2a778)
at /root/code/swoole-src/src/network/process_pool.c:392
#3 0x00007ffff5558710 in swProcessPool_start (pool=0x7ffff2d2a308) at /root/code/swoole-src/src/network/process_pool.c:227
#4 0x00007ffff55741cf in swReactorProcess_start (serv=0x1353f60) at /root/code/swoole-src/src/server/reactor_process.cc:176
#5 0x00007ffff556f21d in swServer_start (serv=0x1353f60) at /root/code/swoole-src/src/server/master.cc:666
#6 0x00007ffff55dc808 in zim_swoole_server_start (execute_data=<optimized out>, return_value=0x7fffffffac50)
at /root/code/swoole-src/swoole_server.cc:2946
#7 0x00000000007bb068 in ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER () at /root/php-7.3.3/Zend/zend_vm_execute.h:980
#8 execute_ex (ex=0x7ffff2d2a308) at /root/php-7.3.3/Zend/zend_vm_execute.h:55485
#9 0x00000000007bbf58 in zend_execute (op_array=op_array@entry=0x7ffff5e7b340, return_value=return_value@entry=0x7ffff5e1d030)
at /root/php-7.3.3/Zend/zend_vm_execute.h:60881
#10 0x0000000000737554 in zend_execute_scripts (type=type@entry=8, retval=0x7ffff5e1d030, retval@entry=0x0,
file_count=file_count@entry=3) at /root/php-7.3.3/Zend/zend.c:1568
#11 0x00000000006db4d0 in php_execute_script (primary_file=primary_file@entry=0x7fffffffd050) at /root/php-7.3.3/main/main.c:2630
#12 0x00000000007be2f5 in do_cli (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:997
#13 0x000000000043fc1f in main (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:1389
五. 参考
- UNIX 网络编程
- UNIX 环境高级编程
- https://wiki.swoole.com/
- https://www.cnblogs.com/welhz…
- https://www.cnblogs.com/JohnA…