news 2026/4/14 20:54:31

使用JsonRPC实现前后台

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用JsonRPC实现前后台

使用 JsonRPC 实现前后台分离

1. 把程序拆分为前后台

1.1 为何要拆分?

对于一个功能比较复杂的程序,如果所有代码(界面显示、业务逻辑、硬件操作)都写在一起,会带来很多麻烦:

  • 牵一发而动全身:比如要更换一个 LED 的控制引脚,或者把温湿度传感器从 DHT11 换成其他型号,你需要去修改那些直接操作硬件的函数。如果这些函数和界面代码混在一起,修改时很可能不小心破坏界面的功能。
  • 团队协作困难:做 Qt 界面的人,和做底层驱动的人,必须频繁地沟通代码改动。任何一方的修改,都可能导致另一方编译不通过。
  • 稳定性差:前台界面一个简单的 bug(比如空指针)就可能让整个程序崩溃,连带着硬件控制也失效。

所以,我们把一个完整的程序拆成两个独立的进程(程序)

  • 前台程序(GUI):只负责显示界面、接收用户点击和输入。它不直接操作任何硬件,而是把用户的要求(比如“打开 LED”)打包成一个请求,通过网络发给后台,然后等待后台返回结果并显示。
  • 后台程序(APP / Service):负责真正“干活”——控制 LED、读取温湿度传感器、处理复杂计算等。它像一个 24 小时值班的服务员,安静地等待前台的请求,执行完操作后把结果返回。

这样做的好处非常明显:

  • 更换硬件(比如改 LED 引脚)时,只需要修改后台程序,前台 Qt 程序完全不用改动,也不用重新编译
  • 想美化界面或调整布局,只需要修改前台,后台程序纹丝不动。
  • 前后台可以由不同团队独立开发,只要约定好通信的“接口格式”即可。
1.2 如何拆分?

前台和后台属于不同的“进程”。进程之间要通信,就需要**进程间通信(IPC)**技术,比如:网络通信、管道、共享内存等。

本课程选用的是基于网络通信的 JsonRPC 远程调用

  • RPC(Remote Procedure Call):远程过程调用。通俗讲,就是让前台程序可以像调用本地函数一样,去调用后台程序里的函数。
  • JSON:一种非常流行的数据格式,便于人和程序阅读,也便于网络传输。

前后台通过网络交换 JSON 格式的数据,就能实现“前台发出请求 → 后台执行 → 前台收到结果”。


2. 网络通信概述

2.1 IP 和端口

在网络中传输数据,就像寄快递一样,必须明确三要素:源、目的、长度

  • IP 地址:用来定位到某一台设备(电脑、手机、开发板)。好比快递单上的“城市+街道+门牌号”。
    • 127.0.0.1是一个特殊 IP,表示“本机”或“本地回环地址”。同一台设备上的两个程序可以用这个地址通信。
  • 端口号:用来定位到该设备上的某个具体程序。好比快递单上的“收件人姓名”。
    • 一个 IP 地址下有 65535 个端口。
    • 0~1023 是“知名端口”,被系统服务占用,例如:80(HTTP 网页服务)、22(SSH 远程登录)。
    • 我们自己的程序一般使用 1024~65535 之间的端口,例如 8888、1234。

服务器如何区分同一台电脑上两个不同的浏览器?
当你用 Chrome 和 Firefox 同时访问百度时,你的电脑(源 IP 相同)向百度服务器(目的 IP 相同)的 80 端口(目的端口相同)发送请求。百度返回数据时,会根据源端口来区分:操作系统为 Chrome 分配一个临时端口(比如 52341),为 Firefox 分配另一个(比如 52342)。服务器把响应的目的端口设置成这些源端口,你的电脑就能把数据正确交给对应的浏览器。

所以,源IP:源端口标识发送者,目的IP:目的端口标识接收者。服务器依靠(源IP, 源端口)的组合来区分不同连接。

2.2 网络传输中的两个角色:Server 和 Client
  • 服务器(Server):被动等待。它启动后会绑定一个固定的端口,然后一直“监听”,等着别人来连接。它从不主动发起连接。我们的后台程序就是服务器角色。
  • 客户端(Client):主动发起。它主动向服务器的 IP 和端口发起连接请求。我们的前台 Qt 程序就是客户端角色。
2.3 两种传输方式:TCP 和 UDP

在网络的“运输层”,有两个最常用的协议:

特点TCPUDP
连接性面向连接。通信前必须先建立连接(三次握手),就像打电话。无连接。直接把数据包发出去,就像寄信,不确认对方是否收到。
可靠性可靠。丢包会重传,乱序会重组,保证数据完整有序到达。不可靠。丢包、乱序都不管,只“尽最大努力”。
速度较慢。因为要维护连接、确认、重传等,头部开销大(20字节)。较快。无复杂机制,头部开销小(8字节)。
适用场景文件传输、网页浏览、数据库、RPC 调用等要求数据完整的场景。视频通话、在线游戏、实时音视频等允许少量丢包,但对延迟敏感的场景。

为什么有了 TCP 还要 UDP?
比如视频通话:偶尔花屏一下可以接受,但如果用 TCP,一旦丢包就会卡住等待重传,反而更影响体验。所以实时应用更喜欢 UDP。

在我们的 JsonRPC 前后台通信中,必须保证请求和响应都不丢失、不乱序,所以我们选择TCP

TCP 和 UDP 的交互流程简图

  • TCP(面向连接,流模式)
    服务器:socket() → bind() → listen() → accept()(阻塞)
    客户端:socket() → connect() → 发送/接收数据 → close()
    连接建立后,双方可以随时用 send() / recv() 交换数据。

  • UDP(无连接,数据报模式)
    服务器:socket() → bind() → recvfrom() / sendto()
    客户端:socket() → sendto() / recvfrom()
    无需 connect(),每次发送都要指定对方地址。


3. 网络编程主要函数介绍

下面这些函数是编写 TCP/UDP 程序的基础。它们都是操作系统提供的,我们只需要按顺序调用即可。

3.1 socket() —— 创建“电话”

c

int socket(int domain, int type, int protocol);
  • domain:协议族。常用AF_INET(IPv4 网络通信),AF_UNIX(单机内进程通信)。
  • type:通信类型。SOCK_STREAM表示 TCP,SOCK_DGRAM表示 UDP。
  • protocol:一般填 0 即可,系统会自动选择。
  • 返回值:成功返回一个套接字描述符(可以理解为一个文件描述符,后续操作都用它);失败返回 -1。
3.2 bind() —— 给“电话”贴上号码牌(服务器用)

c

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
  • 将 socket 绑定到一个具体的 IP 地址和端口号。这样客户端才知道该找谁。
  • sockfd:socket() 返回的描述符。
  • my_addr:包含 IP 和端口信息的结构体。
  • 常用struct sockaddr_in来填充,然后强制转换。

示例:

c

struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8888); // 端口号,htons 转成网络字节序 server_addr.sin_addr.s_addr = INADDR_ANY; // 表示监听本机所有网卡 bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
3.3 listen() —— 让“电话”处于待机状态(服务器用)

c

int listen(int sockfd, int backlog);
  • 将 socket 转为被动监听模式,准备接受客户端的连接。
  • backlog:最大等待队列长度。如果同时有多个客户端连接,超过此数会被拒绝。
  • 返回值:成功 0,失败 -1。
3.4 accept() —— 接起电话(服务器用)

c

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 阻塞等待,直到有一个客户端连接进来。
  • 当连接成功时,会返回一个新的 socket 描述符,专门用于和这个客户端通信。原来的监听 socket 可以继续等待其他连接。
  • addraddrlen会填充客户端的 IP 和端口信息(如果你想知道是谁打来的)。
3.5 connect() —— 拨打电话(客户端用)

c

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
  • 客户端主动连接服务器。
  • serv_addr中填服务器的 IP 和端口。
  • 成功返回 0,失败 -1。
3.6 send() 和 recv() —— 通话(双方都用)

c

ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • send:把buf中的数据发送出去,返回实际发送的字节数。
  • recv:从对方接收数据,存入buf,返回实际接收的字节数(0 表示对方关闭连接)。
  • flags一般填 0。
3.7 sendto() 和 recvfrom() —— 用于 UDP

c

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • UDP 不需要建立连接,所以每次发送都要指定目标地址(dest_addr),每次接收都能获得发送方的地址(src_addr)。

4. TCP 编程示例(完整可运行)

这里给出一个最经典的 TCP 回显服务器(把客户端发来的数据原样返回)和对应的客户端。你可以先在 Ubuntu 上编译运行,感受一下网络通信的过程。

4.1 服务器程序(server.c)

c

#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERVER_PORT 8888 #define BACKLOG 10 int main() { int listen_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_addr_len = sizeof(client_addr); char recv_buf[1024]; int ret; // 1. 创建 socket listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); return -1; } // 2. 绑定地址和端口 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = INADDR_ANY; if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); return -1; } // 3. 开始监听 if (listen(listen_fd, BACKLOG) < 0) { perror("listen"); return -1; } printf("Server is listening on port %d...\n", SERVER_PORT); while (1) { // 4. 接受客户端连接 client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len); if (client_fd < 0) { perror("accept"); continue; } printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 5. 处理客户端(这里用简单的 fork 或循环处理) // 为了简洁,我们只接收一次数据并回应 ret = recv(client_fd, recv_buf, sizeof(recv_buf) - 1, 0); if (ret > 0) { recv_buf[ret] = '\0'; printf("Received: %s\n", recv_buf); send(client_fd, recv_buf, ret, 0); } close(client_fd); printf("Connection closed.\n"); } close(listen_fd); return 0; }
4.2 客户端程序(client.c)

c

#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERVER_PORT 8888 int main(int argc, char *argv[]) { if (argc != 2) { printf("Usage: %s <server_ip>\n", argv[0]); return -1; } int sock_fd; struct sockaddr_in server_addr; char send_buf[1024]; char recv_buf[1024]; // 1. 创建 socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { perror("socket"); return -1; } // 2. 准备服务器地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); if (inet_aton(argv[1], &server_addr.sin_addr) == 0) { printf("Invalid IP address\n"); return -1; } // 3. 连接服务器 if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("connect"); return -1; } // 4. 发送数据 printf("Enter message: "); fgets(send_buf, sizeof(send_buf), stdin); send(sock_fd, send_buf, strlen(send_buf), 0); // 5. 接收回应 int len = recv(sock_fd, recv_buf, sizeof(recv_buf) - 1, 0); if (len > 0) { recv_buf[len] = '\0'; printf("Server echoed: %s\n", recv_buf); } close(sock_fd); return 0; }
4.3 上机实验
  1. 编译:

    bash

    gcc server.c -o server gcc client.c -o client
  2. 运行服务器:./server

  3. 另开一个终端运行客户端:./client 127.0.0.1

  4. 输入一行文字,回车,服务器会返回相同的内容。

这个例子虽然简单,但已经包含了 TCP 通信的全部核心步骤。


5. JSON-RPC 示例与情景分析

理解了基本的 TCP 通信后,我们再来看一个更高层的封装:JSON-RPC。它让你不用手动拼接 JSON 和解析,而是直接像调用本地函数一样调用远程函数。

5.1 JSON 是什么

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,长得像这样:

json

{ "name": "张三", "age": 25, "isStudent": false, "hobby": ["reading", "coding"], "address": { "city": "深圳", "zip": 518000 } }
  • {}表示对象,内部是键值对,键必须用双引号。
  • []表示数组,里面可以放任意类型的值。
  • 值可以是:字符串、数字、布尔值(true/false)、null、对象、数组。

JSON 的优点是:可读性好,并且几乎所有编程语言都有现成的库来解析和生成它

5.2 常用的 JSON 函数(cJSON 库)

我们使用 C 语言时,常用cJSON库来处理 JSON。它的核心是一个结构体cJSON,里面包含了类型、值、子节点等。

创建 JSON

c

cJSON *root = cJSON_CreateObject(); // 创建空对象 cJSON_AddNumberToObject(root, "age", 25); // 添加数字 cJSON_AddStringToObject(root, "name", "张三"); // 添加字符串 cJSON_AddFalseToObject(root, "isStudent"); // 添加 false char *json_str = cJSON_Print(root); // 转换成字符串 printf("%s\n", json_str); free(json_str); cJSON_Delete(root); // 释放内存

解析 JSON

c

cJSON *root = cJSON_Parse(json_string); // 从字符串解析 cJSON *age_item = cJSON_GetObjectItem(root, "age"); if (cJSON_IsNumber(age_item)) { int age = age_item->valueint; // 注意:推荐用 valuedouble,但 valueint 也可用 } cJSON_Delete(root);

从数组中取元素

c

cJSON *array = cJSON_GetObjectItem(root, "hobby"); cJSON *first = cJSON_GetArrayItem(array, 0); printf("%s\n", first->valuestring);
5.3 服务器程序启动(使用 jsonrpc-c 库)

我们使用别人写好的jsonrpc-c库,它基于libev事件循环,可以自动处理网络和 JSON 解析。

服务器核心代码:

c

jrpc_server my_server; jrpc_server_init(&my_server, 1234); // 监听 1234 端口 jrpc_register_procedure(&my_server, say_hello, "sayHello", NULL); jrpc_register_procedure(&my_server, add, "add", NULL); jrpc_server_run(&my_server); // 开始循环,处理请求 jrpc_server_destroy(&my_server);

这里注册了两个函数:say_helloadd。当前台发来"method": "add"的请求时,服务器就会自动调用add函数。

5.4 客户端程序发出请求

客户端需要自己构造 JSON 字符串,并通过 socket 发送。

c

// 构造请求 sprintf(buf, "{\"method\": \"add\", \"params\": [%d, %d], \"id\": 1}", a, b); send(sock, buf, strlen(buf), 0);

然后读取服务器返回的 JSON,从中提取result字段。

5.5 服务器处理请求

服务器端注册的add函数示例:

c

cJSON* add(jrpc_context *ctx, cJSON *params, cJSON *id) { cJSON *a = cJSON_GetArrayItem(params, 0); cJSON *b = cJSON_GetArrayItem(params, 1); int sum = a->valueint + b->valueint; return cJSON_CreateNumber(sum); // 返回结果会自动封装成 {"result": sum} }
5.6 客户端程序解析数据

客户端收到响应后,用 cJSON 解析:

c

cJSON *root = cJSON_Parse(recv_buf); cJSON *result = cJSON_GetObjectItem(root, "result"); int sum = result->valueint; cJSON_Delete(root);
5.7 上机实验(Ubuntu PC 上)
  1. 安装依赖:sudo apt install libtool autoconf make gcc

  2. 编译 libev 和 jsonrpc-c(步骤略,详见您提供的资料)。

  3. 编译测试程序json-rpc_test

  4. 运行:

    bash

    ./rpc server & # 后台运行服务器 ./rpc add 3 4 # 输出 sum = 7 ./rpc hello 100ask # 输出 Hello, 100ask
  5. 也可以用netcat直接测试:

    bash

    echo '{"method": "add", "params": [2,4], "id": 2}' | nc localhost 1234

6. 基于 JSON-RPC 操作硬件

现在我们把前面的知识应用到真实的嵌入式开发板上,让后台程序控制 LED 和 DHT11 温湿度传感器,前台 Qt 程序通过网络远程调用。

6.1 功能目标
  • 前台程序可以发送led_control请求,让后台打开或关闭某个 LED。
  • 前台程序可以发送dht11_read请求,让后台读取温湿度,并返回数值。
6.2 编写后台程序

后台程序需要:

  1. 实现硬件操作函数(比如读写/sys/class/leds/dev/dht11)。
  2. 将这些函数包装成 RPC 可调用的形式(参数和返回值都是 cJSON*)。

示例:LED 控制 RPC 方法

c

cJSON* rpc_led_control(jrpc_context *ctx, cJSON *params, cJSON *id) { cJSON *led_num_item = cJSON_GetArrayItem(params, 0); cJSON *status_item = cJSON_GetArrayItem(params, 1); if (!led_num_item || !status_item) { return cJSON_CreateString("error: need [led, onoff]"); } int led = led_num_item->valueint; int on = status_item->valueint; // 假设 led_control() 是真正的硬件操作函数 int ret = led_control(led, on); return cJSON_CreateString(ret == 0 ? "OK" : "FAIL"); }

然后在main中注册:

c

jrpc_register_procedure(&server, rpc_led_control, "led_control", NULL); jrpc_register_procedure(&server, rpc_dht11_read, "dht11_read", NULL);
6.3 交叉编译与上机实验

因为开发板通常是 ARM 架构,我们需要在 PC 上用交叉编译工具链编译。

  1. 交叉编译 libev

    bash

    ./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp make && make install
  2. 交叉编译 jsonrpc-c,指定 libev 的头文件和库路径。

  3. 编译后台程序rpc_server,链接 libev 和 jsonrpc-c。

  4. 编译前台程序(稍后介绍)。

  5. 将编译好的rpc_server和 Qt 程序通过adb push或 NFS 拷贝到开发板。

  6. 在开发板上运行:

    bash

    ./rpc_server & ./qt_app
6.4 使用多线程改进后台程序

原始的jsonrpc-c库是单线程的:一次只能处理一个客户端的请求,如果这个请求执行时间很长(比如读取网络或等待传感器稳定),其他客户端就会被阻塞。

改进方法:在服务器中,每accept一个新连接,就创建一个新的线程(或进程)去处理该连接上的所有后续 RPC 请求。

伪代码:

c

while (1) { client_fd = accept(listen_fd, ...); pthread_create(&thread_id, NULL, client_handler, &client_fd); pthread_detach(thread_id); } void *client_handler(void *arg) { int fd = *(int*)arg; // 在这个线程中,使用这个 fd 进行 jrpc 处理 // 直到客户端断开 close(fd); return NULL; }

这样,即使一个客户端在读取慢速设备,也不会影响其他客户端的请求响应。


7. 基于 JSON-RPC 改造 Qt 程序

最后,我们把原来的 Qt 程序(里面直接调用硬件函数)改成通过 RPC 远程调用后台程序。

7.1 合并程序(修改 Qt 代码)

原来 Qt 程序中可能有这样的代码:

cpp

void MainWindow::on_ledButton_clicked() { led_control(0, 1); // 直接操作硬件 }

现在我们要把它改成:

cpp

void MainWindow::on_ledButton_clicked() { // 通过 RPC 调用后台的 led_control 方法 callRpc("led_control", {0, 1}); }

具体步骤

  1. 在 Qt 项目的.pro文件中添加QT += network

  2. 包含头文件#include <QTcpSocket>,并声明一个QTcpSocket *socket

  3. 在构造函数或初始化函数中连接后台服务器:

    cpp

    socket = new QTcpSocket(this); socket->connectToHost("127.0.0.1", 1234); // 后台地址和端口 if (!socket->waitForConnected(3000)) { qDebug() << "连接后台失败"; }
  4. 实现一个通用的callRpc函数,它负责:

    • 构造 JSON 请求(包括 method、params、id)。
    • 通过 socket 发送。
    • 等待并读取响应。
    • 解析 JSON,返回 result 部分。
  5. 把所有原来操作硬件的地方,都替换成调用callRpc

注意:为了避免界面卡顿,callRpc中读取响应时应该用事件循环或异步方式,不要直接阻塞 UI 线程。简单起见,可以使用QEventLoop配合readyRead信号来实现同步等待。

7.2 上机实验

我们提供了已经改好的 Qt 程序压缩包LED_and_TempHumli.tar.bz2,以及自启动脚本rcSS99myqt

部署步骤

  1. 在 Ubuntu 上通过 adb 把文件推送到开发板:

    bash

    adb push LED_and_TempHumi /root/ adb push rpc_server /root/ adb push rcS /etc/init.d/ adb push S99myqt /etc/init.d/
  2. 在开发板上设置可执行权限:

    bash

    chmod +x /root/LED_and_TempHumi chmod +x /root/rpc_server chmod +x /etc/init.d/rcS chmod +x /etc/init.d/S99myqt
  3. 手动测试:

    bash

    /root/rpc_server & # 启动后台 /root/LED_and_TempHumi # 启动 Qt 界面
  4. 如果一切正常,可以重启开发板,系统会自动启动后台和 Qt 程序(通过S99myqt脚本)。

这样,您就完成了一个完整的前后台分离的嵌入式应用。以后无论是换 LED 引脚,还是换温湿度传感器,都只需要修改后台程序;想要修改界面风格,只需要修改 Qt 程序。两个部分互不干扰,开发和维护都轻松很多。


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/14 20:51:24

AO3镜像站终极指南:7个关键步骤轻松访问全球最大同人创作平台

AO3镜像站终极指南&#xff1a;7个关键步骤轻松访问全球最大同人创作平台 【免费下载链接】AO3-Mirror-Site 项目地址: https://gitcode.com/gh_mirrors/ao/AO3-Mirror-Site Archive of Our Own&#xff08;AO3&#xff09;镜像站项目致力于为无法直接访问原站的用户提…

作者头像 李华
网站建设 2026/4/14 20:45:48

5分钟掌握3D模型体积计算:STL文件分析完全指南

5分钟掌握3D模型体积计算&#xff1a;STL文件分析完全指南 【免费下载链接】STL-Volume-Model-Calculator STL Volume Model Calculator Python 项目地址: https://gitcode.com/gh_mirrors/st/STL-Volume-Model-Calculator 你是否曾经需要快速估算3D打印模型的材料用量&…

作者头像 李华
网站建设 2026/4/14 20:45:44

华为FusionStorage分布式存储技术解析:架构优势与应用场景

1. 华为FusionStorage的核心架构设计 第一次接触FusionStorage时&#xff0c;我被它彻底颠覆了传统存储的架构设计所震撼。这套分布式存储系统采用全对称分布式架构&#xff0c;所有节点完全对等&#xff0c;没有传统存储中的集中式控制器瓶颈。在实际部署中&#xff0c;我们团…

作者头像 李华
网站建设 2026/4/14 20:45:13

深入理解tempfile.mkstemp:从文件描述符到安全删除的完整流程

深入理解tempfile.mkstemp&#xff1a;从文件描述符到安全删除的完整流程 在Python开发中&#xff0c;处理临时文件是一个看似简单却暗藏玄机的任务。想象一下这样的场景&#xff1a;你的程序需要生成一个中间文件用于数据处理&#xff0c;这个文件只存在于程序运行期间&#x…

作者头像 李华