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++相关的知识
做项目挺有意思的😋,但拖延症导致我挖了很多坑没填

浙公网安备 33010602011771号