Swoole 源码分析之 TCP Server 模块
首发原文链接:https://mp.weixin.qq.com/s/KxgxseLEz84wxUPjzSUd3w
大家好,我是码农先森。
今天我们来分析 TCP Server 模块
的实现原理,下面这张图是来自 Swoole
的官网。
那么,我们就主要分析这段言简意赅的代码,从这段代码中可以看出设置了四个回调方法,还有构造方法 new Swoole\Server(127.0.0.1, 9503)
、服务启动方法 $server->start()
。别小看上面的那段代码,其中可是蕴含了巨大的能量。我们可以先通过下面这张图,来了解接下来要介绍的内容。
话不多说,开整!
Swoole\Server 类及函数的注册
我们之所以可以使用 new Swoole\Server
类,是需要先将该类注册到 PHP 中,然后才能进行调用。从下面 php_swoole_server_minit
这个方法可以看出类的注册,这个方法里面只是展示了 Swoole\Server
类的注册,其他的省略掉了,比如:Swoole\Server\Task
等,更全的代码可以翻看源文件,下面这张图介绍了类及方法的注册定义。
// swoole-src/ext-src/swoole_server.cc:454
void php_swoole_server_minit(int module_number) {
// ---------------------------------------Server-------------------------------------
// 注册一个名为 "Swoole\Server" 的 PHP 类。swoole_server 是在扩展中定义的 zend_class_entry 结构体的变量名
// "Swoole\Server" 是该类的名称,nullptr 表示基类(如果有的话),swoole_server_methods 是一个包含类方法的结构体
SW_INIT_CLASS_ENTRY(swoole_server, "Swoole\\Server", nullptr, swoole_server_methods);
// Server 类不能被序列化
SW_SET_CLASS_NOT_SERIALIZABLE(swoole_server);
// Server 类不能被克隆
SW_SET_CLASS_CLONEABLE(swoole_server, sw_zend_class_clone_deny);
// Server 类不允许删除属性,并且禁止使用 unset() 函数删除该类的对象的属性
SW_SET_CLASS_UNSET_PROPERTY_HANDLER(swoole_server, sw_zend_class_unset_property_deny);
// 使用自定义对象处理。其中 server_create_object 和 server_free_object 是自定义对象的创建和释放函数
// ServerObject 是自定义对象的类型,std 是一个宏定义,用于指定对象的父类
SW_SET_CLASS_CUSTOM_OBJECT(swoole_server, server_create_object, server_free_object, ServerObject, std);
...
}
在第一行代码中,有这样一个参数 swoole_server_methods
,它是一个包含类方法的结构体,我们跟踪过去看看。
// swoole-src/ext-src/swoole_server.cc:373
static zend_function_entry swoole_server_methods[] = {
// 对应的代码 $server = new Swoole\Server('127.0.0.1', 9503);
PHP_ME(swoole_server, __construct, arginfo_class_Swoole_Server___construct, ZEND_ACC_PUBLIC)
// 对应的代码 $server->on('', function ($server) {})
PHP_ME(swoole_server, on, arginfo_class_Swoole_Server_on, ZEND_ACC_PUBLIC)
// 对应的代码 $server->start()
PHP_ME(swoole_server, start, arginfo_class_Swoole_Server_start, ZEND_ACC_PUBLIC)
...
}
这下可以发现 __construct()
、on()
、start()
三个方法,即在前面使用到的方法,就是在这里被定义声明的。
Server 类方法的具体实现
先看 __construct
构造方法的具体实现。首先会先判断 Server 类是否已经被实例化过了,如果是则会直接报错,即不能被重复实例化。然后,做一些参数解析、环境检测、运行模式的判断。最后,会将端口号添加是 Server 对象中,如果没有设置端口,系统则会进行自动分配。
// file: swoole-src/ext-src/swoole_server.cc:1829
static PHP_METHOD(swoole_server, __construct) {
// 判断是否已经存在一个 Server 对象的实例,如果 serv 不为空,则说明构造函数已经被调用过
ServerObject *server_object = server_fetch_object(Z_OBJ_P(ZEND_THIS));
Server *serv = server_object->serv;
if (serv) {
zend_throw_error(NULL, "Constructor of %s can only be called once", SW_Z_OBJCE_NAME_VAL_P(ZEND_THIS));
RETURN_FALSE;
}
// 声明一些变量
zval *zserv = ZEND_THIS;
char *host;
size_t host_len = 0;
zend_long sock_type = SW_SOCK_TCP;
zend_long serv_port = 0;
zend_long serv_mode = Server::MODE_BASE;
// swoole 只允许在命令行模式下运行
if (!SWOOLE_G(cli)) {
zend_throw_exception_ex(
swoole_exception_ce, -1, "%s can only be used in CLI mode", SW_Z_OBJCE_NAME_VAL_P(zserv));
RETURN_FALSE;
}
// 检查 Server 是否已经在运行了
if (sw_server() != nullptr) {
zend_throw_exception_ex(
swoole_exception_ce, -3, "server is running. unable to create %s", SW_Z_OBJCE_NAME_VAL_P(zserv));
RETURN_FALSE;
}
// 解析构造函数上的参数,例如:127.0.0.1、9503 等
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 4)
Z_PARAM_STRING(host, host_len)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(serv_port)
Z_PARAM_LONG(serv_mode)
Z_PARAM_LONG(sock_type)
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
// Swoole 只支持 base 和 process 两种模式
if (serv_mode != Server::MODE_BASE && serv_mode != Server::MODE_PROCESS) {
zend_throw_error(NULL, "invalid $mode parameters %d", (int) serv_mode);
RETURN_FALSE;
}
...
// 在 base 模式下,只会设置一个 reactor 主线程及一个 worker 进程
if (serv_mode == Server::MODE_BASE) {
serv->reactor_num = 1;
serv->worker_num = 1;
}
/* primary port */
do {
// 如果没有设置端口号,系统则会随机分配一个端口号
if (serv_port == 0 && strcasecmp(host, "SYSTEMD") == 0) {
if (serv->add_systemd_socket() <= 0) {
zend_throw_error(NULL, "failed to add systemd socket");
RETURN_FALSE;
}
} else {
// 将 host、port 参数 添加到 server 对象中
ListenPort *port = serv->add_port((enum swSocketType) sock_type, host, serv_port);
if (!port) {
zend_throw_exception_ex(swoole_exception_ce,
swoole_get_last_error(),
"failed to listen server port[%s:" ZEND_LONG_FMT "], Error: %s[%d]",
host,
serv_port,
swoole_strerror(swoole_get_last_error()),
swoole_get_last_error());
RETURN_FALSE;
}
}
// 存在监听多个端口的情况
for (auto ls : serv->ports) {
php_swoole_server_add_port(server_object, ls);
}
// 设置主端口
server_object->property->primary_port = (ServerPortProperty *) serv->get_primary_port()->ptr;
} while (0);
...
}
刚刚将 Server
进行了实例化,且设置了相关的参数。接下来再看 $server->on()
回调函数的设置。
// file: swoole-src/ext-src/swoole_server.cc:2343
static PHP_METHOD(swoole_server, on) {
// 检查 Server 是否已经运行
Server *serv = php_swoole_server_get_and_check_server(ZEND_THIS);
if (serv->is_started()) {
php_swoole_fatal_error(E_WARNING, "server is running, unable to register event callback function");
RETURN_FALSE;
}
// 事件名称,例如:start
zval *name;
// 回调函数 ,例如:function($server){}
zval *cb;
// 参数解析
if (zend_parse_parameters(ZEND_NUM_ARGS(), "zz", &name, &cb) == FAILURE) {
RETURN_FALSE;
}
// 主要是检查回调函数是否能够被正常的调用
char *func_name = nullptr;
zend_fcall_info_cache *fci_cache = (zend_fcall_info_cache *) emalloc(sizeof(zend_fcall_info_cache));
if (!sw_zend_is_callable_ex(cb, nullptr, 0, &func_name, nullptr, fci_cache, nullptr)) {
php_swoole_fatal_error(E_ERROR, "function '%s' is not callable", func_name);
return;
}
efree(func_name);
zend::String _event_name_ori(name);
zend::String _event_name_tolower(zend_string_tolower(_event_name_ori.get()), false);
ServerObject *server_object = server_fetch_object(Z_OBJ_P(ZEND_THIS));
// 寻找对应的事件类型,例如: start、connect 等
auto i = server_event_map.find(_event_name_tolower.to_std_string());
// 没有找到的情况,i == server_event_map.end() 这个条件没有匹配的事件找到才会成立
if (i == server_event_map.end()) {
zval *port_object = server_object->property->ports.at(0);
zval retval;
efree(fci_cache);
sw_zend_call_method_with_2_params(port_object, swoole_server_port_ce, nullptr, "on", &retval, name, cb);
RETURN_BOOL(Z_BVAL_P(&retval));
} else {
// 找到的情况
int event_type = i->second.type;
// 拼接属性名称 例如: onstart
std::string property_name = "on" + i->second.name;
// 将回调函数更新到 Server 对象中
zend_update_property(
swoole_server_ce, SW_Z8_OBJ_P(ZEND_THIS), property_name.c_str(), property_name.length(), cb);
if (server_object->property->callbacks[event_type]) {
efree(server_object->property->callbacks[event_type]);
}
server_object->property->callbacks[event_type] = fci_cache;
RETURN_TRUE;
}
}
设置好回调函数后,就开始调用 $server->start()
这意味这正式启动服务了。
// file: swoole-src/ext-src/swoole_server.cc:2548
static PHP_METHOD(swoole_server, start) {
zval *zserv = ZEND_THIS;
Server *serv = php_swoole_server_get_and_check_server(zserv);
// 检查 Server 是否已经启动
if (serv->is_started()) {
php_swoole_fatal_error(
E_WARNING, "server is running, unable to execute %s->start()", SW_Z_OBJCE_NAME_VAL_P(zserv));
RETURN_FALSE;
}
// 检查 Server 是否已经关闭
if (serv->is_shutdown()) {
php_swoole_fatal_error(
E_WARNING, "server have been shutdown, unable to execute %s->start()", SW_Z_OBJCE_NAME_VAL_P(zserv));
RETURN_FALSE;
}
// 检查 事件循环 是否已经创建
if (SwooleTG.reactor) {
php_swoole_fatal_error(
E_WARNING, "eventLoop has already been created, unable to start %s", SW_Z_OBJCE_NAME_VAL_P(zserv));
RETURN_FALSE;
}
ServerObject *server_object = server_fetch_object(Z_OBJ_P((zval *) serv->private_data_2));
// 注册相关的回调函数
server_object->register_callback();
// 做一些前置操作
server_object->on_before_start();
// 设置运行模式、启动守护进程、设置共享内存,最后启动底层的事件驱动循环
if (serv->start() < 0) {
php_swoole_fatal_error(E_ERROR, "failed to start server. Error: %s", sw_error);
}
RETURN_TRUE;
}
回调函数的实现
这里我主要介绍上面所提到的四个回调函数的实现,start 回调函数对应的实现函数是 php_swoole_server_onStart(Server *serv)
、connect 回调函数对应的实现函数是php_swoole_server_onConnect(Server *serv, DataHead *info)
、receive 回调函数对应的实现函数是 php_swoole_server_onReceive(Server *serv, RecvData *req)
、close 回调函数对应的实现函数是 php_swoole_server_onClose(Server *serv, DataHead *info)
。这些函数的实现中都会使用到 Zend 引擎所提供的 zend::function::call()
函数,用于触发用户自定义的回调函数及参数的回传。可以先看这张图,也是简要的介绍了四个回调函数的实现逻辑,让大家先有个基本的印象。
php_swoole_server_onStart
- 使用
SW_SERVER_CB_onStart
常量通过在callbacks
数组中获取到 onStart 回调函数的数据。 - 还会更新 Server 对象的
master_pid
和manager_pid
。 - 如果启用了 library 模块,则会调用一下对应的方法,这里其实也类似于 Hook 的形式。
- 最后判断是否有注册 onStart 回调函数,如果有则调用该回调函数,并且将
zserv
Server 对象传递给回调函数。 - 至此就是会执行到我们用户写的业务逻辑了。
- 这个 onStart 回调函数,当我们的服务一启动便会执行,可以在这里做一些初始化的操作。
// file: swoole-src/ext-src/swoole_server.cc:1371
static void php_swoole_server_onStart(Server *serv) {
serv->lock();
zval *zserv = (zval *) serv->private_data_2;
ServerObject *server_object = server_fetch_object(Z_OBJ_P(zserv));
// 从 callbacks 数组中获取 onStart 回调函数的信息
auto fci_cache = server_object->property->callbacks[SW_SERVER_CB_onStart];
// 更新服务对象的 master_pid 和 manager_pid 属性,分别表示主进程和管理进程的 PID。
zend_update_property_long(swoole_server_ce, SW_Z8_OBJ_P(zserv), ZEND_STRL("master_pid"), serv->gs->master_pid);
zend_update_property_long(swoole_server_ce, SW_Z8_OBJ_P(zserv), ZEND_STRL("manager_pid"), serv->gs->manager_pid);
// 如果启用了 library 模块,则会调用对应的方法。具体的方法实现可查阅 https://github.com/swoole/library/blob/master/src/core/Server/Helper.php
if (SWOOLE_G(enable_library)) {
zend::function::call("\\Swoole\\Server\\Helper::onStart", 1, zserv);
}
// 判断是否有注册 onStart 回调函数,并调用该回调函数
if (fci_cache && UNEXPECTED(!zend::function::call(fci_cache, 1, zserv, nullptr, serv->is_enable_coroutine()))) {
php_swoole_error(E_WARNING, "%s->onStart handler error", SW_Z_OBJCE_NAME_VAL_P(zserv));
}
serv->unlock();
}
php_swoole_server_onConnect
- 使用
SW_SERVER_CB_onConnect
常量通过php_swoole_server_get_fci_cache
函数获取 onConnect 回调函数数据。 - 如果启用了事件对象
'event_object' => true
,回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
。 - 判断是否有注册 onConnect 回调函数,并调用该回调函数。
- 其中的 args 是回调的参数,参数有
$server
、$fd
、$reactor_id
。 - 不过
$reactor_id
这个参数,只有在多进程模式下才会有值。 - 这个 onConnect 回调函数,当有用户连接之后会进行回调该函数,我们可以在这个函数里处理连接数据。
// file: swoole-src/ext-src/swoole_server.cc:1604
void php_swoole_server_onConnect(Server *serv, DataHead *info) {
// 获取 onConnect 回调函数的信息
auto fci_cache = php_swoole_server_get_fci_cache(serv, info->server_fd, SW_SERVER_CB_onConnect);
if (!fci_cache) {
return;
}
zval *zserv = (zval *) serv->private_data_2;
zval args[3];
int argc;
args[0] = *zserv;
// 如果启用了事件对象 'event_object' => true
// 回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
if (serv->event_object) {
zval *object = &args[1];
object_init_ex(object, swoole_server_event_ce);
zend_update_property_long(swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("fd"), (zend_long) info->fd);
zend_update_property_long(
swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("reactor_id"), (zend_long) info->reactor_id);
zend_update_property_double(
swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("dispatch_time"), info->time);
argc = 2;
} else {
ZVAL_LONG(&args[1], info->fd);
ZVAL_LONG(&args[2], info->reactor_id);
argc = 3;
}
// 判断是否有注册 onConnect 回调函数,并调用该回调函数
// 其中的 args 是回调的参数,例如:$server、$fd
if (UNEXPECTED(!zend::function::call(fci_cache, argc, args, nullptr, serv->enable_coroutine))) {
php_swoole_error(E_WARNING, "%s->onConnect handler error", SW_Z_OBJCE_NAME_VAL_P(zserv));
}
if (serv->event_object) {
zval_ptr_dtor(&args[1]);
}
}
php_swoole_server_onReceive
- 使用
SW_SERVER_CB_onReceive
常量通过php_swoole_server_get_fci_cache
函数获取 onReceive 回调函数数据。 - 如果启用了事件对象
'event_object' => true
,回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
。 - 判断是否有注册 onReceive 回调函数,并调用该回调函数。
- 其中的 args 是回调的参数,参数有
$server
、$fd
、$reactor_id
、$data
。 - 不过
$reactor_id
这个参数,只有在多进程模式下才会有值。 - 相较于 onConnect 回调函数,这里多了一个
$data
参数,这个参数就是用户发送的数据。 - 我们接收到数据,那么就可以进行业务逻辑的处理了,处理完后,要及时响应。
- 通常情况下,为了提高服务的处理能力,会使用到协程来分发请求。
// file: swoole-src/ext-src/swoole_server.cc:1076
int php_swoole_server_onReceive(Server *serv, RecvData *req) {
// 获取 onReceive 回调函数的信息
auto fci_cache = php_swoole_server_get_fci_cache(serv, req->info.server_fd, SW_SERVER_CB_onReceive);
if (fci_cache) {
zval *zserv = (zval *) serv->private_data_2;
zval args[4];
int argc;
args[0] = *zserv;
// 如果启用了事件对象 'event_object' => true
// 回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
if (serv->event_object) {
zval *object = &args[1];
zval data;
object_init_ex(object, swoole_server_event_ce);
zend_update_property_long(
swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("fd"), (zend_long) req->info.fd);
zend_update_property_long(
swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("reactor_id"), (zend_long) req->info.reactor_id);
zend_update_property_double(
swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("dispatch_time"), req->info.time);
php_swoole_get_recv_data(serv, &data, req);
zend_update_property(swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("data"), &data);
zval_ptr_dtor(&data);
argc = 2;
} else {
// 对参数 args 进行赋值
ZVAL_LONG(&args[1], (zend_long) req->info.fd);
ZVAL_LONG(&args[2], (zend_long) req->info.reactor_id);
php_swoole_get_recv_data(serv, &args[3], req);
argc = 4;
}
// 判断是否有注册 onReceive 回调函数,并调用该回调函数
// 其中的 args 是回调的参数,例如:$server, $fd, $reactor_id, $data
if (UNEXPECTED(!zend::function::call(fci_cache, argc, args, nullptr, serv->enable_coroutine))) {
php_swoole_error(E_WARNING, "%s->onReceive handler error", SW_Z_OBJCE_NAME_VAL_P(zserv));
serv->close(req->info.fd, false);
}
if (serv->event_object) {
zval_ptr_dtor(&args[1]);
} else {
// 释放内存资源
zval_ptr_dtor(&args[3]);
}
}
return SW_OK;
}
php_swoole_server_onClose
- 关闭服务之前,要先清除的相关协程资源,避免资源浪费。
- 使用
SW_SERVER_CB_onClose
常量通过php_swoole_server_get_fci_cache
方法获取到 onClose 回调函数信息。 - 如果是 Webscoket 连接,则获取对应的回调函数 onDisconnect 信息。
- 如果启用了事件对象
'event_object' => true
,回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
。 - 判断是否有注册 onClose 回调函数,并调用该回调函数。
- 其中的 args 是回调的参数,参数包括:
$server
,$fd
,$reactor_id
。 - 回调到用户的函数中,做一些业务上的收尾处理,至此服务就结束了。
// file: swoole-src/ext-src/swoole_server.cc:1639
void php_swoole_server_onClose(Server *serv, DataHead *info) {
zval *zserv = (zval *) serv->private_data_2;
ServerObject *server_object = server_fetch_object(Z_OBJ_P(zserv));
SessionId session_id = info->fd;
// 如果启用了协程,则需要清除这个连接的相关协程资源
if (serv->enable_coroutine && serv->send_yield) {
auto _i_co_list = server_object->property->send_coroutine_map.find(session_id);
if (_i_co_list != server_object->property->send_coroutine_map.end()) {
auto co_list = _i_co_list->second;
server_object->property->send_coroutine_map.erase(session_id);
while (!co_list->empty()) {
Coroutine *co = co_list->front();
co_list->pop_front();
swoole_set_last_error(ECONNRESET);
co->resume();
}
delete co_list;
}
}
// 获取 onClose 回调信息
auto *fci_cache = php_swoole_server_get_fci_cache(serv, info->server_fd, SW_SERVER_CB_onClose);
Connection *conn = serv->get_connection_by_session_id(session_id);
if (!conn) {
return;
}
// 如果是 Webscoket 连接,则获取对应的回调函数 onDisconnect 信息
if (conn->websocket_status != swoole::websocket::STATUS_ACTIVE) {
ListenPort *port = serv->get_port_by_server_fd(info->server_fd);
if (port && port->open_websocket_protocol &&
php_swoole_server_isset_callback(serv, port, SW_SERVER_CB_onDisconnect)) {
fci_cache = php_swoole_server_get_fci_cache(serv, info->server_fd, SW_SERVER_CB_onDisconnect);
}
}
if (fci_cache) {
zval *zserv = (zval *) serv->private_data_2;
zval args[3];
int argc;
args[0] = *zserv;
// 如果启用了事件对象 'event_object' => true
// 回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
if (serv->event_object) {
zval *object = &args[1];
object_init_ex(object, swoole_server_event_ce);
zend_update_property_long(
swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("fd"), (zend_long) session_id);
zend_update_property_long(
swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("reactor_id"), (zend_long) info->reactor_id);
zend_update_property_double(
swoole_server_event_ce, SW_Z8_OBJ_P(object), ZEND_STRL("dispatch_time"), info->time);
argc = 2;
} else {
// 对参数 args 进行赋值
ZVAL_LONG(&args[1], session_id);
ZVAL_LONG(&args[2], info->reactor_id);
argc = 3;
}
// 判断是否有注册 onClose 回调函数,并调用该回调函数
// 其中的 args 是回调的参数,例如:$server, $fd, $reactor_id
if (UNEXPECTED(!zend::function::call(fci_cache, argc, args, nullptr, serv->enable_coroutine))) {
php_swoole_error(E_WARNING, "%s->onClose handler error", SW_Z_OBJCE_NAME_VAL_P(zserv));
}
// 释放资源
if (serv->event_object) {
zval_ptr_dtor(&args[1]);
}
}
// 释放 HTTP2 流资源
if (conn->http2_stream) {
swoole_http2_server_session_free(conn);
}
}
总结
从 Swoole 官网的这段短小精悍的代码,就可以看出 Server 服务的关键要点。再通过对类、构造方法、回调函数的层层剖析,我们逐渐的了解了底层的实现原理。TCP Server 模式是基础模块,我们经常使用的 HTTP 模块就是基于此模块进行扩展的,所以我们需要有一定的了解。对我们来说掌握了原理性的内容,在回过头去看用户级的代码,往往会更轻松,所以我们需要有耐心的琢磨。