wakeonlan-tcp 开发复盘

wakeonlan-tcp 开发复盘

巴法云 TCP 协议的应用实践
项目地址:https://github.com/awalol/bemfa-wol-client

AI 摘要

本文记录了使用 C++20 开发轻量化远程开机工具 wakeonlan-tcp 的全过程。针对嵌入式设备存储空间受限的痛点,通过放弃 Golang 改用原生 C++ 与巴法云 TCP 协议通信,成功将程序体积从数 MB 缩减至 KB 级。文章重点分享了基于 getaddrinfo 的域名解析、利用 RAII 机制(jthread) 管理多线程心跳保活、以及通过“逻辑短路”优化重连逻辑的实践经验。

开发背景

主包有远程桌面的需求 (为了在外面远程串流打游戏) 因此需要远程开机。

这个项目有两个前身:
1.esp32版本,连接到点灯平台(Blinker)远程下发指令。但是主包的ESP32被主包玩坏了 (注意不要像主包那么傻把引脚接反啊!!!)
2.Golang版本,原理跟现在的类似,但是使用的是mqtt协议连接。因为golang本身需要塞个runtime进去,生成出来的体积有5.5MB,使用upx压缩以后还有1.6MB,路由器的flash空间太小只能放在tmp目录,重启就没了

为了熟悉c/c++基础组件的使用,所以这个项目选择了c++进行重写 (以后还有可能再用 rust 进行重写?) 反正都是练手

开发过程

协议的选择

为了尽可能减小程序的体积,所以不引入外部库,只使用c++内部的组件。通过巴法云的手册来看,巴法云可以通过TCP与MQTT两种协议接入。于是选择使用TCP协议(其实主包先是用MQTT写了一个版本,就是直接把Golang那个版本搬了过来,但编译出来的大小有5.4MB,这还不如Go写的版本。)

首先先做的肯定是核心部分 — 服务器通信!

Part1:建立TCP连接

C++的TCP通信是围绕 socket 进行的

首先先要进行初始化:
1.如果使用ip进行连接则使用 sockaddr_in 进行初始化,相关代码如下:

点击查看代码
// 1. 创建 socket
int sock = socket(AF_INET, SOCK_STREAM, 0);

// 2. 配置服务端地址
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
    
// 将 IP 地址从字符串转换为二进制
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

// 3. 连接服务端
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

2.如果使用域名连接,则需要先解析域名地址,使用 addrinfo 进行初始化,相关代码如下(也是本次使用的方法)

点击查看代码
addrinfo hints{}, *res; // hints 为 getaddrinfo 传入连接信息,res 为查询结果
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
const int status = getaddrinfo("bemfa.com", "8344", &hints, &res);
if (status != 0) { // 错误处理
    cerr << LOG_PREFIX << "getaddrinfo error: " << gai_strerror(status) << endl;
    return false;
}

sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

之后进行连接

connect(sock, res->ai_addr, res->ai_addrlen)

返回值:
成功:0,失败:-1

注意,无论连接成功与失败都要记得释放 addrinfo,避免内存泄漏

freeaddrinfo(res)

因为程序有重连的需求,所以需要设置超时时间

// 设置超时时间
timeval timeout{
    timeout.tv_sec = 5
};
setsockopt(sock,SOL_SOCKET,SO_RCVTIMEO, &timeout, sizeof(timeout));

这有个知识点,最初我是使用下面这个代码创建 timeout 变量的

timeval timeout;
timeout.tv_sec = 5; // 秒
timeout.tv_usec = 0; // 微秒

我好奇为什么 tv_usec 必须设置,AI 为我解答:
使用这种初始化方式可能会导致结构体里面成员的数值是随机的,而在 C++中,使用 {} 会对执行值初始化,将里面成员都自动初始化为 0

Part2:使用多线程进行消息处理以及发送心跳包保活

由于巴法云要求,我们需要每隔60s向巴法云发送个心跳包
这里我们使用C++20引入的新特性 - jthread,他对比以前的thread自动帮我们做了一些事,简便了开发的流程

首先先构建消息处理线程

线程函数结构:

  void messageReceiver(const stop_token &stopToken) {
    // 通过超时时间为 5 秒,避免在 read 阻塞造成重连事件不触发
    while (!stopToken.stop_requested()) {
        //其余代码请去仓库查看,这里只给出函数结构
    }
}

然后直接使用jthread创建线程
注意:创建完线程以后会立即开始

receiverThread = jthread(messageReceiver)

开发中有一点需要注意:read 函数是会阻塞代码执行的,如果有重连事件的一定要设置超时时间

Part3:构建main函数以及重连机制

点击查看代码
int main() {
    // 从环境变量获取参数初始化

    while (true) {
        if (sock == -1 && connectToServer()) {
            fail = 0;
            subscribeTopic();
            // 启动线程
            receiverThread = jthread(messageReceiver);
            heartbeatThread = jthread(heartbeat);
        }
        this_thread::sleep_for(chrono::seconds(60));
    }

    return 0;
}

在这里学到一个知识点:逻辑短路
在这行代码里

if (sock == -1 && connectToServer())

这里使用了 && 与条件,如果说 sock == -1 为 false,那么就不会继续执行 connectToServer 这个函数
若使用的是 || 条件,如 if (A || B) ,如果条件 A 为 true,那么就不会继续去判断 B

Part4: 编译

先说结论:主包的开发环境是macos,使用动态链接的方式目前还没研究出可行的musl交叉编译方案,因此采用的方案是:使用CLion的远程开发功能,将代码同步到Linux服务器上,使用编译路由器固件时候用的那套工具链。

编译是这个项目最主要的困难之一,有如下困难:
1.架构不同:路由器使用的是mipsel,与开发机使用的aarch64(arm64)不同,因此直接编译传过去是打不开的,因此我们需要配置交叉编译的工具链
2.使用的是轻量C标准库:路由器使用的是musl作为libc

方案一:使用现成的工具链

messense/homebrew-macos-cross-toolchains
这是一套现成的交叉编译工具链,但是唯独在 mipsel 这个架构里面没有编译好的带 musl 的工具链,因此我只能先安装gnu的版本试试
安装过程省略
但很可惜,这个方案失败了,编译后的程序传到路由器上运行显示:

K2P:/tmp # ./wakeonlan_tcp
-sh: ./wakeonlan_tcp: not found
转机?

在 FiloSottile/homebrew-musl-cross 这个仓库下面有一段话:

The "musl" part of the target is important: the binaries will ONLY run on a musl-based system, like Alpine. However, if you build them as static binaries by passing -static as an LDFLAG they will run anywhere. Musl is specifically engineered to support static binaries.

这提醒了我,我们可以使用静态链接的方式进行编译。我们只需要在 CMakeLists.txt 里面添加一行,以启动静态链接

set(CMAKE_EXE_LINKER_FLAGS "-static")

编译出来的大小有2MB,使用upx压缩完后有702KB,不够满意,但是上传到路由器后成功运行了

哦对了,在这期间也遇到了一个坑,可以查看上一篇随笔 无法通过 scp 上传文件至路由器解决方法

方案二:远程编译

既然在Linux平台能成功编译固件,那么我们可以使用现有的工具链进行接下来的编译

使用CLion的远程开发功能,连接方式:设置 -> 构建、执行、部署 -> 部署,位置如图

然后在工具链里面添加远程主机对应的工具链位置,如图(以个人安装的位置为准)

然后在 CMake 里面添加配置文件

完成,编译的时候选择对应的编译目标即可

结语

这次 wakeonlan-tcp 项目让我更深入了学习C++相关的知识
做项目挺有意思的😋,但拖延症导致我挖了很多坑没填

posted @ 2025-12-27 19:15  awalol  阅读(68)  评论(0)    收藏  举报