做物联网设备上云、搞嵌入式网络通信的朋友,对 libwebsockets 这个名字一定不陌生。这是一个用 C 语言实现的轻量级 WebSocket 协议库,附带 HTTP/1.1、HTTP/2 的部分能力,在资源受限的设备环境里非常受欢迎。最近我在做一个 Linux 嵌入式设备的远程管理项目,需要设备端和云端建立双向实时通道,既要主动上报传感器状态和心跳,又要能实时接收云端下发的控制指令,选型时考察了一圈,最终定下用 libwebsockets。但说实话,真正自己从源码下载、配置、编译、安装,再跑通测试,还是花了不少功夫。网上很多资料要么只讲个 apt install 完事,要么直接甩几条 cmake 命令让你自己猜,真正踩坑时找不到人问。
这篇就把这次完整走通的下载、编译、测试过程记录下来,包括我用到的参数、为什么这么配、编译后怎么验证、遇到哪些报错又是怎么解决的。如果你也在做类似的设备联网、WebSocket 通道搭建,或者只是想快速拿到一个能用的 libwebsockets 继续开发上层功能,这篇内容应该能帮你省下不少时间。
1. 项目概况:为什么要自己折腾 libwebsockets
1.1 libwebsockets 能做什么
从使用者的角度说,libwebsockets 把 WebSocket 协议栈里的繁琐细节几乎全部封装好了。你不需要关心 HTTP Upgrade 握手、数据帧解析、掩码处理、分片重组、连接保活这些底层逻辑,只需要注册一组协议回调函数,就能在连接建立、收到消息、连接关闭等事件里处理自己的业务数据。
它的核心能力包括:
- 单线程事件驱动模型,底层基于 poll 或自定义事件循环,适合嵌入式设备这种单核、低内存环境
- 同时支持 ws 和 wss,wss 只需要在编译时启用 OpenSSL 支持
- 自带 HTTP 静态文件服务和 HTTP 解析能力,简单场景下连 Web 服务端都不用单独部署
- 提供了一大批 minimal examples,从 ws-server、ws-client 到 HTTP server、HTTP client,覆盖了绝大多数常见用法
- 跨平台支持 Linux、Windows、macOS、FreeRTOS 等,交叉编译相对比较友好
我当时选它还有个重要原因:它不强制依赖一堆重型框架。相比用 Node.js 或 Python 做 WebSocket 服务端,C 库直接在设备上运行,内存占用可能只有几百 KB 级别,这对动不动只有 64MB 内存的嵌入式设备来说很关键。
1.2 为什么不用系统自带的库,非要自己编译
不少 Linux 发行版默认仓库里就有 libwebsockets-dev 这类开发包,按说直接用能省很多事。但实际做项目时会发现,直接用系统包往往不够用:
- 系统自带的版本通常比较旧,想用新的 API(比如 HTTP/2 支持、新的上下文创建方式)没有
- 默认编译参数不一定适合你的场景。比如你想用静态库、想关闭某些用不到的功能模块,系统包装不出来
- 嵌入式交叉编译时,需要在宿主机上搭好工具链,为目标架构单独编译一份库,这时候系统包能帮上什么忙?它无法满足交叉编译需求
- 希望把库裁剪到最小体积,只保留必须的协议特性,同样需要自己掌控编译选项
所以我这次选择直接从 GitHub 拉源码,在目标板上或者用交叉工具链编译。就算你只是在 PC 上开发调试,自己编译一遍也能更好理解这个库的组成,后续出了问题更容易排查。
1.3 这次搭建的整体流程
整个流程可以拆成四步:准备环境、下载源码、CMake 配置和编译、测试验证。测试我会多做一层,不只是跑通官方示例,还会写一个最小的 C 服务端程序,验证把 libwebsockets 集成进自己工程时的链接和运行情况。这样从库本身到应用层,链路是完整的。
2. 编译前的环境准备
2.1 工具链与依赖清单
先说明一下,我这里的环境是 Ubuntu 20.04 / 22.04 这种常见的 Linux 开发机。如果你用别的发行版,命令换一下包管理器就行,思路是一样的。
需要准备的依赖有:
- gcc、g++、make:基础编译工具链
- cmake:libwebsockets 使用 CMake 构建,版本最好在 3.16 以上
- git:拉取源码
- libssl-dev:OpenSSL 开发库,编译 wss 支持时必需
- zlib1g-dev:zlib 压缩库,用于 WebSocket 的 per-message-deflate 压缩扩展
在 Ubuntu 上一次性装好:
sudo apt update sudo apt install -y build-essential cmake git libssl-dev zlib1g-dev这里解释一下为什么需要 libssl-dev。如果你只跑明文 ws,不启用 SSL,那可以在 CMake 配置时把 LWS_WITH_SSL 关掉。但实际设备上云场景几乎都要走 wss,否则数据在链路上裸奔很容易被抓包。所以我还是建议装上 OpenSSL,编译时启用 SSL 支持,后续想用 wss 随时可用。
2.2 版本选择建议:用稳定 tag 而不是 master
libwebsockets 的 GitHub 仓库是 warmcat/libwebsockets,最新代码通常处于开发状态,API 变动会比较频繁。我做项目时不会直接用 master,而是选择一个稳定的 release tag。
常见的稳定版本有 v4.3.x、v4.4.x 和 v4.5.x 系列。不同版本之间 API 有一些差异,比如 v4.x 中部分创建连接的接口和回调机制就做过调整。我的建议是:
- 新项目选 v4.4.x 或更新的稳定 release,尽量别选太老的 v2.x、v3.x
- 如果有历史项目,一定要保持和原项目一致的版本,否则升级后可能面临大量 API 适配工作
- 不要轻易用 master 分支跑生产,除非你真的需要某个还没有正式发布的新特性
拉取源码并用 tag 切换版本:
git clone https://github.com/warmcat/libwebsockets.git cd libwebsockets git tag -l git checkout v4.3.3git tag -l 可以列出所有可用版本,挑一个你需要的稳定 tag 即可。我这里用 v4.3.3 举例,不代表它是最新的,以你实际看到的 tag 为准。
2.3 检查工具版本
编译前最好确认一下 cmake 版本:
cmake --version如果版本太低,后面配置容易出现兼容性报错。Ubuntu 20.04 自带的 cmake 3.16.3 编译 libwebsockets 基本够用;如果系统没有 cmake,可以用 pip 安装一个较新的版本,或者从 CMake 官网下载安装包,但常规做法是直接用 apt 装,省事。
3. CMake 配置的坑与实战参数
3.1 一次完整的 CMake 配置命令
libwebsockets 的构建系统是 CMake,配置这一步是整个编译过程中最容易出问题的地方。它提供了非常多的编译选项,用来控制功能模块的开关。我这次用的配置命令如下:
mkdir build && cd build cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DLWS_WITH_SSL=ON \ -DLWS_WITHOUT_TESTAPPS=OFF \ -DLWS_WITH_MINIMAL_EXAMPLES=ON \ -DLWS_WITH_STATIC=ON \ -DLWS_WITH_SHARED=ON这里特别提醒一点:如果你之前配置过一次,想修改选项重新配置,光改参数重新执行 cmake 可能不生效,因为 CMake 会缓存之前的配置。稳妥的做法是直接把 build 目录删掉重新建,或者至少删掉 build 目录里的 CMakeCache.txt,再执行 cmake。
我一开始就是在原有 build 目录里反复改参数,结果某些选项死活不生效,折腾了好久才反应过来是缓存的问题。后来老老实实每次配置前 rm -rf build。
3.2 关键 CMake 选项逐个拆解
下面这个表格整理了我用到的、以及常见的几个重要选项,方便你参考:
| 选项 | 默认值 | 含义 | 我的建议 |
|---|---|---|---|
| CMAKE_BUILD_TYPE | 空 | 构建类型,可选 Release / Debug | 正式使用选 Release,排查问题用 Debug |
| LWS_WITH_SSL | ON | 启用 OpenSSL,支持 wss | 保持 ON,否则 wss 不可用 |
| LWS_WITHOUT_TESTAPPS | OFF | 设为 ON 会跳过测试程序编译 | 做测试验证时设为 OFF |
| LWS_WITH_MINIMAL_EXAMPLES | OFF | 编译官方精简示例 | 刚开始建议 ON,拿来测试很方便 |
| LWS_WITH_STATIC | ON | 生成静态库 | 按需开启 |
| LWS_WITH_SHARED | ON | 生成动态库 | 按需开启 |
| LWS_WITH_HTTP2 | OFF | 启用 HTTP/2 支持 | 需要时打开 |
| LWS_WITH_ZLIB | ON | 启用 zlib 压缩 | 有 zlib 就保持 ON |
| LWS_WITH_LIBUV | OFF | 集成 libuv 事件循环 | 除非你需要 libuv,否则保持 OFF |
| LWS_WITH_CLIENT | ON | 启用客户端功能 | 默认即可 |
| LWS_WITH_SERVER | ON | 启用服务端功能 | 默认即可 |
LWS_WITH_MINIMAL_EXAMPLES 这个选项对测试特别重要。官方提供了一批短小精悍的示例程序,比如 minimal-ws-server、minimal-ws-client、minimal-http-server,编译后直接用这些程序就能验证库是否正常工作和学习如何使用 API。如果嫌编译这些示例增加时间,可以忍一忍,前期把它们编译出来,后面测试会非常方便。
LWS_WITHOUT_TESTAPPS 和 LWS_WITH_MINIMAL_EXAMPLES 是相互独立的一组开关。LWS_WITHOUT_TESTAPPS 控制的是更早的一套测试应用,minimal examples 则是后来统一整理的精简示例。做功能验证建议把这两个都打开,或者至少保证 minimal examples 是打开的。
3.3 交叉编译时的 CMake 配置
如果你要编译到 ARM 或其他嵌入式平台,需要在 CMake 里指定工具链文件(toolchain file)。大致思路是新建一个 .cmake 文件,内容类似这样:
set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) set(CMAKE_FIND_ROOT_PATH /path/to/your/rootfs)然后在 build 目录里配置:
cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm-linux-gnueabihf.toolchain.cmake注意:交叉编译时,OpenSSL 必须是目标架构的版本,不能直接复用宿主机上 x86 的 libssl-dev。更省事的做法是,如果业务场景纯走 ws 而不需要 wss,可以显式把 LWS_WITH_SSL 设为 OFF,这样就不用处理 OpenSSL 交叉编译的牵扯了。我在早期原型验证阶段就是这么干的,先把协议链路跑通,后面要上 wss 再补 OpenSSL。
4. 编译与安装
4.1 编译过程与产物检查
配置完 CMake 之后,进入编译:
make -j$(nproc)-j 参数表示并行编译,后面的数字是并行任务数,$(nproc) 会自动读取 CPU 核心数。注意不要一次性用太多并发,内存小的机器编译时容易被 oom-killer 杀掉。我有一台 2 核 4GB 内存的旧机器,之前习惯性直接 make -j8,编到一半进程就没了,换成 make -j2 就稳定很多。
编译完成后,重点看两个目录:
- build/bin:编译出来的可执行文件,包括 minimal examples 和测试程序
- build/lib:编译出来的库文件,libwebsockets.a 和 libwebsockets.so 都在这
可以这样确认:
ls -la build/bin | head -n 30 ls -la build/lib不同版本里可执行程序的命名可能有差异,有的版本直接叫 minimal-ws-server,有的版本前面带 lws- 前缀,比如 lws-minimal-ws-server。所以别记死名字,以你实际 ls 出来的结果为准。
静态库和动态库的区别这里不展开多说,只提一句:如果你要在嵌入式设备上部署,静态库更省事,不用处理设备上的动态库依赖;如果你在 PC 上做快速原型开发,动态库编译更快,链接也省事。我这次两个都编了,编译参数里同时打开 LWS_WITH_STATIC 和 LWS_WITH_SHARED 即可。
4.2 安装到系统目录与动态库路径
编译成功之后,如果想把库安装到系统目录:
sudo make install默认安装路径是:
- 头文件:/usr/local/include/libwebsockets.h 等
- 库文件:/usr/local/lib/libwebsockets.so 和 libwebsockets.a
- pkg-config 文件:/usr/local/lib/pkgconfig/libwebsockets.pc
安装完成后,动态链接库的路径可能需要刷新一下:
sudo ldconfig这里有个小细节:如果你不执行 ldconfig,或者你的 /usr/local/lib 不在默认搜索路径里,运行测试程序时可能会报错找不到 libwebsockets.so。解决办法是在当前终端导出:
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH我后面测试时就碰到过这个问题,编译完全成功,一运行程序就提示 error while loading shared libraries: libwebsockets.so.16: cannot open shared object file,其实就是动态库路径没找到。
5. 功能测试:从回环到真实场景
5.1 用官方 minimal examples 做快速自测
编译完成后,我建议先不急着写自己的代码,先用官方示例验证库本身是好的。这一步能排除“库编译有问题”这个最大的隐患。
先启动一个最简单的 WebSocket 服务器:
cd build/bin ./lws-minimal-ws-server程序跑起来后,会监听 7681 端口,日志里会出现 libwebsockets build、starting 之类的内容。此时再打开一个终端,启动配套的客户端:
cd build/bin ./lws-minimal-ws-client -s localhost -p 7681如果客户端成功连上服务器,两端日志都会显示连接建立,并且客户端会周期性地向服务器发送消息。这个回环演示能直接说明库的协议栈、事件循环、收发路径都是通的。
我建议把日志保存一份,观察几个关键点:
- 服务器是否成功绑定端口
- 客户端握手是否成功完成
- 服务器是否收到客户端消息并作出回应
如果这些都正常,说明库编译得没有大问题,可以进入下一步。
5.2 用浏览器和第三方工具做补充验证
官方示例能跑通,只能说明库自身工作正常。但 WebSocket 是个跨语言协议,我要确认它跟浏览器、跟其他语言客户端也能正常通信,避免将来被别人接不上。
浏览器打开 http://localhost:7681 就能看到 minimal-ws-server 示例自带的测试页面,上面有连接按钮和消息收发界面。这其实是最直观的验证方式,因为浏览器内置了成熟的 WebSocket 客户端。
如果不想用浏览器,也可以用 Python 快速验证。只需要在虚拟环境里安装 websockets 库:
pip install websockets然后运行:
import asyncio import websockets async def test(): uri = "ws://127.0.0.1:7681" async with websockets.connect(uri) as ws: await ws.send("hello from python") resp = await ws.recv() print(f"received: {resp}") asyncio.run(test())这个脚本能连上就说明 libwebsockets 的服务端对标准 WebSocket 客户端的兼容性没问题。我在实测中发现,minimal-ws-server 会对客户端的消息做透传或回显,具体取决于你连接的路径和协议名,总之能看到收到数据就是正常的。
5.3 写一个最小 C 服务端验证集成
官方示例跑通后,我还会写一个非常小的 C 程序来模拟“把 libwebsockets 集成进自己工程”的场景。这不只是为了验证库可用,更是为了确认头文件查找、链接参数这些集成环节没问题。下面是我测试时用的最小服务端代码:
#include <libwebsockets.h> #include <string.h> #include <stdio.h> static int ws_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { switch (reason) { case LWS_CALLBACK_RECEIVE: printf("received: %s\n", (char *)in); lws_write(wsi, (unsigned char *)"pong", 4, LWS_WRITE_TEXT); break; default: break; } return 0; } static struct lws_protocols protocols[] = { { "ws-test", ws_callback, 0, 4096 }, { NULL, NULL, 0, 0 } }; int main(void) { struct lws_context_creation_info info; memset(&info, 0, sizeof(info)); info.port = 8080; info.protocols = protocols; struct lws_context *context = lws_create_context(&info); if (!context) { fprintf(stderr, "create context failed\n"); return 1; } printf("server running on port 8080\n"); while (1) { lws_service(context, 50); } lws_context_destroy(context); return 0; }这段代码的含义是:创建一个监听 8080 端口的 WebSocket 服务器,注册了一个名为 ws-test 的协议,收到任何客户端消息时打印出来,并回发一个 pong 字符串。lws_service(context, 50) 是事件循环,50 表示每次最多阻塞 50ms,这是 libwebsockets 常见的写法。
编译命令:
gcc -o test_server test_server.c -lwebsockets如果头文件不在默认路径,或者库不在默认路径,再手动指定:
gcc -o test_server test_server.c -I/usr/local/include -L/usr/local/lib -lwebsockets我这里没有加 -lpthread、-lm 之类的额外选项,是因为新版 libwebsockets 的 cmake 配置已经处理好了部分依赖。但不同系统上可能需要手动补,如果链接阶段报 undefined reference 到 pthread_create、pow 这类符号,就在命令里加上 -lpthread -lm 再试。
运行:
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH ./test_server浏览器打开 http://localhost:8080 会提示握手不成功或者直接拒绝普通 HTTP 请求,这没关系,因为我们的协议不需要 page。用 Python websockets 连 ws://127.0.0.1:8080,协议名填 ws-test,发送 hello,就能在服务器终端看到 received: hello,同时客户端会收到 pong。
这个小实验能把整条链路串起来:自己的代码 -> 自己编译的 libwebsockets 库 -> 标准 WebSocket 客户端。做到这一步,库的使用才算真正过关。
6. 常见问题与排查实录
6.1 编译阶段的高频问题
先说编译阶段最常见的几个问题。
第一:找不到 OpenSSL 头文件。报错类似 fatal error: openssl/ssl.h: No such file or directory。解决办法很直接,安装 libssl-dev 即可。但如果已经装了还报错,可能是 CMake 没有正确找到 OpenSSL 的路径,可以在 cmake 时手动指定:
cmake .. -DOPENSSL_ROOT_DIR=/usr/lib/ssl第二:CMake 找不到 OpenSSL 库导致 SSL 功能被静默关闭。这个更隐蔽,因为编译可能不会报错,但最后库不支持 wss。排查方法是编译完成后查看 build 目录里的 CMakeCache.txt,搜索 LWS_WITH_SSL 字段,看它实际被置为 ON 还是 OFF。
第三:系统 cmake 版本太低,报出各种 not found 的错误。直接用 pip 安装新版 cmake 或者下载安装脚本可以解决,但最快的是用发行版自带的软件包管理再升级一下。
第四:make 并行编译时内存不够。前面提过,把 -j 的数值调小,或者干脆不写 -j 参数,用单单 make 编译。
6.2 运行阶段的坑
运行阶段我也会遇到一些低级但很常见的坑:
动态库找不到。报错前面已经提过,解决办法是 export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH,或者把 /usr/local/lib 写进 /etc/ld.so.conf.d/ 下的配置里再执行 ldconfig。
端口被占用。启动服务器时报 bind 失败,用 lsof -i:7681 查一下谁占了这个端口,换一个端口或者杀掉占用进程。
客户端连接被拒绝。如果客户端和服务端在同一台机器上,先确认服务器是否真的在监听;如果在不同机器上,检查防火墙。嵌入式开发里这是最常被忽略的。
在 CMake 集成时链接不上库。如果你在自己的 CMake 工程里引用 libwebsockets,推荐用 pkg-config:
find_package(PkgConfig REQUIRED) pkg_check_modules(LWS REQUIRED IMPORTED_TARGET libwebsockets) target_link_libraries(your_target PRIVATE PkgConfig::LWS)如果 pkg-config 找不到 libwebsockets.pc,检查 /usr/local/lib/pkgconfig 是否存在这个文件,并把 PKG_CONFIG_PATH 导出。
6.3 问题速查表
把高频问题整理成一个表,方便以后排查:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 编译报 openssl/ssl.h 找不到 | 未安装 libssl-dev | sudo apt install libssl-dev |
| 编译成功后库不支持 wss | CMake 未找到 OpenSSL 或未显式开启 LWS_WITH_SSL | 配置时加 -DLWS_WITH_SSL=ON 并检查 openssl 安装 |
| 运行程序报 libwebsockets.so.xx not found | 动态库不在系统搜索路径 | export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH |
| 修改 cmake 选项后不生效 | CMake 缓存未清理 | 删除 build 目录或 CMakeCache.txt 后重新 cmake |
| make -j 编译时被杀 | 系统内存不足 | 降低并行数,例如 make -j2 |
| 服务器 bind 失败 | 端口被占用 | 用 lsof -i:port 查找并处理占用进程 |
| 链接时报 undefined reference to pthread_create | 缺少线程库 | 链接时加 -lpthread |
| 客户端在其他机器连不上服务器 | 防火墙或监听地址限制 | 检查防火墙规则,确认监听在 0.0.0.0 或具体网卡地址 |
这些坑看起来都不难,但实际排查起来很耗时间,尤其是 CMAKE 缓存问题,很多人会反复踩。
6.4 一点补充:如何判断库是否裁剪成功
如果你比较关注最终库体积,想在裁剪掉一些功能模块后观察变化,可以用 file 和 size 查看库文件信息:
file build/lib/libwebsockets.a size build/lib/libwebsockets.a通过对比不同编译选项下静态库的体积,可以直观感受到 LWS_WITH_SSL、LWS_WITH_HTTP2、LWS_WITH_ZLIB 这些选项对最终产物体积的影响。对于做嵌入式固件的朋友,这一步值得花点时间调,能省下不少 flash 空间。
我个人在实际操作中的体会是:libwebsockets 的编译本身不复杂,复杂的是弄懂每个选项背后的功能取舍。如果你只是先让程序跑起来,最快路径就是照着我上面的命令一步步走,先用默认配置打开 minimal examples,把回环测试跑通,再根据你的实际场景逐个调整编译选项。真到了要裁剪、要交叉编译、要上 wss 的阶段,再回头仔细研究这些 CMake 开关也不迟。最后再分享一个小技巧:编译完成后的 build/bin 目录里那些 minimal examples 千万别删,它们不只是玩具,临时调接口、对比行为、验证协议兼容性,比你自己从头写测试代码快太多。