我最早接触libwebsockets,是在一个智能网关项目里,需要让设备端和云端保持实时通信。当时第一反应是用WebSocket,但搜了一圈发现,轻量级的方案里libwebsockets几乎是最靠谱的选择——纯C实现、对嵌入式环境友好、支持SSL/TLS,而且协议栈完整。后来我在不同平台(x86 Linux、ARM交叉编译、Windows)上都编译过这个库,也踩了不少坑,今天把整个过程整理出来,从下载、编译到测试,一次性讲透。
1. libwebsockets是什么,为什么值得用
libwebsockets是一个用C语言写的开源WebSocket协议库,由Andy Green维护,在GitHub上长期保持活跃。它的核心定位是给资源受限的环境提供完整的WebSocket通信能力,但又不只是在嵌入式里能用——你在普通的Linux服务器、macOS、Windows上都能编译运行。
它的几个核心特性我实际用下来感受很深:
- 协议支持非常全:RFC6455标准的WebSocket协议、H2(HTTP/2)多路复用、HTTP/3(QUIC)的实验性支持,还有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)、cjson(JSON解析)、sqlite3(存储相关示例使用)。
在Ubuntu/Debian上安装基础依赖:
sudo apt update sudo apt install build-essential cmake libssl-dev zlib1g-dev如果后面交叉编译,主机上的依赖库和最终目标板用的库要区分清楚。交叉编译时,目标板跑的程序需要的是目标架构的库,而不是主机上的x86库。
3.2 标准编译流程:CMake三板斧
libwebsockets从v3.x开始全面转向CMake构建系统。原来的autotools(configure/make)在老版本里还有,但新版本已经不推荐了。整个编译过程其实就是三句话:
mkdir build cd build cmake .. make但这只是最基本的流程,实际工程里基本不可能这么朴素地编译——你几乎总要配置一些选项,比如禁用某些不需要的功能、开启测试、指定安装路径等等。
3.3 关键CMake选项解析(选型必看)
我把自己常用的一些关键选项整理成了表格,方便对照查阅。这里面的选项基本决定了你编译出的库是精简版还是全功能版:
| 选项 | 默认值 | 作用说明 | 我的建议 |
|---|---|---|---|
LWS_WITH_SSL | ON | 启用TLS/SSL支持,依赖OpenSSL | 做产品建议打开,现在wss几乎是标配 |
LWS_WITH_CLIENT | ON | 编译客户端模式支持 | 需要主动连接WebSocket服务端时保留 |
LWS_WITH_SERVER | ON | 编译服务端模式支持 | 服务端开发必须保留 |
LWS_WITH_MINIMAL_EXAMPLES | ON | 同时编译minimal示例程序 | 开发调试阶段打开,方便验证功能 |
LWS_WITHOUT_TESTAPPS | OFF | 是否跳过测试程序 | 如果不想编译test-apps里的工具,设为ON |
LWS_WITH_SHARED | ON | 编译动态库(.so/.dll) | 默认是动态库,设为OFF生成静态库 |
LWS_WITH_STATIC | OFF | 编译静态库(.a/.lib) | 需要静态链接时设为ON |
CMAKE_INSTALL_PREFIX | /usr/local | 指定安装路径 | 交叉编译时务必改成你的工具链sysroot |
LWS_IPV6 | ON | 启用IPv6支持 | 视实际网络环境而定 |
LWS_WITH_HTTP2 | OFF | 启用HTTP/2支持 | 有HTTP/2需求时打开,功能相对独立 |
LWS_WITH_CJSON | OFF | 集成cJSON以支持JSON相关示例 | 示例代码需要时自动开启 |
实际编译时我一般这么配:
cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr/local/libwebsockets \ -DLWS_WITH_MINIMAL_EXAMPLES=ON \ -DLWS_WITHOUT_TESTAPPS=ON \ -DLWS_WITH_SHARED=ON \ -DLWS_WITH_STATIC=ON这里-DLWS_WITH_STATIC=ON和-DLWS_WITH_SHARED=ON可以同时开启,这样动态库和静态库都会生成——调试时用静态库更方便,部署到设备上可以用动态库减小体积。
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的新版本通常依赖较新的OpenSSL(3.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.cmake:
set(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_SSL=OFF \ -DLWS_WITH_MINIMAL_EXAMPLES=OFF 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-client:WebSocket客户端,支持连接外部ws://服务器。lws-minimal-ws-server:WebSocket服务器,可以接受客户端连接并收发消息。lws-minimal-ws-server-tls:带TLS加密的WebSocket服务器,用于验证wss功能。
这些例子是认识libwebsockets功能边界最好的入口,每一个都是一个独立的完整程序,直接用CMake编译好之后运行即可。
4.2 启动自带的WebSocket服务器
假设我们编译时开启了LWS_WITH_MINIMAL_EXAMPLES=ON,在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 "/CN=localhost" ./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/null - CMake重新指定路径或用
sudo apt install libssl-dev
问题2:OpenSSL版本太新导致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问题2:TLS握手失败
客户端报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_TYPE=Release .. && make |
| 开启所有示例 | -DLWS_WITH_MINIMAL_EXAMPLES=ON |
| 只编静态库 | -DLWS_WITH_SHARED=OFF -DLWS_WITH_STATIC=ON |
| 指定安装路径 | -DCMAKE_INSTALL_PREFIX=/your/path |
| 生成工程后查看可用选项 | ccmake ..或cmake -L .. |
| 运行ws服务器 | ./lws-minimal-ws-server -p 7681 |
| 运行ws客户端测试 | websocat ws://localhost:7681 |
| 启用TLS | -DLWS_WITH_SSL=ON,运行时带证书启动 |
| 安装 | make install,默认到/usr/local |
这套流程下来,从源码下载到服务跑通,基本能覆盖 libwebsockets 使用的主路径。后续你完全可以基于minimal-examples里的代码,改出一个满足具体业务需求的服务端或客户端——我后面好几次做项目,都是直接在示例代码上改出来的。
根据我个人经验,libwebsockets这类库最大的学习价值在于:它把WebSocket这个协议完全透明地呈现在你面前——你可以在日志里看到每一个数据帧的流向,在回调里感受每一次状态机的切换。理解了这个过程之后,无论是排查网络问题、还是实现自己的长连接服务,心里都会非常有底。