
做物联网设备上云、搞嵌入式网络通信的朋友对 libwebsockets 这个名字一定不陌生。这是一个用 C 语言实现的轻量级 WebSocket 协议库附带 HTTP/1.1、HTTP/2 的部分能力在资源受限的设备环境里非常受欢迎。最近我在做一个 Linux 嵌入式设备的远程管理项目需要设备端和云端建立双向实时通道既要主动上报传感器状态和心跳又要能实时接收云端下发的控制指令选型时考察了一圈最终定下用 libwebsockets。但说实话真正自己从源码下载、配置、编译、安装再跑通测试还是花了不少功夫。网上很多资料要么只讲个 apt install 完事要么直接甩几条 cmake 命令让你自己猜真正踩坑时找不到人问。这篇就把这次完整走通的下载、编译、测试过程记录下来包括我用到的参数、为什么这么配、编译后怎么验证、遇到哪些报错又是怎么解决的。如果你也在做类似的设备联网、WebSocket 通道搭建或者只是想快速拿到一个能用的 libwebsockets 继续开发上层功能这篇内容应该能帮你省下不少时间。1. 项目概况为什么要自己折腾 libwebsockets1.1 libwebsockets 能做什么从使用者的角度说libwebsockets 把 WebSocket 协议栈里的繁琐细节几乎全部封装好了。你不需要关心 HTTP Upgrade 握手、数据帧解析、掩码处理、分片重组、连接保活这些底层逻辑只需要注册一组协议回调函数就能在连接建立、收到消息、连接关闭等事件里处理自己的业务数据。它的核心能力包括单线程事件驱动模型底层基于 poll 或自定义事件循环适合嵌入式设备这种单核、低内存环境同时支持 ws 和 wsswss 只需要在编译时启用 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基础编译工具链cmakelibwebsockets 使用 CMake 构建版本最好在 3.16 以上git拉取源码libssl-devOpenSSL 开发库编译 wss 支持时必需zlib1g-devzlib 压缩库用于 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 而不是 masterlibwebsockets 的 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_TYPERelease \ -DLWS_WITH_SSLON \ -DLWS_WITHOUT_TESTAPPSOFF \ -DLWS_WITH_MINIMAL_EXAMPLESON \ -DLWS_WITH_STATICON \ -DLWS_WITH_SHAREDON这里特别提醒一点如果你之前配置过一次想修改选项重新配置光改参数重新执行 cmake 可能不生效因为 CMake 会缓存之前的配置。稳妥的做法是直接把 build 目录删掉重新建或者至少删掉 build 目录里的 CMakeCache.txt再执行 cmake。我一开始就是在原有 build 目录里反复改参数结果某些选项死活不生效折腾了好久才反应过来是缓存的问题。后来老老实实每次配置前 rm -rf build。3.2 关键 CMake 选项逐个拆解下面这个表格整理了我用到的、以及常见的几个重要选项方便你参考选项默认值含义我的建议CMAKE_BUILD_TYPE空构建类型可选 Release / Debug正式使用选 Release排查问题用 DebugLWS_WITH_SSLON启用 OpenSSL支持 wss保持 ON否则 wss 不可用LWS_WITHOUT_TESTAPPSOFF设为 ON 会跳过测试程序编译做测试验证时设为 OFFLWS_WITH_MINIMAL_EXAMPLESOFF编译官方精简示例刚开始建议 ON拿来测试很方便LWS_WITH_STATICON生成静态库按需开启LWS_WITH_SHAREDON生成动态库按需开启LWS_WITH_HTTP2OFF启用 HTTP/2 支持需要时打开LWS_WITH_ZLIBON启用 zlib 压缩有 zlib 就保持 ONLWS_WITH_LIBUVOFF集成 libuv 事件循环除非你需要 libuv否则保持 OFFLWS_WITH_CLIENTON启用客户端功能默认即可LWS_WITH_SERVERON启用服务端功能默认即可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.apkg-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(freceived: {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-configfind_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-devsudo apt install libssl-dev编译成功后库不支持 wssCMake 未找到 OpenSSL 或未显式开启 LWS_WITH_SSL配置时加 -DLWS_WITH_SSLON 并检查 openssl 安装运行程序报 libwebsockets.so.xx not found动态库不在系统搜索路径export LD_LIBRARY_PATH/usr/local/lib:$LD_LIBRARY_PATH修改 cmake 选项后不生效CMake 缓存未清理删除 build 目录或 CMakeCache.txt 后重新 cmakemake -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 千万别删它们不只是玩具临时调接口、对比行为、验证协议兼容性比你自己从头写测试代码快太多。