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

  1. 使用 SW_SERVER_CB_onStart 常量通过在 callbacks 数组中获取到 onStart 回调函数的数据。
  2. 还会更新 Server 对象的 master_pidmanager_pid
  3. 如果启用了 library 模块,则会调用一下对应的方法,这里其实也类似于 Hook 的形式。
  4. 最后判断是否有注册 onStart 回调函数,如果有则调用该回调函数,并且将 zserv Server 对象传递给回调函数。
  5. 至此就是会执行到我们用户写的业务逻辑了。
  6. 这个 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

  1. 使用 SW_SERVER_CB_onConnect 常量通过 php_swoole_server_get_fci_cache 函数获取 onConnect 回调函数数据。
  2. 如果启用了事件对象 'event_object' => true,回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
  3. 判断是否有注册 onConnect 回调函数,并调用该回调函数。
  4. 其中的 args 是回调的参数,参数有 $server$fd$reactor_id
  5. 不过 $reactor_id 这个参数,只有在多进程模式下才会有值。
  6. 这个 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

  1. 使用 SW_SERVER_CB_onReceive 常量通过 php_swoole_server_get_fci_cache 函数获取 onReceive 回调函数数据。
  2. 如果启用了事件对象 'event_object' => true,回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
  3. 判断是否有注册 onReceive 回调函数,并调用该回调函数。
  4. 其中的 args 是回调的参数,参数有 $server$fd$reactor_id$data
  5. 不过 $reactor_id 这个参数,只有在多进程模式下才会有值。
  6. 相较于 onConnect 回调函数,这里多了一个 $data 参数,这个参数就是用户发送的数据。
  7. 我们接收到数据,那么就可以进行业务逻辑的处理了,处理完后,要及时响应。
  8. 通常情况下,为了提高服务的处理能力,会使用到协程来分发请求。
// 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

  1. 关闭服务之前,要先清除的相关协程资源,避免资源浪费。
  2. 使用 SW_SERVER_CB_onClose 常量通过 php_swoole_server_get_fci_cache 方法获取到 onClose 回调函数信息。
  3. 如果是 Webscoket 连接,则获取对应的回调函数 onDisconnect 信息。
  4. 如果启用了事件对象 'event_object' => true,回调函数的参数,会集成到事件对象中,例如:function (Swoole\Server $serv, Swoole\Server\Event $object){}
  5. 判断是否有注册 onClose 回调函数,并调用该回调函数。
  6. 其中的 args 是回调的参数,参数包括:$server, $fd, $reactor_id
  7. 回调到用户的函数中,做一些业务上的收尾处理,至此服务就结束了。
// 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 模块就是基于此模块进行扩展的,所以我们需要有一定的了解。对我们来说掌握了原理性的内容,在回过头去看用户级的代码,往往会更轻松,所以我们需要有耐心的琢磨。

posted @ 2023-09-23 15:41  Yxh_blogs  阅读(61)  评论(2编辑  收藏  举报