使用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

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

特点 TCP UDP
连接性 面向连接。通信前必须先建立连接(三次握手),就像打电话。 无连接。直接把数据包发出去,就像寄信,不确认对方是否收到。
可靠性 可靠。丢包会重传,乱序会重组,保证数据完整有序到达。 不可靠。丢包、乱序都不管,只"尽最大努力"。
速度 较慢。因为要维护连接、确认、重传等,头部开销大(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 程序。两个部分互不干扰,开发和维护都轻松很多。


相关推荐
小码哥_常3 小时前
从0到1:Spring Boot 中WebSocket实战揭秘,开启实时通信新时代
后端
小码哥_常3 小时前
深度剖析:为什么Android选择了Binder
前端
lolo大魔王3 小时前
Go语言的异常处理
开发语言·后端·golang
方安乐4 小时前
单元测试之helper函数
前端·javascript·单元测试
音仔小瓜皮4 小时前
【Web八股】深入理解浏览器DOM事件流,灵活控制它!
前端·web
灼灼桃花夭5 小时前
js之阳历 → 农历(含时辰)转换函数
开发语言·前端·javascript
gyx_这个杀手不太冷静5 小时前
大人工智能时代下前端界面全新开发模式的思考(三)
前端·架构·ai编程
小李子呢02115 小时前
前端八股性能优化(1)---防抖和节流
开发语言·前端·javascript
IT_陈寒5 小时前
Python多进程共享变量那个坑,我差点没爬出来
前端·人工智能·后端