ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

libwebsockets实战指南:从源码编译到嵌入式WebSocket通信

libwebsockets实战指南:从源码编译到嵌入式WebSocket通信 我最早接触libwebsockets是在一个智能网关项目里需要让设备端和云端保持实时通信。当时第一反应是用WebSocket但搜了一圈发现轻量级的方案里libwebsockets几乎是最靠谱的选择——纯C实现、对嵌入式环境友好、支持SSL/TLS而且协议栈完整。后来我在不同平台x86 Linux、ARM交叉编译、Windows上都编译过这个库也踩了不少坑今天把整个过程整理出来从下载、编译到测试一次性讲透。1. libwebsockets是什么为什么值得用libwebsockets是一个用C语言写的开源WebSocket协议库由Andy Green维护在GitHub上长期保持活跃。它的核心定位是给资源受限的环境提供完整的WebSocket通信能力但又不只是在嵌入式里能用——你在普通的Linux服务器、macOS、Windows上都能编译运行。它的几个核心特性我实际用下来感受很深协议支持非常全RFC6455标准的WebSocket协议、H2HTTP/2多路复用、HTTP/3QUIC的实验性支持还有MQTT over WebSocket。依赖极小只用到的核心依赖是OpenSSL如果启用TLS和zlib如果启用压缩。不需要一堆没完没了的第三方库。自带测试用例和示例代码源码里有大量minimal example每个示例针对一种功能场景比如最小HTTP服务器、WebSocket客户端、WS over TLS、异步DNS解析等对学习协议和库的API非常有帮助。事件驱动模型基于自己的event loop也支持集成到外部event loop比如libuv、glib、ev/uv设计上不算臃肿。这个库适合谁来用呢一是做嵌入式设备端通信的开发者二是需要在C/C服务端上直接集成WebSocket能力的人三是研究WebSocket协议本身、想通过实际代码加深理解的入门者。相比直接用Boost.Beast或者OpenSSL裸手写握手解析libwebsockets把协议细节封装得足够好同时在调用层面又保留了足够的控制力。2. 下载libwebsockets源码版本选择与获取方式2.1 从GitHub获取最新代码libwebsockets的官方仓库地址是https://github.com/warmcat/libwebsockets。下载方式有两种一种是直接clone git仓库另一种是下载release tarball源码包。git clone https://github.com/warmcat/libwebsockets.git如果你只需要某个特定版本不想把整个提交历史都拉下来可以加--depth 1参数做浅克隆配合--branch指定分支或标签git clone --depth 1 --branch v4.3-stable https://github.com/warmcat/libwebsockets.git2.2 稳定版与开发版怎么选libwebsockets的发布节奏很有特点它有长期维护的stable分支类似v4.3-stable同时main分支上也持续加入新特性。根据我实际项目经验做产品选型时用stable分支更稳妥特别是要做长期维护的设备端固件时stable分支的API稳定性明显更好编译依赖也更可控。而main分支上的代码可能更早支持新的协议特性和优化但偶尔会引入构建系统调整或者API改动如果只是跑测试玩一下无所谓但用于生产环境的项目我不建议直接跟踪main。另外很多发行版Debian/Ubuntu的软件源里也有libwebsockets-dev包版本通常会滞后一些但胜在安装省事sudo apt install libwebsockets-dev当然发行版自带的版本对只想快速用到功能的人来说很合适但如果需要自定义编译选项、裁剪功能或者交叉编译到ARM平台还是得从源码自己编译。2.3 下载后确认目录结构源码下载完成后先看一眼顶层目录结构方便后面找东西CMakeLists.txt构建系统主文件整个编译配置都靠它。cmake/CMake的辅助脚本和模块包括找依赖库的脚本。lib/libwebsockets核心库的全部源码这是我们最终编译产物的来源。bin/一些测试工具和辅助程序的源码比如测试证书生成脚本。minimal-examples/官方提供的大量极简示例每个目录对应一个独立小项目特别适合学习。test-apps/早期版本里的测试程序目录现在很多新功能示例都迁移到minimal-examples了。如果是老版本比如v3.x顶层目录会略有差别但核心的lib/和CMakeLists.txt两个单元永远都在。拿到源码先不急着编译花两分钟看看minimal-examples里的示例对理解库的能力边界很有帮助。3. 编译libwebsockets从CMake配置到生成产物3.1 前置依赖比想象中简单libwebsockets的依赖真的不多但缺了会导致某些功能编译不出来。我这里按功能分类列一下基础编译环境gcc/clang、make、cmake建议3.16以上版本旧版本有些选项不支持。TLS支持OpenSSL开发库libssl-dev。如果不配置这个编译出的库默认不支持wss://WebSocket over TLS和https://。压缩支持zlib开发库zlib1g-dev。启用后HTTP压缩和permessage-deflate扩展才可用。可选能力libuv外部事件循环、libev、libevent、mbedtls轻量TLS、cjsonJSON解析、sqlite3存储相关示例使用。在Ubuntu/Debian上安装基础依赖sudo apt update sudo apt install build-essential cmake libssl-dev zlib1g-dev如果后面交叉编译主机上的依赖库和最终目标板用的库要区分清楚。交叉编译时目标板跑的程序需要的是目标架构的库而不是主机上的x86库。3.2 标准编译流程CMake三板斧libwebsockets从v3.x开始全面转向CMake构建系统。原来的autotoolsconfigure/make在老版本里还有但新版本已经不推荐了。整个编译过程其实就是三句话mkdir build cd build cmake .. make但这只是最基本的流程实际工程里基本不可能这么朴素地编译——你几乎总要配置一些选项比如禁用某些不需要的功能、开启测试、指定安装路径等等。3.3 关键CMake选项解析选型必看我把自己常用的一些关键选项整理成了表格方便对照查阅。这里面的选项基本决定了你编译出的库是精简版还是全功能版选项默认值作用说明我的建议LWS_WITH_SSLON启用TLS/SSL支持依赖OpenSSL做产品建议打开现在wss几乎是标配LWS_WITH_CLIENTON编译客户端模式支持需要主动连接WebSocket服务端时保留LWS_WITH_SERVERON编译服务端模式支持服务端开发必须保留LWS_WITH_MINIMAL_EXAMPLESON同时编译minimal示例程序开发调试阶段打开方便验证功能LWS_WITHOUT_TESTAPPSOFF是否跳过测试程序如果不想编译test-apps里的工具设为ONLWS_WITH_SHAREDON编译动态库.so/.dll默认是动态库设为OFF生成静态库LWS_WITH_STATICOFF编译静态库.a/.lib需要静态链接时设为ONCMAKE_INSTALL_PREFIX/usr/local指定安装路径交叉编译时务必改成你的工具链sysrootLWS_IPV6ON启用IPv6支持视实际网络环境而定LWS_WITH_HTTP2OFF启用HTTP/2支持有HTTP/2需求时打开功能相对独立LWS_WITH_CJSONOFF集成cJSON以支持JSON相关示例示例代码需要时自动开启实际编译时我一般这么配cmake .. \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX/usr/local/libwebsockets \ -DLWS_WITH_MINIMAL_EXAMPLESON \ -DLWS_WITHOUT_TESTAPPSON \ -DLWS_WITH_SHAREDON \ -DLWS_WITH_STATICON这里-DLWS_WITH_STATICON和-DLWS_WITH_SHAREDON可以同时开启这样动态库和静态库都会生成——调试时用静态库更方便部署到设备上可以用动态库减小体积。3.4 遇到编译错误时的排查思路有次编译v4.3-stable时报错提示找不到openssl/ssl.h但我明明确认过libssl-dev已经安装了。后来发现是因为系统同时装了多个OpenSSL版本CMake的find_package找到了错误的路经。解决办法是指定OpenSSL根目录cmake .. -DOPENSSL_ROOT_DIR/usr/local/openssl -DOPENSSL_INCLUDE_DIR/usr/local/openssl/include另一个常见问题是在比较老的glibc环境下编译新版本libwebsockets会出现某些符号未定义的错误——这说明系统基础库太老要么升级系统要么换用更老的libwebsockets版本。最典型的经验是libwebsockets的新版本通常依赖较新的OpenSSL3.x而很多老系统自带的是OpenSSL 1.1.1。v4.3-stable对OpenSSL 1.1.1兼容性还不错但v5.x以后最好直接上OpenSSL 3.x不然可能遇到API不兼容的编译错误。3.5 交叉编译到ARM目标板做嵌入式项目时交叉编译是绕不开的。libwebsockets的CMake交叉编译流程和大多数库类似需要指定工具链文件关键是要让CMake找到正确的编译器、头文件和库路径。我整理了一份通用的交叉编译工具链文件放在cmake_toolchain_arm.cmakeset(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER /opt/gcc-arm-none-eabi/bin/arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER /opt/gcc-arm-none-eabi/bin/arm-none-eabi-g) set(CMAKE_SYSROOT /opt/gcc-arm-none-eabi/arm-none-eabi) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)编译命令mkdir build-arm cd build-arm cmake .. -DCMAKE_TOOLCHAIN_FILE../cmake_toolchain_arm.cmake \ -DCMAKE_INSTALL_PREFIX$PWD/_install \ -DLWS_WITH_SSLOFF \ -DLWS_WITH_MINIMAL_EXAMPLESOFF make -j$(nproc) make install注意我把LWS_WITH_SSL关了——ARM板如果不需要wss关掉TLS能省下不少Flash空间和内存。实际上很多嵌入式场景确实用不到TLS因为TLS的握手和证书管理对MCU类设备来说开销太大。4. 测试libwebsockets验证功能的核心方法4.1 编译自带的测试工具libwebsockets源码里带了几个非常实用的测试程序。在build/bin/目录下编译完成后会生成一些可执行文件比如lws-minimal-http-server极简HTTP服务器可以测试基础的HTTP GET/POST请求。lws-minimal-ws-clientWebSocket客户端支持连接外部ws://服务器。lws-minimal-ws-serverWebSocket服务器可以接受客户端连接并收发消息。lws-minimal-ws-server-tls带TLS加密的WebSocket服务器用于验证wss功能。这些例子是认识libwebsockets功能边界最好的入口每一个都是一个独立的完整程序直接用CMake编译好之后运行即可。4.2 启动自带的WebSocket服务器假设我们编译时开启了LWS_WITH_MINIMAL_EXAMPLESON在build目录下找到示例程序cd build/bin ./lws-minimal-ws-server -p 7681这个命令会启动一个WebSocket服务监听7681端口。启动日志会显示[2025/01/12 10:23:45:1234] NOTICE: lws-minimal-ws-server: listening on :7681如果要测试TLS版本需要先准备证书。libwebsockets源码里带了自签名证书生成脚本在scripts/或者直接用系统的openssl命令生成openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 -subj /CNlocalhost ./lws-minimal-ws-server-tls -p 7682 --ssl --cert cert.pem --key key.pem4.3 用命令行工具测试连接验证WebSocket服务最简单的方式是用现成的WebSocket客户端工具比如websocat或wscat。我用得比较多的是websocat安装也简单cargo install websocat # 或者 apt install websocat连接测试websocat ws://localhost:7681连接成功后客户端输入内容回车服务端会原样回显echo模式。这就是WebSocket的基本通信流程——握手升级、双向消息传递。如果使用的是TLS版本连接命令要改一下websocat wss://localhost:7682 -k-k参数的作用是跳过证书校验因为自签名证书不被信任。4.4 写一个简单的C语言测试客户端命令行工具能验证基本连通性但如果你想测libwebsockets的C API是否调用正确、消息收发是否可靠建议写一个简单客户端用库的API来主动建立连接。我基于minimal-examples里ws-client示例简化了一个测试客户端核心逻辑如下#include libwebsockets.h #include string.h #include signal.h static int interrupted 0; static int callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { switch (reason) { case LWS_CALLBACK_CLIENT_ESTABLISHED: lws_callback_on_writable(wsi); break; case LWS_CALLBACK_CLIENT_RECEIVE: printf(received: %.*s\n, (int)len, (char *)in); break; case LWS_CALLBACK_CLIENT_WRITEABLE: { unsigned char buf[LWS_PRE 64]; unsigned char *p buf[LWS_PRE]; size_t n sprintf((char *)p, hello from lws client); lws_write(wsi, p, n, LWS_WRITE_TEXT); break; } case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: fprintf(stderr, connection error\n); interrupted 1; break; default: break; } return 0; } int main(void) { struct lws_context_creation_info info; struct lws_client_connect_info ccinfo; struct lws_context *context; struct lws *wsi; memset(info, 0, sizeof(info)); info.port CONTEXT_PORT_NO_LISTEN; info.protocols (struct lws_protocols[]) { { example-protocol, callback_ws, 0, 4096 }, { NULL, NULL, 0, 0 } }; info.options LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; context lws_create_context(info); if (!context) { fprintf(stderr, context creation failed\n); return 1; } memset(ccinfo, 0, sizeof(ccinfo)); ccinfo.context context; ccinfo.address localhost; ccinfo.port 7681; ccinfo.path /; ccinfo.protocol example-protocol; ccinfo.ietf_version_or_minus_one -1; wsi lws_client_connect_via_info(ccinfo); if (!wsi) { fprintf(stderr, connection failed\n); lws_context_destroy(context); return 1; } while (!interrupted lws_service(context, 0) 0) { // 事件循环持续运行 } lws_context_destroy(context); return 0; }这段代码做的事情是创建上下文、发起客户端连接、在握手建立后发送一条文本消息、把服务端回显的消息打印出来。编译时链接库gcc my_client.c -o my_client -I/usr/local/include -L/usr/local/lib -lwebsockets ./my_client如果一切正常服务端日志里能看到accepted client之类的连接日志客户端则打印出received: hello from lws client——这是服务端echo回来的消息。4.5 压力测试与连接稳定性验证测试WebSocket服务除了功能通断还要看它能扛多少并发连接。libwebsockets自带一个多线程压力测试程序在某些版本位于test-apps下名字类似lws-mirror或lws-spawn。也可以通过外部工具做压测websocat -n ws://localhost:7681 # 一次性发完消息断开更实际的方案是自己写一个压测脚本用Python的websocket-client库批量建立连接同时收发消息。我在实际项目里会关注几个指标最大并发连接数受文件描述符上限影响ulimit -n。单连接长连稳定性长时间不通信服务端和客户端心跳保活。重连恢复能力服务端重启后客户端能否自动重连。libwebsockets的心跳PING/PONG机制默认开启在server模式下每隔一段时间会给客户端发PING客户端回PONG这能保证连接不会因中间设备超时而断开。如果在测试过程中出现连接频繁断开优先检查心跳间隔配置在lws_context_creation_info里设置ws_ping_pong_interval。5. 常见问题与踩坑记录5.1 编译阶段的问题问题1找不到OpenSSL头文件报错特征fatal error: openssl/ssl.h: No such file or directory排查步骤确认libssl-dev有没有安装dpkg -l | grep libssl-dev搜索头文件实际位置find /usr -name ssl.h 2/dev/nullCMake重新指定路径或用sudo apt install libssl-dev问题2OpenSSL版本太新导致API编译失败报错特征error: RSA {aka struct rsa_st} has no member named e之类。这是OpenSSL 3.x中很多结构体变为不透明opaque导致的。解决思路是换用支持OpenSSL 3的libwebsockets版本或者将OpenSSL降级到1.1.1。我个人更倾向换库版本因为新系统的OpenSSL 3是安全更新基线没必要为了旧库强行降级系统组件。问题3链接阶段找不到 -lwebsockets编译自己的程序时提示cannot find -lwebsockets原因通常是库文件没有安装到系统搜索路径中或者动态库运行时加载路径没配置。解决办法export LD_LIBRARY_PATH/usr/local/libwebsockets/lib:$LD_LIBRARY_PATH或者在编译时用-Wl,-rpath,/usr/local/libwebsockets/lib把库路径写进可执行文件。5.2 运行阶段的问题问题1连接被拒绝客户端报Connection refused检查服务端是否真的在监听、端口是否正确netstat -tlnp | grep 7681问题2TLS握手失败客户端报TLS handshake failed优先检查证书路径是否正确、证书格式是否是PEM。有次我用.crt格式证书直接指定结果libwebsockets只认PEM格式用openssl转换一下就通过了。问题3发送消息乱序或丢失在低配设备上如果发送缓冲设置太小高频消息可能被丢弃。libwebsockets的发送是异步的不能在一个回调里连续调用多次lws_write必须等LWS_CALLBACK_CLIENT_WRITEABLE再一次触发后继续写。如果发现自己发的消息总是丢检查是否在这个回调里一次性写了过多数据或者没有关注lws_write的返回值。5.3 我积累的几个实用技巧技巧1开启详细日志调试libwebsockets提供lws_set_log_level接口可以动态调整日志级别。编译时如果开启LWS_WITH_DEBUG测试阶段把日志级别调到最高能看到完整的手握包、数据帧收发过程lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG, NULL);这比抓包工具直观得多特别适合理解WebSocket协议细节。技巧2检查文件描述符上限压测连接数超过1024之后连接失败十有八九是文件描述符限制。临时调整ulimit -n 65535如果是生产环境需要改/etc/security/limits.conf。技巧3尽量用static库嵌入产品固件我做嵌入式产品时有条经验能用静态库就不用动态库。动态库在Linux桌面场景没问题但到嵌入式环境版本管理和依赖关系很容易变成隐形炸弹。libwebsockets的静态库编译出来体积在100KB~300KB左右取决于裁剪选项对现代设备来说完全可接受。6. 在项目里集成libwebsockets时的架构建议libwebsockets用起来不难但真正要跟自己的业务架构融合有几个关键设计点需要想清楚。回调驱动的编程思维libwebsockets是事件驱动模型你的业务逻辑都挂在各种回调里。这和写线性执行的传统C程序不太一样新手容易在回调里做耗时操作结果把event loop卡住导致掉线或者消息延迟。后来我把耗时的业务处理全部丢到工作线程池回调里只做数据拷贝和状态标记整个稳定性一下就上来了。数据缓冲区的生命周期LWS_CALLBACK_CLIENT_RECEIVE回调里的in指针只在回调期间有效你不要存下来异步使用。正确做法是立即memcpy出来或者用lws_traffic之类机制管理缓冲。这个坑我踩过一次——当时把in指针直接传给了工作线程结果两分钟后读到的全是垃圾数据。与业务层解耦我建议把libwebsockets封装成独立的通信模块对外只提供简单的接口connect()、send()、on_message(callback)。业务层完全不需要知道WebSocket握手的细节也不需要关心底层是ws还是wss。这样即使以后要换通信协议业务层代码不受影响。7. 常见操作速查表最后整理一份速查表覆盖最常用的操作方便你日常开发时翻查操作命令/配置克隆仓库git clone https://github.com/warmcat/libwebsockets.git编译release版cmake -DCMAKE_BUILD_TYPERelease .. make开启所有示例-DLWS_WITH_MINIMAL_EXAMPLESON只编静态库-DLWS_WITH_SHAREDOFF -DLWS_WITH_STATICON指定安装路径-DCMAKE_INSTALL_PREFIX/your/path生成工程后查看可用选项ccmake ..或cmake -L ..运行ws服务器./lws-minimal-ws-server -p 7681运行ws客户端测试websocat ws://localhost:7681启用TLS-DLWS_WITH_SSLON运行时带证书启动安装make install默认到/usr/local这套流程下来从源码下载到服务跑通基本能覆盖 libwebsockets 使用的主路径。后续你完全可以基于minimal-examples里的代码改出一个满足具体业务需求的服务端或客户端——我后面好几次做项目都是直接在示例代码上改出来的。根据我个人经验libwebsockets这类库最大的学习价值在于它把WebSocket这个协议完全透明地呈现在你面前——你可以在日志里看到每一个数据帧的流向在回调里感受每一次状态机的切换。理解了这个过程之后无论是排查网络问题、还是实现自己的长连接服务心里都会非常有底。
返回列表