1. 为什么嵌入式系统需要轻量级Web服务?

如果你玩过树莓派或者类似的开发板,肯定想过一个问题:怎么才能方便地远程控制它?比如,我想在办公室的电脑上看看家里树莓派的CPU温度,或者调整一下智能花盆的浇水参数。这时候,一个运行在设备上的Web界面就成了最直观、最通用的选择。它不需要你在电脑上安装任何特殊软件,打开浏览器,输入设备IP,就能搞定一切。

但问题来了,我们熟悉的那些“大家伙”——比如Apache,功能确实强大,可动辄几十MB的内存占用,对资源捉襟见肘的嵌入式设备来说,实在是太奢侈了。这就好比在一辆微型电动车上装一台V8发动机,不仅浪费,还跑不起来。嵌入式系统的核心诉求是在有限的资源(CPU、内存、存储)下,稳定、高效地完成特定任务。因此,我们需要一个“小而美”的解决方案。

这就是lighttpd(发音是“lighty”)闪亮登场的场景。我把它比作是Web服务器里的“瑞士军刀”,体积小巧,但该有的功能一个不少。它的设计哲学就是事件驱动、单进程、低内存开销,特别适合并发连接数不是特别巨大,但对资源极其敏感的嵌入式环境。我实测过,一个基础的lighttpd服务,内存占用可以轻松控制在几MB以内,这对于只有几十MB甚至更少内存的设备来说,简直是福音。

不过,光有静态页面展示还不够。我们往往需要设备能“动”起来,能根据我们的指令执行操作、返回动态数据(比如实时传感器读数)。这就需要一种技术,让Web服务器能和后端的应用程序“对话”。传统CGI技术每次请求都“创建进程-处理-销毁”,开销巨大,在嵌入式设备上这么搞,系统很快就会被拖垮。而FastCGI的出现,完美解决了这个问题。它让应用进程常驻内存,Web服务器通过一个高效的协议(通常是Unix Socket或TCP)与这些“长工”通信,省去了反复“招工、培训、解雇”的繁琐过程,性能提升是数量级的。

所以,lighttpd + FastCGI这个组合,就成了嵌入式Web应用开发的经典搭档。一个负责高效地处理HTTP协议和网络I/O,一个负责稳定地执行业务逻辑,两者通过清晰的接口协作,共同在资源受限的舞台上,演出一场功能完备的Web服务大戏。接下来,我就带你从零开始,手把手搭建并优化这套系统,分享一些我这些年踩过坑才总结出来的实战经验。

2. 动手之前:理解FastCGI的工作机制

在撸起袖子编译代码之前,我觉得有必要先把FastCGI的原理掰扯清楚。这能帮你后面理解配置参数时,知道为什么要这么调,出了问题也知道该往哪个方向排查。

你可以把整个系统想象成一家餐厅。lighttpd就是前台接待和传菜员,它的职责非常专一:迎接客人(客户端请求),把菜单(HTTP请求)准确无误地送到后厨(FastCGI应用),然后把做好的菜(HTTP响应)端给客人。FastCGI应用就是后厨里的大厨,他们负责根据菜单烹饪菜肴(执行业务逻辑)。

传统CGI模式是:每来一位客人,前台就现去招聘一位大厨,大厨做完一道菜就立刻被解雇。这效率可想而知,大部分时间都浪费在“招聘”和“解雇”上了。FastCGI则不同,它提前雇好了几位大厨(进程),让他们一直在后厨待命。前台收到菜单后,通过一个传菜窗口(通常是Unix Socket文件)递给后厨,后厨里空闲的大厨接单、烹饪、出菜,然后继续等待下一单。这个传菜窗口就是FastCGI协议,它规定了菜单和菜肴的标准化格式。

这个过程有几个关键点,直接对应到我们的配置上:

  1. 进程常驻:大厨(FastCGI进程)是长期存在的,避免了每次创建进程的巨大开销。这是性能提升的根本。
  2. 进程池管理:后厨不会无限招人。我们会根据餐厅的规模和客流量(系统资源和并发请求量),决定雇佣几位大厨。这就是配置里的 max-procs 参数。比如设为2,就意味着有两个FastCGI进程在后台随时待命。
  3. 通信方式:传菜可以用窗口(Unix Socket),也可以派人跑腿(TCP Socket)。在嵌入式系统里,Unix Socket是更优选择,因为它不经过网络协议栈,是纯粹的本地进程间通信,速度更快、开销更小。我们的配置里就会使用 socket => "/tmp/fastcgi.socket" 这样的方式。
  4. 请求复用:一位大厨可以连续做很多道菜。FastCGI进程在处理完一个请求后,会立刻回到等待状态,准备处理下一个。为了预防大厨太累或者状态异常(内存泄漏),我们还可以设置一个“连续做菜上限”,这就是 PHP_FCGI_MAX_REQUESTS 这类环境变量的作用,达到上限后,管理器会优雅地重启这个进程。

理解了这套“餐厅模型”,再看lighttpd的FastCGI配置模块,你就会觉得特别直观。它本质上就是在定义:后厨在哪里(socket路径)、大厨是谁(bin-path)、雇几个大厨(max-procs)、以及大厨的工作环境(bin-environment)。搞明白了原理,配置就不再是死记硬背,而是按需调整的艺术了。

3. 从零构建:交叉编译与环境部署

好了,理论铺垫得差不多了,咱们开始动手。嵌入式开发的第一步,永远是在你的高性能开发主机(通常是x86_64的Linux PC)上,为你的嵌入式目标板(可能是ARM、MIPS等)搭建交叉编译环境。这就像是在你的电脑上制造一个能在目标板上运行的“零件”。

3.1 准备你的“零件加工厂”

首先,确保你的Ubuntu或Debian主机上安装了必要的工具。我们以最常见的ARM架构为例:

# 更新软件包列表
sudo apt update

# 安装ARM交叉编译工具链
sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf

# 安装编译所需的通用工具,如autoconf, automake, libtool等
sudo apt install build-essential autoconf automake libtool pkg-config

# 可选但推荐:安装调试和依赖检查工具
sudo apt install file binutils-dev

这里 arm-linux-gnueabihf 中的 hf 代表硬浮点,如果你的目标板支持硬件浮点运算单元(现在绝大多数都支持),就用这个,性能更好。如果不确定,可以用不带 hf 的版本 arm-linux-gnueabi。

接下来,我们需要两个核心“零件”的源码:lighttpd和FastCGI开发库。

# 创建一个专门的工作目录,避免混乱
mkdir ~/embedded_web && cd ~/embedded_web

# 下载lighttpd源码(请替换为最新稳定版链接)
wget https://download.lighttpd.net/lighttpd/releases-1.4.x/lighttpd-1.4.71.tar.gz
tar xzf lighttpd-1.4.71.tar.gz
cd lighttpd-1.4.71

3.2 编译“轻量级前台”:lighttpd

编译嵌入式软件的关键在于 configure 步骤,我们要通过参数告诉编译系统:“请用ARM的编译器来编译,并且安装到我指定的临时目录,而不是我的主机系统。”

# 配置编译选项,针对ARM平台
./configure --host=arm-linux-gnueabihf \
            --prefix=/usr/local/lighttpd \
            --without-bzip2 \
            --disable-ipv6 \
            --with-openssl=no \
            --with-pcre=no \
            --with-zlib=yes \
            --with-fam=no \
            --enable-static=yes \
            --enable-shared=no \
            --with-fastcgi

这里我解释几个关键参数:

  • --host=arm-linux-gnueabihf:这是最重要的,指定交叉编译器。
  • --prefix=/usr/local/lighttpd:指定安装目录结构。注意,这只是在编译时定义的目录布局。
  • --with-openssl=no --with-pcre=no:在资源极其紧张时,可以禁用SSL和正则表达式库以进一步减小体积。如果你的应用需要HTTPS,则必须保留 --with-openssl。
  • --enable-static=yes --enable-shared=no:编译成静态链接库。这会让生成的可执行文件变大一点,但部署时无需携带额外的 .so 动态库文件,更简单,依赖性更少。你可以根据实际情况选择。
  • --with-fastcgi:必须启用FastCGI支持。

配置完成后,开始编译和“安装”到我们自定义的目录:

# 编译
make -j$(nproc)

# 安装到当前目录下的 `_install` 文件夹,而不是真正的系统目录
make install DESTDIR=$(pwd)/_install

编译完成后,_install 目录下就会出现我们需要的 usr/local/lighttpd 目录结构,里面的 sbin/lighttpd 就是ARM平台的可执行文件。用 file 命令验证一下:

file _install/usr/local/lighttpd/sbin/lighttpd
# 应该输出类似:lighttpd: ELF 32-bit LSB executable, ARM, version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=..., stripped

3.3 编译“后厨通信协议”:FastCGI库

接下来编译FastCGI的开发库,我们的应用需要链接它。

cd ~/embedded_web
wget https://github.com/FastCGI-Archives/fcgi2/archive/refs/tags/2.4.2.tar.gz -O fcgi-2.4.2.tar.gz
tar xzf fcgi-2.4.2.tar.gz
cd fcgi2-2.4.2

# 生成配置脚本
./autogen.sh

# 配置交叉编译
./configure --host=arm-linux-gnueabihf \
            --prefix=/usr/local/fcgi \
            --enable-static=yes \
            --enable-shared=no

# 编译和安装到临时目录
make -j$(nproc)
make install DESTDIR=$(pwd)/_install

同样,检查 _install/usr/local/fcgi/lib 目录下是否生成了 libfcgi.a(静态库)。

3.4 部署到嵌入式设备

现在,我们把编译好的“零件”搬运到目标板上。假设你已经通过NFS挂载或者SD卡拷贝的方式,能访问目标板的根文件系统(例如路径是 /mnt/rootfs)。

# 复制lighttpd
sudo cp -r ~/embedded_web/lighttpd-1.4.71/_install/usr/local/lighttpd /mnt/rootfs/usr/local/
# 复制FastCGI库(如果你的应用是动态链接,才需要这个)
sudo cp -r ~/embedded_web/fcgi2-2.4.2/_install/usr/local/fcgi /mnt/rootfs/usr/local/

# 在目标板根文件系统上创建必要的运行时目录
sudo mkdir -p /mnt/rootfs/var/{log,run,www}/lighttpd
sudo mkdir -p /mnt/rootfs/var/cache/lighttpd/compress
sudo chown -R www-data:www-data /mnt/rootfs/var/{log,run,www}/lighttpd /mnt/rootfs/var/cache/lighttpd

这里创建了日志、运行文件、网站根目录和缓存目录,并设置了权限(假设你使用 www-data 用户运行lighttpd,这是常见做法)。至此,基础环境就准备妥当了。

4. 核心配置:让lighttpd与FastCGI协同工作

配置是让整个系统跑起来的关键。lighttpd的配置文件语法清晰,但有些细节不注意就会掉坑里。我把一个经过实战打磨的配置文件拆开给你讲。

4.1 主配置文件骨架

首先,在目标板的 /usr/local/lighttpd/etc/lighttpd.conf 创建主配置文件。

# 加载必要的模块。mod_fastcgi是核心,mod_access做访问控制,mod_alias做别名,mod_compress做压缩省流量。
server.modules = (
    "mod_access",
    "mod_alias",
    "mod_compress",
    "mod_fastcgi"
)

# 服务器基础设置
server.document-root = "/var/www/lighttpd" # 静态网页就放在这里
server.upload-dirs = ( "/var/cache/lighttpd/uploads" )
server.errorlog = "/var/log/lighttpd/error.log"
server.pid-file = "/var/run/lighttpd.pid"
server.username = "www-data" # 以低权限用户运行,安全!
server.groupname = "www-data"
server.port = 80 # 监听端口,嵌入式设备常用80

# 索引文件:当访问目录时,按顺序寻找这些文件
index-file.names = ( "index.html", "index.php", "index.fcgi" )

# 静态文件压缩,对嵌入式设备的网络带宽非常友好
compress.cache-dir = "/var/cache/lighttpd/compress/"
compress.filetype = (
    "application/javascript",
    "text/css",
    "text/html",
    "text/plain",
    "application/json"
)

4.2 FastCGI配置详解:连接“前台”与“后厨”

这是最核心的部分,配置lighttpd如何与你的FastCGI应用对话。

# FastCGI 服务器配置
fastcgi.server = (
    # 映射规则:将所有以 .fcgi 结尾的请求,或者 /api/ 路径下的请求,转发给FastCGI进程
    ".fcgi" => (
        "localhost" => ( # 给这组进程起个名字,叫localhost
            # 使用Unix Socket通信,比TCP更快,更安全(仅本地访问)
            "socket" => "/var/run/lighttpd/fastcgi.socket",
            # 你的FastCGI应用程序的绝对路径
            "bin-path" => "/usr/local/bin/myapp.fcgi",
            # 最重要的参数之一:启动多少个FastCGI工作进程。
            # 嵌入式设备上,通常1-2个就够了。太多会浪费内存,太少可能阻塞请求。
            "max-procs" => 1,
            # 传递给FastCGI进程的环境变量
            "bin-environment" => (
                # 如果你的FastCGI程序是PHP,这个表示每个进程创建多少子进程来处理请求。
                # 对于C/C++写的单进程程序,这个参数通常不需要。
                # "PHP_FCGI_CHILDREN" => "0",
                # 单个进程处理多少个请求后自动重启,防止内存泄漏。对C程序也很有用!
                "FCGI_MAX_REQUESTS" => "10000",
                # 工作目录
                "PWD" => "/usr/local/bin"
            ),
            # 从lighttpd继承哪些环境变量
            "bin-copy-environment" => ( "PATH", "SHELL", "USER" ),
            # 如果FastCGI应用对SCRIPT_FILENAME变量处理有问题,启用此项
            "broken-scriptfilename" => "enable",
            # 请求超时时间(秒),防止挂死的请求拖垮系统
            "check-local" => "disable",
            "allow-x-send-file" => "enable"
        )
    ),
    # 另一个例子:将特定路径映射到不同的FastCGI应用
    "/api/" => (
        "api-backend" => (
            "socket" => "/var/run/lighttpd/api.socket",
            "bin-path" => "/usr/local/bin/api_service.fcgi",
            "max-procs" => 2
        )
    )
)

关于 max-procs,我多说两句。在嵌入式设备上,这个值不是越大越好。因为每个进程都会占用一份内存。如果你的应用逻辑简单,处理请求很快,设1个进程可能就足够了。如果请求处理比较耗时(比如涉及复杂的计算或I/O),可以设为2,让一个进程处理请求时,另一个能响应新的请求,提高并发能力。你需要根据实际压力测试来调整。

4.3 访问控制与URL重写

为了安全和灵活,我们还需要一些额外配置。

# 访问控制:禁止访问隐藏文件(如.htaccess, .git)
url.access-deny = ( "~", ".inc", ".git" )

# URL重写:让URL更美观,或者做路由转发。
# 例如,把所有对 /app/ 的访问,都交给 /usr/local/bin/myapp.fcgi 处理
url.rewrite-once = (
    "^/app/(.*)$" => "/usr/local/bin/myapp.fcgi/$1"
)

# 限制客户端连接,防止资源被耗尽
connection.kbytes-per-second = 1024 # 每个连接最大带宽 1MB/s
server.max-keep-alive-requests = 16 # 一个连接上最多处理16个请求
server.max-keep-alive-idle = 5 # 空闲连接5秒后关闭

配置完成后,强烈建议先用 -t 参数测试配置文件语法:

/usr/local/lighttpd/sbin/lighttpd -t -f /usr/local/lighttpd/etc/lighttpd.conf

如果显示“Syntax OK”,恭喜你,配置文件的坑基本都避开了。

5. 编写你的第一个FastCGI应用(C语言示例)

配置好了服务器,我们来写一个真正的“后厨大厨”——FastCGI应用。这里用一个C语言的例子,它比脚本语言(如PHP)性能更高,更适合嵌入式环境。

假设我们要做一个简单的设备信息查询接口。

/* device_info.fcgi.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#include "fcgi_stdio.h" // 关键头文件

int main(void) {
    // FastCGI应用的主循环:等待并处理请求
    while (FCGI_Accept() >= 0) {
        // 1. 获取一些简单的系统信息
        struct sysinfo si;
        sysinfo(&si);
        long uptime = si.uptime;
        long total_ram = si.totalram / (1024 * 1024); // 转换为MB
        long free_ram = si.freeram / (1024 * 1024);

        // 2. 必须首先输出HTTP响应头
        printf("Content-type: application/json\r\n"); // 我们返回JSON格式
        printf("\r\n"); // 空行分隔头部和正文

        // 3. 输出响应正文(JSON格式)
        printf("{\n");
        printf("  \"status\": \"success\",\n");
        printf("  \"data\": {\n");
        printf("    \"hostname\": \"%s\",\n", getenv("SERVER_NAME"));
        printf("    \"uptime_seconds\": %ld,\n", uptime);
        printf("    \"memory_mb\": {\n");
        printf("      \"total\": %ld,\n", total_ram);
        printf("      \"free\": %ld\n", free_ram);
        printf("    },\n");
        printf("    \"client_ip\": \"%s\"\n", getenv("REMOTE_ADDR"));
        printf("  }\n");
        printf("}\n");

        // 注意:不需要手动关闭stdout,FCGI库会处理
    }
    return 0;
}

这个程序做了几件事:

  1. 进入一个 while(FCGI_Accept() >= 0) 循环,这是FastCGI程序的标志。它会阻塞直到收到新请求。
  2. 调用 sysinfo 获取系统运行时间和内存信息。
  3. 通过 printf 输出HTTP响应。切记:必须先输出 Content-type 和空行。
  4. 我们这里返回的是JSON格式的数据,方便前端或其他程序解析。

接下来,用交叉编译工具链编译它:

cd ~/embedded_web
# 编译,链接FastCGI静态库,并静态链接C标准库,生成独立的可执行文件
arm-linux-gnueabihf-gcc -o device_info.fcgi device_info.fcgi.c \
    -I./fcgi2-2.4.2/_install/usr/local/fcgi/include \
    -L./fcgi2-2.4.2/_install/usr/local/fcgi/lib \
    -lfcgi -static

-static 参数很重要,它会把所有依赖库(包括libfcgi.a和libc.a)都打包进最终的可执行文件,这样部署到目标板时,就不需要额外安装任何运行时库了,真正做到开箱即用。编译成功后,把 device_info.fcgi 复制到目标板的 /usr/local/bin/,并修改lighttpd配置中 bin-path 指向它,重启服务,访问 http://设备IP/device_info.fcgi 就能看到返回的JSON数据了。

6. 性能调优与稳定性实战技巧

系统能跑起来只是第一步,在嵌入式环境下,我们还得让它跑得稳、跑得快。下面是我总结的几个关键调优点。

1. 内存限制是生命线 嵌入式设备内存小,必须严防内存泄漏。除了在FastCGI配置中设置 FCGI_MAX_REQUESTS(例如10000次请求后重启进程)外,还可以使用系统的 ulimit 命令来限制lighttpd进程本身的内存使用。可以在启动脚本中加入:

# 在启动lighttpd的脚本中,例如 /etc/init.d/lighttpd
ulimit -v 1572864 # 限制虚拟内存为1.5GB左右,根据设备调整
ulimit -m 1048576 # 限制常驻内存集为1GB
/usr/local/lighttpd/sbin/lighttpd -f /usr/local/lighttpd/etc/lighttpd.conf

2. 连接数管理与超时设置 嵌入式设备处理并发能力有限,必须合理限制。

# 在lighttpd.conf中
server.max-connections = 1024 # 最大总连接数
server.max-fds = 2048 # lighttpd能打开的最大文件描述符数,通常为max-connections的2倍
server.max-worker = 1 # lighttpd是单进程模型,这个值保持为1
# FastCGI特定超时
fastcgi.debug = 0 # 生产环境关闭debug日志
fastcgi.timeout = 30 # FastCGI后端响应超时时间,防止慢请求堆积

3. 静态文件服务优化 如果你的Web服务也提供静态文件(如图片、CSS、JS),启用压缩和缓存能极大提升体验并节省带宽。

# 启用并配置mod_compress
compress.cache-dir = "/var/cache/lighttpd/compress/"
compress.filetype = ( "text/", "application/javascript", "application/json", "application/xml" )
compress.max-filesize = 102400 # 只压缩小于100KB的文件,大文件压缩消耗CPU可能不划算

# 设置静态文件缓存头,让浏览器缓存
$HTTP["url"] =~ "\.(jpg|jpeg|png|gif|ico|css|js)$" {
    expire.url = ( "" => "access 7 days" )
}

4. 日志轮转与监控 嵌入式设备的存储空间有限,不能让日志无限增长。使用 logrotate 工具定期轮转和清理日志。创建一个 /etc/logrotate.d/lighttpd 文件:

/var/log/lighttpd/*.log {
    daily
    missingok
    rotate 7 # 保留7天的日志
    compress
    delaycompress
    notifempty
    create 640 www-data www-data
    sharedscripts
    postrotate
        /bin/kill -USR1 $(cat /var/run/lighttpd.pid 2>/dev/null) 2>/dev/null || true
    endscript
}

7. 故障排查:当服务不听话时怎么办?

即使配置再仔细,上线后也难免遇到问题。别慌,按照这个顺序来排查,基本能解决90%的问题。

第一步:检查服务是否真的启动了

# 在目标板上执行
ps aux | grep lighttpd # 查看lighttpd进程是否存在
netstat -tulpn | grep :80 # 查看80端口是否在监听

如果进程不存在,去检查错误日志,这是最重要的线索:

tail -f /var/log/lighttpd/error.log

常见的启动错误:配置文件语法错误、端口被占用、没有权限创建socket文件或写入日志目录。

第二步:FastCGI应用是否正常工作? 如果lighttpd启动了,但访问你的 .fcgi 链接返回502 Bad Gateway或空白页,问题很可能出在FastCGI应用上。

  1. 检查应用本身:直接在目标板命令行运行你的FastCGI程序,看是否能正常启动,不报错。

    /usr/local/bin/myapp.fcgi
    

    如果直接运行就崩溃,那可能是交叉编译的环境问题,或者程序有bug。

  2. 检查Socket文件:确认lighttpd配置的socket路径(如 /var/run/lighttpd/fastcgi.socket)是否存在,并且权限正确(www-data用户可读写)。

    ls -la /var/run/lighttpd/
    
  3. 查看lighttpd的FastCGI错误:有时错误会记录在lighttpd的error.log里,提示“连接FastCGI失败”或“读/写超时”。

第三步:性能瓶颈分析 如果服务能访问但特别慢,或者高并发下挂掉。

  1. 系统资源:用 top 或 htop 命令,看是CPU满了,还是内存用光了(特别是Swap是否被频繁使用)。
  2. 进程状态:用 strace -p <pid> 跟踪lighttpd或FastCGI进程,看它卡在哪个系统调用上(可能是磁盘I/O、网络或死锁)。
  3. 网络连接:用 ss -tlnp 查看连接状态,是否有大量 TIME_WAIT 或 CLOSE_WAIT 的连接,这可能需要调整内核网络参数。

一个我踩过的坑:有一次部署后,FastCGI应用偶尔会无响应。排查很久才发现,是目标板的 /tmp 目录挂载在了tmpfs(内存盘)上,而内存太小,当并发请求多、生成临时文件时,把内存撑爆了,导致进程被系统杀死。后来把lighttpd的socket文件和临时目录改到了容量更大的Flash存储分区,问题就解决了。所以,嵌入式环境下,对存储介质和空间的考量要格外细致。

8. 进阶实战:构建一个设备监控仪表盘

最后,我们综合运用所学,打造一个稍微复杂点但非常实用的例子:一个完整的设备监控仪表盘。它不仅能显示信息,还能通过简单的表单执行命令(比如重启某个服务)。为了安全,我们只实现一个查看版本信息的只读操作。

这个应用由两部分组成:

  1. 前端HTML/JS:放在 /var/www/lighttpd/ 下,通过jQuery Ajax动态从后端获取数据并更新页面。
  2. 后端FastCGI应用:提供JSON API,处理数据查询和简单的命令调用。

这里重点展示后端的C代码,它提供了两个API端点:

  • GET /api/system_info:返回系统概览。
  • POST /api/execute:执行一个安全的预定义命令(此处仅作示例,实际生产环境需极其严格的校验和过滤)。
/* dashboard_api.fcgi.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sysinfo.h>
#include <unistd.h>
#include "fcgi_stdio.h"
#include "cJSON.h" // 引入cJSON库来方便生成和解析JSON

#define MAX_POST_DATA 4096

// 处理 /api/system_info
void handle_system_info() {
    struct sysinfo si;
    sysinfo(&si);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "success");
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "uptime", si.uptime);
    cJSON_AddNumberToObject(data, "total_ram_mb", si.totalram / 1024 / 1024);
    cJSON_AddNumberToObject(data, "free_ram_mb", si.freeram / 1024 / 1024);
    cJSON_AddNumberToObject(data, "load_1min", (double)si.loads[0] / (1 << SI_LOAD_SHIFT));
    cJSON_AddItemToObject(root, "data", data);

    char *json_str = cJSON_Print(root);
    printf("Content-Type: application/json\r\n\r\n%s", json_str);
    free(json_str);
    cJSON_Delete(root);
}

// 处理 /api/execute (极度简化的示例,生产环境必须做严格的白名单校验和输入过滤!)
void handle_execute(const char *post_data) {
    cJSON *root = cJSON_CreateObject();
    // 这里应该解析post_data中的JSON,获取命令参数,并进行严格的安全检查
    // 例如,只允许执行 "get_version" 这个命令
    cJSON *json = cJSON_Parse(post_data);
    char *command = NULL;
    if (json) {
        cJSON *cmd_item = cJSON_GetObjectItem(json, "command");
        if (cJSON_IsString(cmd_item)) {
            command = cmd_item->valuestring;
        }
    }

    if (command && strcmp(command, "get_version") == 0) {
        FILE *fp = popen("uname -a", "r");
        if (fp) {
            char buffer[256];
            if (fgets(buffer, sizeof(buffer), fp)) {
                cJSON_AddStringToObject(root, "status", "success");
                cJSON_AddStringToObject(root, "output", buffer);
            }
            pclose(fp);
        } else {
            cJSON_AddStringToObject(root, "status", "error");
            cJSON_AddStringToObject(root, "message", "Failed to execute command");
        }
    } else {
        cJSON_AddStringToObject(root, "status", "error");
        cJSON_AddStringToObject(root, "message", "Command not allowed or invalid");
    }

    if (json) cJSON_Delete(json);

    char *json_str = cJSON_Print(root);
    printf("Content-Type: application/json\r\n\r\n%s", json_str);
    free(json_str);
    cJSON_Delete(root);
}

int main(void) {
    char *request_method;
    char *content_length_str;
    long content_length;
    char *post_data = NULL;

    while (FCGI_Accept() >= 0) {
        request_method = getenv("REQUEST_METHOD");
        char *path_info = getenv("PATH_INFO");

        if (strcmp(path_info, "/system_info") == 0 && strcmp(request_method, "GET") == 0) {
            handle_system_info();
        } else if (strcmp(path_info, "/execute") == 0 && strcmp(request_method, "POST") == 0) {
            content_length_str = getenv("CONTENT_LENGTH");
            content_length = (content_length_str != NULL) ? atol(content_length_str) : 0;
            if (content_length > 0 && content_length < MAX_POST_DATA) {
                post_data = (char *)malloc(content_length + 1);
                fread(post_data, 1, content_length, stdin);
                post_data[content_length] = '\0';
                handle_execute(post_data);
                free(post_data);
            } else {
                printf("Content-Type: application/json\r\n\r\n{\"status\":\"error\",\"message\":\"Invalid content length\"}");
            }
        } else {
            printf("Content-Type: application/json\r\n\r\n{\"status\":\"error\",\"message\":\"API endpoint not found\"}");
        }
    }
    return 0;
}

这个例子展示了如何在一个FastCGI应用中处理不同的路由(PATH_INFO)和HTTP方法(GET/POST),以及如何解析简单的POST数据。请务必注意:生产环境中执行系统命令是极度危险的行为,必须构建严格的命令白名单和参数过滤机制,避免命令注入漏洞。这里仅为展示后端如何处理动态请求。

前端页面通过Ajax调用这些API,就能实时刷新显示CPU负载、内存使用率等信息,并提供一个安全的按钮来触发预定义的后台任务。通过这样的组合,你就用最精简的资源,在嵌入式设备上搭建起了一个功能丰富、交互友好的现代化Web管理界面。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐