libwebsockets 完整文档
本文档来自: https://zread.ai/warmcat/libwebsockets
1. 概述
来源: https://zread.ai/warmcat/libwebsockets/1-overview
libwebsockets 是一个轻量级、纯C语言库,为WebSockets和其他协议提供了健壮且灵活的客户端-服务器实现。该库在设计时注重性能和安全,为从嵌入式系统到大规模云服务的网络应用提供了全面的解决方案。
什么是libwebsockets?
libwebsockets(LWS)是一个MIT许可的库,提供了高质量的实现,包括:
- WebSocket 客户端和服务器功能
- HTTP/1 和 HTTP/2 支持
- MQTT 协议实现
- 各种辅助协议和工具
该库设计为轻量级 和可配置,允许你仅构建所需功能,同时在各种硬件上保持卓越性能------从资源受限的嵌入式设备到强大的服务器。
来源:README.md
主要特性
协议支持
| 协议 | 客户端 | 服务器 |
|---|---|---|
| WebSockets | ✓ | ✓ |
| HTTP/1.x | ✓ | ✓ |
| HTTP/2 | ✓ | ✓ |
| MQTT | ✓ | ✓ |
技术特性
- 注重安全的设计,提供全面的TLS支持(OpenSSL, MbedTLS v2/v3)
- 跨平台兼容性,支持POSIX系统、Windows和嵌入式平台
- 事件循环灵活性,兼容libuv、libevent、libev、sdevent、glib和uloop
- 轻量级JSON、CBOR、JOSE、COSE实现,满足常见数据格式需求
- 低资源消耗,适用于受限环境
来源:README.md
架构概述
事件驱动设计
Libwebsockets围绕事件循环架构构建,这一设计选择带来了显著优势:
- 单线程操作,消除了复杂的锁定需求
- 非阻塞I/O,提供高效的资源利用
- 空闲时休眠,在无待处理事件时节省电力
- 响应式事件处理,在高负载下保持性能
这种架构使libwebsockets特别适合高并发应用,如Web服务器和IoT设备,在这些应用中高效资源使用至关重要。
来源:README.event-loops-intro.md
API层
Libwebsockets提供两个主要API层:
-
低级WSI(WebSocket实例)APIs
- 直接控制连接
- 协议特定处理
- 传统的基于回调的编程模型
-
安全流APIs(更高级别)
- 抽象的连接管理
- 协议无关代码
- 策略驱动的配置(与代码分离)
- 简化的错误处理和重试
对于新项目,建议从安全流APIs开始。它们通过将连接策略与应用逻辑分离,提供了更易于维护的代码库,结果代码更简单且与协议无关。
来源:README.md, README.secure-streams.md
集成选项
Libwebsockets设计为与现有代码库和库无缝集成。它提供了几种方法,按优先顺序列出:
- 通用事件库集成:使用libuv或libevent等库与应用程序共享事件循环
- 原生WSI语义:在libwebsockets框架内将连接表达为原始WSI
- 自定义事件循环适配器:为你的事件循环实现创建自定义适配器
- 线程级协作:在直接集成不可行时协调线程间操作
这种灵活性使libwebsockets能够适应广泛的架构和现有应用。
来源:README.event-loops-intro.md
项目资源
文档
项目包含广泛的文档:
- READMEs目录:包含50多个关于库各方面专业指南
- 最小示例:超过100个独立、最小示例,演示特定用例
- API文档:全面的Doxygen生成的API参考
测试和质量
Libwebsockets通过广泛的持续集成保持高标准质量:
- 582+构建,覆盖30个平台,每次代码推送
- 全面的测试套件,确保跨平台可靠性
- 注重安全的开发实践和定期审计
来源:README.md
入门指南
要开始使用libwebsockets,你可以:
- 探索最小示例,了解基本使用模式
- 查看READMEs目录,获取关于特定功能的详细信息
- 参考快速入门指南,获取逐步操作说明
项目结构将核心库功能(/lib)、公共头文件(/include)、示例(/minimal-examples)和测试应用(/test-apps)分开,便于导航和理解。
2. 快速入门
来源: https://zread.ai/warmcat/libwebsockets/2-quick-start
本指南将帮助您快速上手libwebsockets。只需几分钟,您就能让一个简单的HTTP客户端应用程序运行起来,并理解libwebsockets的关键组件。
什么是libwebsockets?
Libwebsockets是一个轻量级、可移植的C库,提供HTTP/1、HTTP/2、WebSockets、MQTT等协议的客户端和服务器实现。它设计为:
- 轻量级:小巧的占用空间,适合嵌入式系统
- 安全:注重安全的实现,支持TLS
- 灵活:可与各种事件循环(如libuv、libevent等)配合使用
- 可扩展:从微小的IoT设备到大型云服务器
来源:README.md
安装
前置条件
- CMake (3.5+)
- C编译器(gcc、clang、MSVC)
- OpenSSL或MbedTLS(用于TLS支持)
构建和安装libwebsockets
bash
git clone https://libwebsockets.org/repo/libwebsockets
# 创建构建目录
cd libwebsockets
mkdir build
cd build
# 配置和构建
cmake ..
make
# 安装
sudo make install
对于特定平台的构建说明或自定义构建,请查看READMEs目录中的详细构建指南,例如README.build.md、README.build-windows.md或README.build-android.md。
创建一个简单的HTTP客户端
让我们使用libwebsockets的Secure Streams API创建一个简单的HTTP客户端。这个高级API抽象了许多协议细节,使操作更加简便。
项目结构
我们的示例将包含三个主要组件:
| 文件 | 用途 |
|---|---|
| main.c | 设置libwebsockets上下文和事件循环 |
| hello_world-ss.c | 包含Secure Streams实现 |
| CMakeLists.txt | 构建配置 |
来源:minimal-examples/client/hello_world/README.md
1. 创建主应用程序(main.c)
c
#include <libwebsockets.h>
#include <signal.h>
int test_result = 3; /* b0: 当对等方ACK请求时清除,b1: 当接收完成时清除 */
extern const lws_ss_info_t ssi_hello_world_t; /* 来自hello_world-ss.c */
static struct lws_context *cx;
static void
sigint_handler(int sig)
{
lws_default_loop_exit(cx);
}
int
main(int argc, const char **argv)
{
const char *url = "https://warmcat.com/index.html", *p;
struct lws_context_creation_info info;
struct lws_ss_handle *h;
/* 使用默认值初始化上下文信息 */
lws_context_info_defaults(&info, NULL /* 默认策略 */);
lws_cmdline_option_handle_builtin(argc, argv, &info);
signal(SIGINT, sigint_handler);
/* 检查命令行参数中的自定义URL */
p = lws_cmdline_option(argc, argv, "--url");
if (p)
url = p;
/* 创建libwebsockets上下文 */
cx = lws_create_context(&info);
if (!cx) {
lwsl_err("lws初始化失败\n");
return 1;
}
lwsl_cx_user(cx, "LWS hello_world示例 [-d<详细级别>]\n");
/* 创建Secure Stream */
if (lws_ss_create(cx, 0, &ssi_hello_world_t, NULL, &h, NULL, NULL)) {
lwsl_cx_err(cx, "创建SS失败");
goto bail;
}
/* 设置端点URL */
if (lws_ss_set_metadata(h, "endpoint", url, strlen(url))) {
lwsl_err("%s: 无法使用元数据 %s\n", __func__, url);
goto bail;
}
/* 运行事件循环直到完成 */
lws_context_default_loop_run_destroy(cx);
return lws_cmdline_passfail(argc, argv, test_result);
bail:
lws_context_destroy(cx);
return 1;
}
来源:minimal-examples/client/hello_world/main.c
2. 实现Secure Stream(hello_world-ss.c)
c
#include <libwebsockets.h>
#include <signal.h>
extern int test_result;
LWS_SS_USER_TYPEDEF
/* 您的每个流的实例化成员在此处定义 */
} hello_world_t;
static lws_ss_state_return_t
hello_world_rx(void *userobj, const uint8_t *in, size_t len, int flags)
{
hello_world_t *g = (hello_world_t *)userobj;
struct lws_ss_handle *h = lws_ss_from_user(g);
lwsl_ss_user(h, "RX %zu, 标志 0x%x", len, (unsigned int)flags);
if (len) { /* 记录数据块的前16字节和后16字节 */
lwsl_hexdump_ss_info(h, in, len >= 16 ? 16 : len);
if (len >= 16)
lwsl_hexdump_ss_info(h, in + len - 16, 16);
}
if ((flags & LWSSS_FLAG_EOM) == LWSSS_FLAG_EOM) /* 接收到完整消息 */
test_result &= ~2;
return LWSSSSRET_OK;
}
static lws_ss_state_return_t
hello_world_state(void *userobj, void *h_src, lws_ss_constate_t state,
lws_ss_tx_ordinal_t ack)
{
hello_world_t *g = (hello_world_t *)userobj;
switch ((int)state) {
case LWSSSCS_CREATING: /* 一旦存在就开始事务 */
return lws_ss_request_tx(lws_ss_from_user(g));
case LWSSSCS_QOS_ACK_REMOTE: /* 服务器喜欢我们的请求 */
test_result &= ~1;
break;
case LWSSSCS_DISCONNECTED: /* 对于我们的示例,断开连接即完成 */
lws_default_loop_exit(lws_ss_cx_from_user(g));
break;
}
return LWSSSSRET_OK;
}
LWS_SS_INFO("__default", hello_world_t)
.rx = hello_world_rx,
.state = hello_world_state,
};
来源:minimal-examples/client/hello_world/hello_world-ss.c
3. 创建CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.5)
project(lws-minimal-ss-hello_world C)
set(CMAKE_C_FLAGS "-Wall -Werror")
find_path(LWS_INCLUDE_DIRS
NAMES libwebsockets.h
PATH_SUFFIXES libwebsockets)
find_library(LWS_LIBRARIES
NAMES websockets libwebsockets)
set(SOURCES
main.c
hello_world-ss.c)
add_executable(lws-minimal-ss-hello_world ${SOURCES})
target_link_libraries(lws-minimal-ss-hello_world ${LWS_LIBRARIES})
target_include_directories(lws-minimal-ss-hello_world PUBLIC ${LWS_INCLUDE_DIRS})
理解代码
让我们分解这个示例的工作原理:
关键组件:
- 上下文创建:首先创建一个libwebsockets上下文,管理所有连接和事件循环
- Secure Stream:创建一个Secure Stream,使用默认策略处理连接
- 回调 :
hello_world_rx:接收来自服务器的数据hello_world_state:处理连接状态变化
流程:
- 创建上下文和Secure Stream
- 设置URL端点
- 当流进入
CREATING状态时,请求传输(lws_ss_request_tx()) - 库处理连接建立和TLS协商
- 接收的数据传递给
hello_world_rx()回调 - 断开连接时,应用程序退出事件循环
构建和运行
bash
# 为您的项目创建一个目录
mkdir hello_lws && cd hello_lws
# 复制上述三个文件
# 保存main.c、hello_world-ss.c和CMakeLists.txt
# 构建项目
cmake .
make
# 运行应用程序
./lws-minimal-ss-hello_world
您应该看到输出显示来自warmcat.com的HTTP响应,并显示每个数据块的开始和结束部分。
下一步
现在您已经使用libwebsockets创建了一个基本的HTTP客户端,您可以:
- 探索不同协议:尝试创建WebSocket客户端和服务器
- 了解Secure Streams策略:在JSON中定义自定义连接策略
- 查看示例:浏览minimal-examples目录获取更多示例代码
对于更高级的用法,探索以下功能:
- 事件循环集成(libuv、libevent)
- 服务器实现
- WebSocket协议
- MQTT客户端
- TLS配置
3. 安装指南
来源: https://zread.ai/warmcat/libwebsockets/3-installation-guide
概述
Libwebsockets 是一个轻量级、可移植的 C 语言库,实现了 WebSocket 客户端和服务器功能,以及 HTTP/1、HTTP/2、MQTT 等其他协议。本指南将指导您在多个平台上安装 libwebsockets,包括配置选项和依赖项。
前提条件
在安装 libwebsockets 之前,您需要根据您的平台安装一些工具和库。
常见要求
- CMake 2.8 或更高版本(某些功能建议使用 3.7+)
- C 编译器(GCC、Clang、MSVC 等)
- SSL 库(OpenSSL、MbedTLS、wolfSSL 等)
- Git(用于克隆存储库)
平台特定要求
| 平台 | 要求 |
|---|---|
| Linux | 构建 tools(Debian/Ubuntu 上的 build-essential),OpenSSL 开发库 |
| macOS | Xcode 命令行工具,Homebrew(推荐用于依赖项) |
| Windows | 包含 C/C++ 工具链的 Visual Studio,OpenSSL 二进制文件,pthreads-win32 |
| 嵌入式系统 | 平台特定的工具链 |
基本安装
选项 1:使用软件包管理器(vcpkg)
安装 libwebsockets 最简单的方法是使用 vcpkg:
bash
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh # Linux/macOS
./vcpkg integrate install
vcpkg install libwebsockets
这将下载、构建并使用默认选项安装 libwebsockets。
资源:README.md
选项 2:在类 Unix 系统上从源代码构建
克隆存储库:
bash
git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets
创建构建目录:
bash
mkdir build
cd build
使用 CMake 进行配置:
bash
cmake ..
构建和安装:
bash
make
sudo make install
默认情况下,这将在 /usr/local 中安装 libwebsockets。要更改安装目录,请使用 cmake 的 -DCMAKE_INSTALL_PREFIX 选项。
选项 3:使用 Visual Studio 在 Windows 上从源代码构建
克隆存储库:
cmd
git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets
安装依赖项:
使用 vcpkg 安装 OpenSSL:
cmd
git clone https://github.com/microsoft/vcpkg
cd vcpkg
.\bootstrap-vcpkg.bat
.\vcpkg integrate install
.\vcpkg install openssl:x64-windows
安装 pthreads-win32:
从 ftp://sourceware.org/pub/pthreads-win32 下载并解压到 "C:\Program Files (x86)\pthreads"
创建构建目录和配置:
cmd
mkdir build
cd build
cmake .. -DLWS_HAVE_PTHREAD_H=1 ^
-DLWS_EXT_PTHREAD_INCLUDE_DIR="C:\Program Files (x86)\pthreads\include" ^
-DLWS_EXT_PTHREAD_LIBRARIES="C:\Program Files (x86)\pthreads\lib\x64\libpthreadGC2.a" ^
-DOPENSSL_ROOT_DIR="c:\Users\<user>\vcpkg\packages\openssl_x64-windows"
构建和安装:
cmd
cmake --build . -j 4 --config DEBUG
cmake --install . --config DEBUG
使库可用(可选):
cmd
copy "C:\Program Files (x86)\pthreads\dll\x64\pthreadGC2.dll" C:\Windows\system32
copy "c:\Users\<user>\vcpkg\packages\openssl_x64-windows\bin\libcrypto-3.dll" C:\Windows\system32
copy "c:\Users\<user>\vcpkg\packages\openssl_x64-windows\bin\libssl-3.dll" C:\Windows\system32
选项 4:使用 MinGW 在 Windows 上构建
安装 MinGW:
对于 Fedora,使用:dnf install mingw64-gcc
安装 OpenSSL:
对于 Fedora,使用:mingw64-openssl.noarch mingw64-openssl-static.noarch
生成构建文件:
bash
mkdir build
cd build
mingw64-cmake .. # 如果使用 Fedora 的包装器
# 或
cmake -G "MSYS Makefiles" -DCMAKE_INSTALL_PREFIX=C:/MinGW ..
构建和安装:
bash
make && make install
选项 5:在 macOS 上构建
对于使用 Homebrew 安装 OpenSSL 的 macOS 用户:
bash
export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2k
cmake ..
make -j4
高级配置选项
Libwebsockets 通过 CMake 提供了许多配置选项。以下是一些常见选项:
列出可用选项
要查看所有可用选项:
bash
cmake -LH ..
常见配置选项
| 选项 | 描述 | 默认值 |
|---|---|---|
| LWS_WITH_SSL | 启用 SSL 支持 | 开启 |
| LWS_WITH_MBEDTLS | 使用 mbedTLS 替代 OpenSSL | 关闭 |
| LWS_WITH_WOLFSSL | 使用 wolfSSL 替代 OpenSSL | 关闭 |
| LWS_WITH_BORINGSSL | 使用 BoringSSL 替代 OpenSSL | 关闭 |
| LWS_WITH_HTTP2 | 启用 HTTP/2 支持 | 关闭 |
| LWS_IPV6 | 启用 IPv6 支持 | 关闭 |
| LWS_WITH_ZIP_FOPS | 启用 ZIP 文件操作 | 关闭 |
| LWS_WITH_SOCKS5 | 启用 SOCKS5 代理支持 | 关闭 |
| LWS_WITH_PLUGINS | 启用插件支持 | 关闭 |
| LWS_WITH_DISTRO_RECOMMENDED | 启用针对发行版的推荐选项 | 关闭 |
| CMAKE_BUILD_TYPE | 构建类型(DEBUG、RELEASE 等) | 发布 |
| CMAKE_INSTALL_PREFIX | 安装路径 | /usr/local |
更改 CMake 选项时,您可能需要删除构建目录的内容或至少删除 CMakeCache.txt 文件,以确保更改生效。
SSL 实现选项
Libwebsockets 支持多种 SSL 实现:
OpenSSL(默认):
bash
cmake ..
mbedTLS(占用空间更小,速度较慢):
bash
cmake .. -DLWS_WITH_MBEDTLS=1
wolfSSL:
bash
cmake .. -DLWS_WITH_WOLFSSL=1 \
-DLWS_WOLFSSL_INCLUDE_DIRS=/path/to/wolfssl \
-DLWS_WOLFSSL_LIBRARIES=/path/to/wolfssl/wolfssl.a
BoringSSL:
bash
cmake .. -DOPENSSL_LIBRARIES="/usr/boringssl/lib64/libssl.so;/usr/boringssl/lib64/libcrypto.so" \
-DOPENSSL_INCLUDE_DIRS=/usr/boringssl/include \
-DLWS_WITH_BORINGSSL=1
事件循环库
Libwebsockets 可以与各种事件循环库一起使用:
bash
# 对于 libuv 支持
cmake .. -DLWS_WITH_LIBUV=1
# 对于 libevent 支持
cmake .. -DLWS_WITH_LIBEVENT=1
# 对于 libev 支持
cmake .. -DLWS_WITH_LIBEV=1
# 对于 glib 事件循环
cmake .. -DLWS_WITH_GLIB=1
跨编译
创建工具链文件
对于跨编译,您需要一个 CMake 工具链文件。Libwebsockets 在 contrib/ 目录中包含示例:
bash
mkdir build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../contrib/cross-arm-linux-gnueabihf.cmake \
-DCMAKE_INSTALL_PREFIX:PATH=/usr/lib/my-cross-root
为 ESP32 跨编译
bash
git submodule add https://github.com/warmcat/libwebsockets.git components/libwebsockets
然后按照 ESP-IDF 构建过程进行操作。
测试安装
安装完成后,您可以通过运行测试服务器来测试是否一切正常:
bash
# Linux/macOS
libwebsockets-test-server
# Windows
libwebsockets-test-server.exe
如果您启用了 SSL 支持:
bash
libwebsockets-test-server -s
然后在浏览器中导航到 http://localhost:7681(或 https://localhost:7681 如果使用 SSL)。
故障排除
常见问题
CMake 找不到 OpenSSL:
bash
# 指定 OpenSSL 位置:
cmake .. -DOPENSSL_ROOT_DIR=/path/to/openssl
运行时缺少 SSL 库:
将 LD_LIBRARY_PATH 设置为包含 SSL 库安装的目录:
bash
LD_LIBRARY_PATH=/usr/local/ssl/lib libwebsockets-test-server --ssl
更改选项后出现构建错误:
删除您的构建目录或至少删除 CMakeCache.txt 文件:
bash
rm -rf build/* # 或仅删除 build/CMakeCache.txt
Windows:找不到 DLL 错误:
确保所有必需的 DLL 都在您的系统路径中或在可执行文件相同的目录中。
内存使用注意事项
对于内存受限的环境,请考虑以下配置:
bash
# 最小服务器配置
cmake .. --without-client --without-extensions --disable-debug --without-daemonize
此最小配置使用大约:
- 16KB 用于创建上下文,具有 1024 个 fd 限制
- 每个连接 72 字节
结论
现在您应该已经安装了 libwebsockets,并准备好在项目中使用它。有关使用库的更多信息,请参阅 minimal-examples 目录中的示例或查看 READMEs 文件夹中的其他文档。
如需进一步帮助或报告问题,请访问官方 libwebsockets 网站:https://libwebsockets.org
4. WebSocket基础
来源: https://zread.ai/warmcat/libwebsockets/4-websockets-fundamentals
WebSockets通过实现真正的双向通信,彻底改变了我们构建交互式Web应用的方式。如果你正在使用libwebsockets,那么你选择对了------它是一个强大、轻量级的C库,使得WebSocket的实现变得简单直接。让我们深入探讨WebSockets的基础知识以及如何使用libwebsockets。
什么是WebSockets?
WebSockets在客户端和服务器之间提供了一个持久连接,双方可以随时发送数据。与遵循请求-响应模型的传统HTTP不同,WebSockets允许:
- 全双工通信:客户端和服务器可以独立发送消息
- 实时数据传输:低延迟,开销小
- 持久连接:单个TCP连接保持打开状态,直到明确关闭
WebSockets对于需要实时更新的应用尤为重要,如聊天应用、实时仪表盘、游戏和协作工具。
来源:libwebsockets.h#L1-L25
WebSockets与HTTP的区别
| HTTP | WebSockets |
|---|---|
| 每次请求建立新连接 | 持久连接 |
| 每次请求发送头部 | 仅在初始握手时发送头部 |
| 客户端发起通信 | 双方均可发起通信 |
| 无状态 | 有状态 |
| 文本协议 | 二进制协议(握手后) |
WebSockets从HTTP握手开始,然后"升级"为WebSocket连接。这个升级过程由libwebsockets自动处理。
WebSocket协议
WebSocket协议包括两个主要阶段:
1. 握手阶段
客户端通过发送带有特定头部的HTTP请求来启动WebSocket连接,表明希望将连接升级为WebSockets。服务器响应其握手确认,连接建立。
2. 数据传输阶段
一旦连接建立,通信通过"帧"进行------带有小型头部和有效负载的二进制消息。libwebsockets库为你处理所有低级帧细节。
连接流程:
服务器 ←→ 客户端
连接建立 → 双向通信
HTTP GET with Upgrade: websocket
HTTP 101 Switching Protocols
WebSocket 数据帧
WebSocket 数据帧
关闭帧
关闭帧响应
来源:lws-ws-state.h#L25-L30
WebSocket帧类型
WebSockets支持不同类型的帧,用于不同目的:
| 帧类型 | 目的 | libwebsockets常量 |
|---|---|---|
| 文本 | UTF-8编码的文本数据 | LWS_WRITE_TEXT |
| 二进制 | 任意二进制数据 | LWS_WRITE_BINARY |
| 继续 | 继续分段消息 | LWS_WRITE_CONTINUATION |
| 关闭 | 启动连接关闭 | (由lws_close_reason处理) |
| Ping | 连接保活检查 | LWS_WRITE_PING |
| Pong | 对Ping的响应 | LWS_WRITE_PONG |
来源:lws-write.h#L36-L56
使用libwebsockets创建WebSocket连接
设置客户端连接
要建立客户端连接,你需要创建一个lws_client_connect_info结构,包含你的连接参数:
c
struct lws_client_connect_info info;
memset(&info, 0, sizeof(info));
info.context = context; // 你先前创建的上下文
info.address = "example.com"; // 服务器地址
info.port = 443; // 服务器端口
info.path = "/ws"; // WebSocket路径
info.host = "example.com"; // 主机头部值
info.origin = "https://example.com"; // Origin头部
info.protocol = "my-protocol"; // 子协议
info.ssl_connection = LCCSCF_USE_SSL; // 如需使用SSL
struct lws *wsi = lws_client_connect_via_info(&info);
lws_client_connect_via_info()函数启动连接过程,该过程是异步的。连接建立或有错误时,你会收到回调。
来源:lws-client.h#L116-L196
在设置WebSocket连接时,始终检查
lws_client_connect_via_info()的返回值。虽然它返回一个wsi指针,但这并不意味着连接已完全建立------这只是表示连接过程已开始。你的连接后续仍可能失败。
通过WebSockets发送数据
通过WebSockets发送数据需要使用lws_write()函数。一个重要细节是需要在你的实际数据前添加LWS_PRE字节的填充:
c
// 分配带有LWS_PRE填充的缓冲区
char buffer[LWS_PRE + 128];
char *payload = &buffer[LWS_PRE];
// 填充有效负载
strcpy(payload, "Hello WebSocket World!");
int length = strlen(payload);
// 当套接字可写时发送消息
if (lws_write(wsi, (unsigned char *)payload, length, LWS_WRITE_TEXT) < length) {
// 错误处理 - 连接可能已断开
return -1;
}
LWS_PRE字节是保留给库在发送前添加协议头部的,避免了额外的内存复制操作。这使得libwebsockets在高性能应用中非常高效。
来源:lws-write.h#L107-L168
WebSocket消息分段
WebSockets支持消息分段,这对于大消息很有用:
- 使用
LWS_WRITE_NO_FIN标志开始一个帧 - 使用
LWS_WRITE_CONTINUATION发送继续帧 - 发送不带
LWS_WRITE_NO_FIN的最终帧
c
// 第一个分段
lws_write(wsi, first_part, first_len, LWS_WRITE_TEXT | LWS_WRITE_NO_FIN);
// 中间分段
lws_write(wsi, middle_part, middle_len, LWS_WRITE_CONTINUATION | LWS_WRITE_NO_FIN);
// 最后一个分段
lws_write(wsi, last_part, last_len, LWS_WRITE_CONTINUATION);
你可以使用lws_is_first_fragment()和lws_is_final_fragment()来检查接收数据时的消息边界。
来源:lws-ws-state.h#L40-L54, lws-write.h#L84-L86
WebSocket连接生命周期
WebSocket连接遵循定义的生命周期:
客户端发起连接
↓
HTTP握手
↓
WebSocket连接建立
↓
数据交换
↓
连接关闭 ← Ping/Pong (保活)
↓
连接终止 ← 发生错误
关闭WebSocket连接
要优雅地关闭连接,使用lws_close_reason()函数:
c
lws_close_reason(wsi, LWS_CLOSE_STATUS_NORMAL,
(unsigned char *)"Goodbye", 7);
这会发送一个带有状态码和可选消息的适当关闭帧。常见的关闭状态码:
| 状态码 | 含义 | 常量 |
|---|---|---|
| 1000 | 正常关闭 | LWS_CLOSE_STATUS_NORMAL |
| 1001 | 离开 | LWS_CLOSE_STATUS_GOINGAWAY |
| 1002 | 协议错误 | LWS_CLOSE_STATUS_PROTOCOL_ERR |
| 1009 | 消息过大 | LWS_CLOSE_STATUS_MESSAGE_TOO_LARGE |
来源:lws-ws-close.h#L39-L107
管理WebSocket状态
libwebsockets提供辅助函数来管理WebSocket状态:
lws_send_pipe_choked():检查套接字当前是否可写lws_partial_buffered():检查上次写入是否仅部分完成lws_get_opcode():获取当前帧的操作码lws_frame_is_binary():检查当前帧是二进制还是文本
这些函数帮助你正确管理应用中的WebSocket状态。
来源:lws-ws-state.h#L32-L99
事件驱动架构
libwebsockets使用基于事件的回调系统。你需要实现一个回调函数,该函数会在各种事件中被调用:
c
int callback_example(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
// 连接建立
break;
case LWS_CALLBACK_RECEIVE:
// 数据接收
// 'in'包含数据,'len'是长度
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
// 套接字可写 - 现在可以安全发送数据
break;
case LWS_CALLBACK_CLOSED:
// 连接关闭
break;
default:
break;
}
return 0;
}
最佳实践
- 始终检查发送缓冲区可用性 :使用
lws_send_pipe_choked()在尝试写入前检查。 - 正确处理分段:接收时检查首/末分段状态。
- 尊重LWS_PRE缓冲区:始终在数据前分配LWS_PRE字节。
- 优雅地关闭连接:使用适当的关闭状态码。
- 处理部分写入 :检查
lws_write()的返回值并处理部分缓冲。
结论
WebSockets与libwebsockets为你的应用提供了实现实时双向通信的强大方式。库处理复杂的协议细节,同时提供清晰的API,让你专注于应用逻辑。
通过理解这些基础知识,你已具备使用libwebsockets构建健壮的WebSocket应用的条件。库的事件驱动架构使其易于与各种事件循环集成,高效的缓冲处理确保即使在负载下也能保持高性能。
请记住,WebSockets维护连接状态,因此你的应用需要考虑到这一点------特别是处理重新连接逻辑和正确管理连接状态。
5. 事件循环简介
来源: https://zread.ai/warmcat/libwebsockets/5-event-loops-introduction
事件循环是网络库中的一个基本概念,libwebsockets就是围绕这个强大的范式构建的。本指南介绍了事件循环,解释了它们在libwebsockets中的工作原理,并帮助您为您的应用程序选择正确的方案。
什么是事件循环?
事件循环是一种编程模式,它等待并分发程序中的事件或消息。在其核心,事件循环:
- 在单个线程上运行,消除了复杂锁定机制的需求
- 使用非阻塞I/O操作来避免线程阻塞
- 在等待事件时睡眠,在空闲时消耗最少的CPU
- 当事件发生时唤醒,处理它们,然后返回睡眠状态
这种方法提供了一种高效的方式同时处理多个连接,而不需要为每个连接创建一个线程。
来源:README.event-loops-intro.md#L22-L31
为什么事件循环很重要
事件循环相对于基于线程的方法提供了显著的优势:
- 效率:单个线程可以处理数千个连接
- 简单性:无需复杂的线程同步和锁定
- 可扩展性:非常适合像Web服务器这样的高连接场景
- 资源节约:空闲时CPU使用量最小
例如,一个处理10,000个连接的Web服务器在基于每个连接一个线程的模式下需要10,000个线程,但可以用单个事件循环线程高效地管理所有这些连接。
来源:README.event-loops-intro.md#L102-L114
libwebsockets中的事件循环
libwebsockets需要事件循环来执行其网络操作。默认情况下,它提供了平台特定的实现:
poll()用于类Unix系统- WSA(Windows套接字API)用于Windows系统
此外,libwebsockets可以与流行的外部事件循环库一起工作:
| 事件库 | 描述 | 常见用例 |
|---|---|---|
| libuv | 跨平台异步I/O | Node.js,现代应用程序 |
| libevent | 事件通知库 | 高性能服务器 |
| libev | 高性能事件循环 | 实时应用程序 |
| glib | GNOME的主要事件循环 | GTK应用程序,GNOME桌面 |
| sdevent | systemd事件循环 | systemd集成 |
| uloop | OpenWrt事件循环 | OpenWrt应用程序 |
来源:README.event-libs.md#L5-L13,lib/event-libs/README.md#L5-L11
事件循环的关键特性
1. 单线程执行
事件循环在单个线程上运行,消除了线程之间的同步问题。操作按顺序发生,而不是并发发生,这防止了竞争条件,并使代码更易于预测。
c
// 事件循环简单性的示例 - 无需锁定
void handle_connection(struct lws *wsi, void *user, int reason) {
// 无需锁定访问共享数据
shared_data.counter++;
// 处理事件
// ...
}
来源:README.event-loops-intro.md#L33-L42
2. 非线程安全
由于事件循环在单个线程上运行,因此它们的API没有被设计为可以从其他线程调用。在libwebsockets中,必须从事件循环线程调用所有API,唯一的例外是lws_cancel_service()。
⚠️ 重要 :永远不要从除事件循环线程以外的线程调用libwebsockets API,除非是
lws_cancel_service()。这可能导致竞争条件和意外的行为。
来源:README.event-loops-intro.md#L45-L50
3. 非阻塞I/O
事件循环依赖于非阻塞I/O操作。与等待(阻塞)数据可用来读取或空间可用来写入不同,非阻塞操作会立即返回,并且事件循环会在I/O操作可以继续时得到通知。
c
// 非阻塞回调方法的示例
int callback_http(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
switch (reason) {
case LWS_CALLBACK_HTTP:
// 开始响应 - 立即返回
lws_serve_http_file(wsi, "index.html", "text/html", NULL, 0);
return 0;
case LWS_CALLBACK_HTTP_WRITEABLE:
// 当套接字准备好写入更多数据时调用
// 写入更多数据...
return 0;
}
return 0;
}
来源:README.event-loops-intro.md#L52-L61
4. 空闲时睡眠
当没有事件要处理时,事件循环会进入睡眠状态,等待套接字事件或计划好的计时器。这有助于节省CPU资源,对于需要持续运行的系统尤其重要。
在空闲系统中,事件循环几乎不消耗CPU时间。
来源:README.event-loops-intro.md#L63-L71
选择事件循环
libwebsockets为您提供了三种事件循环选项:
1. 默认事件循环
最简单的方法是使用libwebsockets内置的事件循环,它在Unix系统上基于poll(),在Windows上基于WSA:
c
struct lws_context_creation_info info;
memset(&info, 0, sizeof info);
// 配置上下文
info.port = 9000;
info.protocols = protocols;
// ... 其他设置 ...
// 使用默认事件循环创建上下文
struct lws_context *context = lws_create_context(&info);
// 运行事件循环直到终止
while (!interrupted) {
lws_service(context, 0);
}
// 清理
lws_context_destroy(context);
这种方法非常适合简单的应用程序或当您刚开始使用libwebsockets时。
2. 外部事件循环
如果您已经使用了一个事件库,如libuv、libevent或glib,您可以配置libwebsockets使用它:
c
struct lws_context_creation_info info;
memset(&info, 0, sizeof info);
// 配置为使用libuv
info.options = LWS_SERVER_OPTION_LIBUV;
// ... 其他设置 ...
struct lws_context *context = lws_create_context(&info);
这种方法非常适合将libwebsockets集成到现有应用程序中。
来源:README.event-libs.md#L15-L16
3. "外国"循环集成
为了实现更紧密的集成,您可以使用现有的事件循环实例:
c
// 使用libuv的示例
uv_loop_t *loop = uv_default_loop();
struct lws_context_creation_info info;
memset(&info, 0, sizeof info);
info.options = LWS_SERVER_OPTION_LIBUV;
info.foreign_loops = (void **)&loop; // 使用现有循环
struct lws_context *context = lws_create_context(&info);
这允许您将libwebsockets集成到已经管理自己的事件循环的复杂应用程序中。
来源:lib/event-libs/README.md#L23-L28
集成策略
当与其他使用事件循环的代码一起工作时,您有几种集成选项:
-
使用公共事件库
这是Linux类设备上最好的方法。如果您的应用程序和libwebsockets使用相同的事件库(如libuv或libevent),它们可以无缝共享相同的事件循环。
-
使用libwebsockets的本地抽象
您可以将非WebSocket连接包装在libwebsockets的"RAW"wsi抽象中,这样就可以由相同的事件循环管理一切。
-
创建自定义事件循环外壳
对于特殊需求,您可以通过实现
lws_event_loop_ops接口来创建自定义事件循环实现。 -
线程级协作
如果您无法将代码统一到单个事件循环中,您可以使用
lws_cancel_service()进行线程级协作来在线程之间进行通信。
来源:README.event-loops-intro.md#L115-L171
常见误解
"线程更好"
虽然线程有其位置,但事件循环模型通常更适合I/O密集型应用程序,如Web服务器。线程通过锁定、上下文切换开销和资源分配带来了复杂性。
"阻塞I/O更简单"
阻塞I/O可能看起来更简单,但它需要为任何可能等待的操作创建线程。事件循环提供了一个干净的回调模型,没有多个线程的开销。
"关于延迟怎么办?"
处理事件按顺序可能会引入延迟,这是一个常见的担忧。在实践中:
- 大多数系统的大部分时间都在空闲状态
- 网络操作由操作系统异步处理
- 数据包序列化在网络层发生,无论应用程序架构如何
来源:README.event-loops-intro.md#L195-L276
结论
事件循环是网络编程的一个强大模式,允许libwebsockets以最小的资源高效地处理数千个连接。通过了解事件循环的工作原理并选择正确的集成方法,您可以构建高性能、可扩展的应用程序。
最佳结果来自于您的系统中的所有组件都使用相同的事件循环,无论是通过使用公共事件库还是通过调整组件以与单个循环一起工作。
10. 安全流
来源: https://zread.ai/warmcat/libwebsockets/10-secure-streams
Secure Streams 是 libwebsockets 中一个强大的抽象层,它从根本上改变了你处理网络通信的方式。本指南将解释 Secure Streams 是什么,为什么它很有价值,以及如何在你的应用程序中使用它。
Secure Streams 是什么?
Secure Streams 是一个专注于有效负载的网络 API,它严格地将数据与连接元数据分开。与传统的网络编程不同,在传统网络编程中,你必须在代码中指定端点、协议和安全细节,Secure Streams 将所有这些配置细节移至外部的策略数据库。
架构示意图:
仅有效负载 ←→ 你的应用程序 ←→ Secure Streams API ←→ 网络
↓
处理连接细节
↓
配置(策略数据库)
作为开发者,你只需要处理:
- 发送和接收有效负载数据
- 对连接状态变化做出反应
其他所有内容------端点、TLS 设置、协议选择、重试行为------都外部化并可通过配置更改,而无需修改代码。
来源:lws-secure-streams.h#L31-L56
为什么使用 Secure Streams?
Secure Streams 解决了网络应用设计中的几个基本问题:
- 关注点分离:应用逻辑与连接细节完全解耦
- 无需重新编译的配置:更改端点、协议或安全设置,而无需触及代码
- 一致的 API:无论底层协议是什么(HTTP、WebSockets、MQTT),接口都相同
- 自动重试和退避:内置可靠性机制
- 简化的安全管理:TLS 配置集中在策略中
提示: Secure Streams 对于 IoT 设备和嵌入式系统尤其有价值,在这些系统中,你可能需要更改连接端点或安全证书,而无需更新固件。
来源:README.secure-streams.md#L1-L22
Secure Streams 架构
Secure Streams 提供两种不同的操作模式:
1. 直接回调模式
在这种模式下,你的应用程序代码直接从 libwebsockets 事件循环线程中被调用。这是单进程应用程序最简单、最高效的方法。
流程:
创建流 → LWSSSCS_CREATING 状态 → 处理连接细节 → 使用策略中的协议连接
↓
连接建立 → 连接就绪 → LWSSSCS_CONNECTED 状态
↓
请求发送 → TX 回调(准备发送) → 返回有效负载 → 发送有效负载
↓
传输数据 → 接收数据 → 数据接收 → 带有效负载的 RX 回调
2. 代理模式(IPC)
在这种模式下,你的应用程序在独立的进程中进行,通过代理套接字使用序列化 Secure Streams(SSS)协议与 libwebsockets 通信。
架构:
用户进程 ←→ API 调用 ←→ 套接字 IPC ←→ LWS 进程
↓ ↓
用户代码 libwebsockets
SS Shim 互联网
SS 代理
来源:README.secure-streams.md#L8-L19, lws-secure-streams.h#L38-L56
生命周期和状态机
Secure Streams 实现了一个全面的状态机,处理整个连接生命周期:
CREATING → CONNECTING → CONNECTED → QOS_ACK_REMOTE → 事务成功
↓ ↓ ↓
↓ ↓ UNREACHABLE
↓ ↓ ↓
↓ AUTH_FAILED ↓
↓ ↓ ↓
↓ ↓ ALL_RETRIES_FAILED
↓ ↓ ↓
↓ ↓ DISCONNECTED
↓ ↓ ↓
↓ (重试) ← (重试) ← (重试)
这些状态转换通过状态回调报告给你的应用程序,允许你对连接事件做出适当反应。
来源:secure-streams.c#L32-L74
开始使用 Secure Streams
步骤 1:创建用户对象类型
首先,定义你的流的用户数据结构:
c
typedef struct myss {
struct lws_ss_handle *ss; // 必须存在
void *opaque_data; // 必须存在
// 你的自定义数据
uint8_t rx_buffer[1024];
size_t rx_len;
// ... 其他状态变量
} myss_t;
步骤 2:实现所需的回调
c
// 处理接收到的数据
static lws_ss_state_return_t
myss_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
{
myss_t *m = (myss_t *)userobj;
// 处理接收到的数据
memcpy(m->rx_buffer, buf, len);
m->rx_len = len;
return LWSSSSRET_OK;
}
// 提供要发送的数据
static lws_ss_state_return_t
myss_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len, int *flags)
{
myss_t *m = (myss_t *)userobj;
// 填充缓冲区以发送数据
const char *msg = "Hello, Secure Streams!";
size_t msg_len = strlen(msg);
memcpy(buf, msg, msg_len);
*len = msg_len;
*flags = LWSSS_FLAG_SOM | LWSSS_FLAG_EOM; // 消息开始和结束
return LWSSSSRET_OK;
}
// 处理状态变化
static lws_ss_state_return_t
myss_state(void *userobj, void *h_src, lws_ss_constate_t state, lws_ss_tx_ordinal_t ack)
{
myss_t *m = (myss_t *)userobj;
switch (state) {
case LWSSSCS_CREATING:
printf("流正在创建\n");
break;
case LWSSSCS_CONNECTED:
printf("流已连接\n");
// 连接时请求发送数据
lws_ss_request_tx(m->ss);
break;
case LWSSSCS_DISCONNECTED:
printf("流已断开\n");
break;
// ... 处理其他状态
}
return LWSSSSRET_OK;
}
步骤 3:创建流
c
const lws_ss_info_t ssi = {
.handle_offset = offsetof(myss_t, ss),
.opaque_user_data_offset = offsetof(myss_t, opaque_data),
.rx = myss_rx,
.tx = myss_tx,
.state = myss_state,
.user_alloc = sizeof(myss_t),
.streamtype = "mystream" // 必须与策略中的类型匹配
};
struct lws_ss_handle *ss;
lws_ss_create(context, 0, &ssi, NULL, &ss, NULL, NULL);
来源:lws-secure-streams.h#L147-L186
JSON 策略配置
策略数据库是一个 JSON 文档,定义了所有连接细节。以下是一个简化的示例:
json
{
"release": "01234567",
"product": "myproduct",
"schema-version": 1,
"retry": [{
"default": {
"backoff": [1000, 2000, 3000, 5000, 10000],
"conceal": 5,
"jitterpc": 20
}
}],
"certs": [{
"isrg_root_x1": "MIIFazCCA1OgAw...AnX5iItreGCc="
}],
"trust_stores": [{
"le_via_isrg": ["isrg_root_x1"]
}],
"s": [{
"mystream": {
"endpoint": "api.example.com",
"port": 443,
"protocol": "h1",
"aux": "/api/v1/data",
"plugins": [],
"tls": true,
"retry": "default",
"tls_trust_store": "le_via_isrg"
}
}]
}
此策略定义:
- 默认的重试策略,带有指数退避
- 证书数据
- 引用该证书的信任存储
- 一个名为 "mystream" 的流类型,使用 HTTPS 连接到 api.example.com
来源:README.secure-streams.md#L178-L204
高级功能
流元数据
你可以设置和获取与流相关的元数据,例如自定义 HTTP 头:
c
lws_ss_set_metadata(m->ss, "custom-header", "header-value", 12);
请求-响应事务流程
对于类似 HTTP 的事务:
- 创建流
- 连接后,调用
lws_ss_request_tx() - 在你的 tx 回调中,填充缓冲区以发送请求
- 在你的 rx 回调中接收响应
- 事务成功完成时,状态变为
LWSSSCS_QOS_ACK_REMOTE
服务器端 Secure Streams
Secure Streams 也支持服务器功能,其生命周期略有不同:
- 创建一个服务器类型的 Secure Stream 以建立监听套接字
- 当客户端连接时,为每个连接创建新的 Secure Streams
LWSSSCS_SERVER_TXN状态表示可以准备事务
来源:README.secure-streams.md#L81-L146
协议支持
Secure Streams 通过专用处理程序支持多种协议:
| 协议 | 处理程序 | 描述 |
|---|---|---|
| HTTP/1 | ss-h1.c | HTTP/1.x 客户端请求 |
| HTTP/2 | ss-h2.c | HTTP/2 客户端请求 |
| WebSocket | ss-ws.c | WebSocket 客户端连接 |
| MQTT | ss-mqtt.c | MQTT 客户端连接 |
| Raw | ss-raw.c | 原始套接字连接 |
根据你的策略配置选择适当的协议处理程序。
来源:lib/secure-streams/protocols
结论
Secure Streams 通过将有效负载处理与连接细节分离,代表了网络编程范式的转变。这种架构促进了:
- 更好的安全性:连接和安全细节与应用逻辑隔离
- 改进的可维护性:更改端点或协议而无需修改代码
- 简化的 API:无论底层协议如何,接口都一致
- 弹性连接:内置重试和退避机制
通过使用 Secure Streams,你可以专注于应用程序的核心功能,而将网络连接、协议和安全的复杂性委托给 libwebsockets 库。
更多详情,请查看仓库中的最小示例,它们展示了实际的使用模式。
12. WebSocket客户端与服务器
来源: https://zread.ai/warmcat/libwebsockets/12-websocket-client-and-server
WebSockets 通过单个 TCP 连接提供全双工通信通道,实现客户端和服务器之间的实时双向数据传输。Libwebsockets (LWS) 提供了 WebSocket 协议的强大实现,并具有跨平台兼容性。本指南将指导您如何使用 libwebsockets 实现 WebSocket 客户端和服务器。
架构概述
Libwebsockets 通过事件驱动架构实现 WebSockets。该库管理 WebSocket 协议的所有复杂性(如握手、帧处理、掩码等),并通过回调提供简化的 API。
Libwebsockets 中的通信遵循非阻塞、事件驱动的模型,您的应用注册回调,当事件发生(如建立连接、接收到数据等)时触发回调。
来源:libwebsockets.h
WebSocket 协议基础
WebSockets 运行在两个主要阶段:
- 握手阶段:初始 HTTP 升级请求,从 HTTP 切换到 WebSocket 协议
- 数据传输阶段:双向二进制或文本帧交换
在 LWS 中,WebSocket 帧可以是:
- 文本帧:UTF-8 编码的数据
- 二进制帧:原始二进制数据
- 控制帧:用于协议级操作(如 ping/pong,关闭)
您可以使用辅助函数检查帧特性:
lws_frame_is_binary():检查当前帧是否为二进制lws_is_final_fragment():检查这是否为消息的最后一个片段lws_get_opcode():获取 WebSocket 操作码
来源:lws-ws-state.h
设置 WebSocket 客户端
创建 WebSocket 客户端连接需要填写 lws_client_connect_info 结构体,包含连接详细信息:
c
struct lws_client_connect_info info;
memset(&info, 0, sizeof(info));
info.context = context; // 您的 LWS 上下文
info.address = "echo.websocket.org"; // 服务器地址
info.port = 443; // 服务器端口
info.path = "/"; // URI 路径
info.host = "echo.websocket.org"; // 主机头
info.origin = "origin"; // 来源头
info.protocol = "protocol-name"; // WS 子协议
info.ssl_connection = LCCSCF_USE_SSL; // 如需使用 SSL,设置 SSL 标志
// 连接
struct lws *wsi = lws_client_connect_via_info(&info);
关键连接标志包括:
LCCSCF_USE_SSL:使用 TLS/SSL 进行安全连接LCCSCF_ALLOW_SELFSIGNED:接受自签名证书LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK:跳过主机名验证
来源:lws-client.h
实现 WebSocket 服务器
在 libwebsockets 中,WebSocket 服务器通过协议处理器和虚拟主机实现:
定义协议结构体:
c
static struct lws_protocols protocols[] = {
{
"my-protocol", // 协议名称
callback_my_protocol, // 回调函数
sizeof(struct per_session_data), // 每个连接的数据大小
1024, // Tx 数据包缓冲区大小
0, NULL, 0 // 其他参数(id,用户指针等)
},
{ NULL, NULL, 0, 0, 0, NULL, 0 } // 终结符
};
创建和配置上下文:
c
struct lws_context_creation_info info;
memset(&info, 0, sizeof(info));
info.port = 8080;
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
struct lws_context *context = lws_create_context(&info);
运行事件循环:
c
while (1) {
lws_service(context, 0);
}
服务器实现的核心在于协议回调函数,它处理各种事件,如建立连接、接收数据和断开连接。
来源:lws-protocols-plugins.h
处理 WebSocket 事件
客户端和服务器都使用回调处理 WebSocket 事件。回调函数的签名如下:
c
int callback_function(
struct lws *wsi,
enum lws_callback_reasons reason,
void *user,
void *in,
size_t len
)
关键的回调原因包括:
LWS_CALLBACK_ESTABLISHED:WebSocket 连接建立LWS_CALLBACK_RECEIVE:接收到数据LWS_CALLBACK_SERVER_WRITEABLE:套接字可写LWS_CALLBACK_CLIENT_RECEIVE:客户端接收到数据LWS_CALLBACK_CLIENT_WRITEABLE:客户端套接字可写LWS_CALLBACK_CLOSED:连接关闭
示例回调实现:
c
int callback_my_protocol(
struct lws *wsi,
enum lws_callback_reasons reason,
void *user,
void *in,
size_t len
) {
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
printf("连接建立\n");
break;
case LWS_CALLBACK_RECEIVE:
printf("收到 %zu 字节: %.*s\n", len, (int)len, (char *)in);
// 请求写入机会(响应)
lws_callback_on_writable(wsi);
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
// 写入响应
char buf[LWS_PRE + 128];
const char *msg = "来自服务器的问候!";
size_t msg_len = strlen(msg);
memcpy(&buf[LWS_PRE], msg, msg_len);
lws_write(wsi, (unsigned char*)&buf[LWS_PRE], msg_len, LWS_WRITE_TEXT);
break;
}
return 0;
}
来源:lws-callbacks.h
发送和接收 WebSocket 消息
发送消息
要通过 WebSockets 发送数据:
- 使用
lws_callback_on_writable(wsi)请求可写回调 - 当
LWS_CALLBACK_SERVER_WRITEABLE或LWS_CALLBACK_CLIENT_WRITEABLE事件发生时:- 准备一个缓冲区,前面保留
LWS_PRE字节(用于协议头) - 使用
lws_write()发送数据
- 准备一个缓冲区,前面保留
c
// 在可写回调内:
char buf[LWS_PRE + 100]; // 为协议头保留 LWS_PRE 字节
strcpy(&buf[LWS_PRE], "你好 WebSocket!");
lws_write(wsi, (unsigned char*)&buf[LWS_PRE], strlen("你好 WebSocket!"), LWS_WRITE_TEXT);
写入模式包括:
LWS_WRITE_TEXT:作为文本帧发送LWS_WRITE_BINARY:作为二进制帧发送LWS_WRITE_CONTINUATION:继续分段消息
来源:lws-write.h
接收消息
消息通过回调以 LWS_CALLBACK_RECEIVE 或 LWS_CALLBACK_CLIENT_RECEIVE 原因接收:
c
case LWS_CALLBACK_RECEIVE:
// 'in' 包含接收到的数据
// 'len' 是数据长度
// 检查是否为二进制
bool is_binary = lws_frame_is_binary(wsi);
// 检查是否为最后一个片段
bool is_final = lws_is_final_fragment(wsi);
// 处理数据
printf("收到 %s 数据: %.*s\n",
is_binary ? "二进制" : "文本",
(int)len, (char *)in);
break;
性能提示 :对于大消息,使用分段消息处理,通过
lws_is_first_fragment()和lws_is_final_fragment()函数高效处理到达的数据,而不是等待整个消息。
来源:lws-ws-state.h
完整的 WebSocket 客户端示例
以下是一个连接到服务器并交换消息的简单 WebSocket 客户端:
c
#include <libwebsockets.h>
#include <string.h>
#include <signal.h>
static int interrupted;
static int
callback_client(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
switch (reason) {
case LWS_CALLBACK_CLIENT_ESTABLISHED:
printf("连接到服务器\n");
lws_callback_on_writable(wsi);
break;
case LWS_CALLBACK_CLIENT_RECEIVE:
printf("收到:%.*s\n", (int)len, (char *)in);
break;
case LWS_CALLBACK_CLIENT_WRITEABLE: {
unsigned char buf[LWS_PRE + 128];
const char *message = "来自 WebSocket 客户端的问候!";
size_t message_len = strlen(message);
memcpy(&buf[LWS_PRE], message, message_len);
lws_write(wsi, &buf[LWS_PRE], message_len, LWS_WRITE_TEXT);
break;
}
case LWS_CALLBACK_CLOSED:
printf("连接关闭\n");
break;
}
return 0;
}
static struct lws_protocols protocols[] = {
{
"example-protocol",
callback_client,
0, // 无会话数据
0, // rx 缓冲区大小(0 = 默认)
0, NULL, 0
},
{ NULL, NULL, 0, 0, 0, NULL, 0 } // 终结符
};
void sigint_handler(int sig)
{
interrupted = 1;
}
int main(void)
{
struct lws_context_creation_info info;
struct lws_client_connect_info conn_info;
struct lws_context *context;
signal(SIGINT, sigint_handler);
memset(&info, 0, sizeof info);
info.port = CONTEXT_PORT_NO_LISTEN;
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
context = lws_create_context(&info);
if (!context) {
fprintf(stderr, "上下文创建失败\n");
return 1;
}
memset(&conn_info, 0, sizeof conn_info);
conn_info.context = context;
conn_info.address = "echo.websocket.org";
conn_info.port = 443;
conn_info.path = "/";
conn_info.host = conn_info.address;
conn_info.origin = "origin";
conn_info.protocol = protocols[0].name;
conn_info.ssl_connection = LCCSCF_USE_SSL;
lws_client_connect_via_info(&conn_info);
while (!interrupted) {
lws_service(context, 0);
}
lws_context_destroy(context);
return 0;
}
完整的 WebSocket 服务器示例
以下是一个回显接收消息的简单 WebSocket 服务器:
c
#include <libwebsockets.h>
#include <string.h>
#include <signal.h>
static int interrupted;
// 会话数据
struct per_session_data {
size_t msg_len;
unsigned char buffer[4096];
};
static int
callback_example(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
struct per_session_data *psd = (struct per_session_data *)user;
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
printf("客户端连接\n");
break;
case LWS_CALLBACK_RECEIVE:
// 将消息复制到我们的缓冲区
memcpy(psd->buffer, in, len);
psd->msg_len = len;
// 请求当套接字可写时的回调
lws_callback_on_writable(wsi);
break;
case LWS_CALLBACK_SERVER_WRITEABLE: {
// 回显消息
unsigned char buf[LWS_PRE + 4096];
memcpy(&buf[LWS_PRE], psd->buffer, psd->msg_len);
// 发送消息
lws_write(wsi, &buf[LWS_PRE], psd->msg_len,
lws_frame_is_binary(wsi) ? LWS_WRITE_BINARY : LWS_WRITE_TEXT);
break;
}
case LWS_CALLBACK_CLOSED:
printf("客户端断开连接\n");
break;
default:
break;
}
return 0;
}
static struct lws_protocols protocols[] = {
{
"example-protocol",
callback_example,
sizeof(struct per_session_data),
4096, // rx 缓冲区大小
0, NULL, 0
},
{ NULL, NULL, 0, 0, 0, NULL, 0 } // 终结符
};
void sigint_handler(int sig)
{
interrupted = 1;
}
int main(void)
{
struct lws_context_creation_info info;
struct lws_context *context;
signal(SIGINT, sigint_handler);
memset(&info, 0, sizeof info);
info.port = 8000;
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
context = lws_create_context(&info);
if (!context) {
fprintf(stderr, "上下文创建失败\n");
return 1;
}
printf("WebSocket 服务器启动在端口 %d\n", info.port);
while (!interrupted) {
lws_service(context, 0);
}
lws_context_destroy(context);
return 0;
}
高级 WebSocket 特性
连接状态管理
Libwebsockets 提供函数检查连接状态:
lws_send_pipe_choked(wsi): 如果连接当前无法接受更多数据,返回非零值lws_partial_buffered(wsi): 如果上次写入仅部分完成,返回非零值
c
if (!lws_send_pipe_choked(wsi)) {
// 安全发送更多数据
lws_write(wsi, /* ... */);
}
来源:lws-ws-state.h
安全 WebSockets (WSS)
要使用安全 WebSockets (WSS),在客户端连接信息中设置 LCCSCF_USE_SSL 标志:
c
conn_info.ssl_connection = LCCSCF_USE_SSL;
对于服务器,提供 SSL 证书和密钥路径:
c
info.ssl_cert_filepath = "/path/to/cert.pem";
info.ssl_private_key_filepath = "/path/to/key.pem";
来源:lws-client.h
WebSocket 扩展
Libwebsockets 支持如 permessage-deflate 的 WebSocket 扩展用于压缩:
c
// 对于服务器
info.extensions = lws_get_internal_extensions();
// 对于客户端
conn_info.client_exts = lws_get_internal_extensions();
最佳实践
-
始终使用 LWS_PRE :在使用
lws_write()时,始终在缓冲区开头保留LWS_PRE字节用于协议头。 -
使用可写回调 :在尝试写入数据前,始终使用
lws_callback_on_writable()请求可写回调。 -
检查阻塞状态 :在写入大量数据前,使用
lws_send_pipe_choked()检查套接字是否阻塞。 -
处理分段消息 :对于大消息,使用
lws_is_first_fragment()和lws_is_final_fragment()处理分段帧。 -
控制事件循环:不要在事件循环中过度写入,应在服务周期中分散写入操作。
结论
Libwebsockets 提供了一个强大且灵活的 API,用于实现 WebSocket 客户端和服务器。通过利用事件驱动架构和回调系统,您可以轻松构建高性能的实时应用程序。
该库处理所有 WebSocket 协议的复杂性,使您能够专注于应用逻辑,而不是 WebSocket 帧处理、握手和连接管理的细节。
无论您是在构建简单的聊天应用、复杂的实时仪表板还是多人游戏,libwebsockets 都提供了您所需的工具,以建立可靠的客户端和服务器之间的双向通信。
16. TLS集成
来源: https://zread.ai/warmcat/libwebsockets/16-tls-integration
传输层安全协议(TLS)是保障网页通信安全的关键组件。Libwebsockets(LWS)为客户端和服务器应用程序提供了全面的TLS支持,拥有多个后端选项和配置能力。本文档将介绍如何在您的libwebsockets应用程序中集成和配置TLS。
概述
Libwebsockets提供了内置的TLS集成,支持以下功能:
- 多个TLS后端(OpenSSL, mbedTLS, WolfSSL)
- 服务器和客户端证书管理
- 通过ALPN选择协议
- 资源使用控制
- 用于提升性能的会话管理
该库通过一个通用API层抽象了TLS实现的细节,使您能够在不直接处理特定后端代码的情况下使用TLS。
参考资料:private-lib-tls.h
启用TLS支持
TLS支持必须在编译时启用。主要选项是LWS_WITH_TLS,此外还有用于选择特定后端的附加选项。
cmake
set(LWS_WITH_TLS ON)
# 使用mbedTLS而不是OpenSSL(默认)
set(LWS_WITH_MBEDTLS ON)
对于ESP32和其他嵌入式平台,有特定的配置选项:
cmake
# 对于ESP32
set(LWS_WITH_MBEDTLS ON)
set(LWS_FOR_ESP32 ON)
参考资料:CMakeLists.txt
服务器端TLS集成
基本服务器TLS设置
要创建支持TLS的服务器:
- 设置所需的上下文创建选项
- 提供证书和密钥路径
- 初始化启用TLS的虚拟主机
以下是一个简化示例:
c
struct lws_context_creation_info info;
memset(&info, 0, sizeof info);
// 配置SSL/TLS
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
info.ssl_cert_filepath = "/path/to/cert.pem";
info.ssl_private_key_filepath = "/path/to/key.pem";
info.ssl_cipher_list = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384";
// 可选:要求客户端证书
if (require_client_cert)
info.options |= LWS_SERVER_OPTION_REQUIRE_VALID_OPENSSL_CLIENT_CERT;
// 创建启用TLS的上下文
struct lws_context *context = lws_create_context(&info);
库会自动处理TLS库的初始化和上下文创建期间证书的加载。
参考资料:tls-server.c
使用内存中的证书
除了从文件加载证书,您还可以在内存中提供它们:
c
info.server_ssl_cert_mem = cert_data;
info.server_ssl_cert_mem_len = cert_len;
info.server_ssl_private_key_mem = key_data;
info.server_ssl_private_key_mem_len = key_len;
这对于没有文件系统或证书动态生成的嵌入式系统特别有用。
参考资料:tls-server.c#L49-L52
客户端证书认证
要求并验证客户端证书:
c
// 要求客户端提供有效证书
info.options |= LWS_SERVER_OPTION_REQUIRE_VALID_OPENSSL_CLIENT_CERT;
// 提供CA证书以验证客户端证书
info.ssl_ca_filepath = "/path/to/ca.pem";
服务器将在TLS握手期间使用提供的CA验证客户端证书。
参考资料:tls-server.c#L100-L113
ALPN支持协议选择
应用层协议协商(ALPN)允许在TLS握手期间动态选择协议,这对于HTTP/2支持至关重要:
c
// 启用ALPN并指定协议
info.alpn = "h2,http/1.1";
这配置服务器通过ALPN广告支持HTTP/2和HTTP/1.1。
参考资料:tls.c#L203-L225
客户端TLS集成
基本客户端TLS设置
对于支持TLS的客户端连接:
c
struct lws_client_connect_info i;
memset(&i, 0, sizeof i);
i.context = context;
i.ssl_connection = LCCSCF_USE_SSL; // 启用TLS
i.host = "example.com";
i.port = 443;
// 使用TLS连接
struct lws *wsi = lws_client_connect_via_info(&i);
ssl_connection字段为客户端连接启用TLS。库会自动处理TLS握手。
参考资料:tls-client.c
客户端证书认证
使用客户端证书进行认证:
c
// 创建带有客户端证书选项的上下文
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
info.client_ssl_cert_filepath = "/path/to/client-cert.pem";
info.client_ssl_private_key_filepath = "/path/to/client-key.pem";
// 创建上下文
struct lws_context *context = lws_create_context(&info);
// 然后使用此上下文进行客户端连接
当服务器请求时,这些证书将用于客户端认证。
参考资料:tls-client.c#L151-L154
自定义证书验证
对于高级证书验证,您可以注册一个回调:
c
// 在您的协议定义中
static int callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
{
switch (reason) {
case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
// 在此处自定义证书验证
// 'in' 包含 SSL_CTX*
return 0;
}
return 0;
}
此回调允许自定义证书处理,如加载额外的根证书或实现自定义验证逻辑。
参考资料:tls-client.c#L172-L177
高级功能
TLS会话管理
Libwebsockets支持TLS会话缓存,以加速对同一服务器的后续连接:
c
// 在上下文创建期间启用会话缓存
info.options |= LWS_SERVER_OPTION_ENABLE_SSL_SESSION_CACHE;
// 设置缓存会话的超时时间(以秒为单位)
info.ssl_session_cache_timeout = 3600; // 1小时
这通过避免重复连接的完整TLS握手来提高性能。
参考资料:tls-sessions.c
资源使用控制
Libwebsockets提供了限制TLS资源使用的机制:
c
// 限制并发TLS连接
info.simultaneous_ssl_restriction = 10;
// 限制并发TLS握手
info.simultaneous_ssl_handshake_restriction = 5;
这些限制有助于防止资源耗尽,特别是在资源受限的环境中。
参考资料:tls.c#L81-L110
证书续期监控
库可以自动检查证书过期:
c
// 在上下文创建期间
info.options |= LWS_SERVER_OPTION_AUTO_CERT_EXPIRY_MONITORING;
这启用了一个后台任务,定期检查证书过期日期,并记录即将过期的证书警告。
参考资料:tls-server.c#L105-L111
使用不同的TLS后端
Libwebsockets抽象了不同TLS实现之间的差异,但某些后端特定功能可能需要直接访问底层的TLS对象。
访问OpenSSL对象
c
// 从wsi获取OpenSSL SSL对象
SSL *ssl = lws_get_ssl(wsi);
if (ssl) {
// 使用OpenSSL对象
}
访问mbedTLS对象
当使用mbedTLS编译时,相同的API提供对mbedTLS对象的访问。
重要提示 : 在直接使用TLS对象时,始终检查库是否使用预期的TLS后端编译,使用
LWS_WITH_MBEDTLS或类似的定义。直接操作TLS对象会降低在不同TLS后端之间的可移植性。
故障排除TLS连接
常见问题
- 证书验证失败:确保您的证书有效、未过期且格式正确(PEM或DER)。
- 缺少中间证书:确保您的证书链完整。
- 私钥/证书不匹配:验证私钥是否与证书对应。
启用TLS调试日志
要启用详细的TLS日志:
c
// 在上下文创建期间设置日志级别
info.log_level = LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG | LLL_TLS;
LLL_TLS标志启用TLS特定的调试信息。
结论
Libwebsockets提供了一个强大且灵活的TLS实现,适用于不同平台和多个TLS后端。通过在上下文创建和连接设置期间正确配置TLS,您可以最小化代码量来保障WebSocket和HTTP通信的安全。
对于特定平台的TLS考虑,特别是嵌入式系统,请参考Libwebsockets存储库中READMEs目录下的平台特定文档。
13. HTTP实现
来源: https://zread.ai/warmcat/libwebsockets/13-http-implementation
libwebsockets库提供了一个健壮且全面的HTTP实现,支持客户端和服务器操作。本指南将向您介绍libwebsockets中HTTP的实现方式、其架构以及如何在您的应用程序中使用它。
概述
Libwebsockets将HTTP实现作为其核心网络能力的一部分,既作为WebSocket连接的基础,也作为独立的HTTP服务器/客户端。HTTP实现遵循以下关键原则:
- 性能优先:针对嵌入式和高性能系统进行优化
- 内存高效:为资源受限环境提供最小的内存占用
- 可扩展:支持HTTP/1.0、HTTP/1.1和HTTP/2
- 易于集成:与WebSockets无缝协作
参考资料:lws-http.h
架构
Libwebsockets中的HTTP实现围绕以下主要组件构建:
- HTTP解析器:处理HTTP头部和请求/响应主体
- 连接管理:处理管道化、保持连接和连接池
- 内容服务:提供文件和动态内容服务的API
- 头部处理:处理HTTP头部的实用工具
- Cookie和缓存管理:可选的Cookie和HTTP缓存处理
HTTP请求流程:
客户端 → HTTP请求 → libwebsockets
↓
检查管道化/重用 → HTTP解析器 → 解析头部
↓
连接管理器 → 处理连接 → 执行用户回调
↓
生成响应 → HTTP响应 → 客户端
参考资料:lib/core-net/README.md
HTTP头部处理
HTTP处理的一个核心方面是处理头部。Libwebsockets使用高效的标记化方法处理HTTP头部:
c
enum lws_token_indexes {
WSI_TOKEN_GET_URI, // 用于GET请求URI
WSI_TOKEN_POST_URI, // 用于POST请求URI
WSI_TOKEN_HOST, // Host头部
WSI_TOKEN_CONNECTION, // Connection头部
// 更多用于各种HTTP头部的标记
// ...
};
头部在HTTP握手期间被解析并临时存储,然后在协议过渡到主要操作时释放,以节省内存,特别是对于长连接。
参考资料:lws-http.h#L215-L368
访问HTTP头部
为了访问头部值,libwebsockets提供了以下关键函数:
c
// 获取头部的总长度
int length = lws_hdr_total_length(wsi, WSI_TOKEN_HOST);
// 将头部复制到缓冲区
char host[256];
lws_hdr_copy(wsi, host, sizeof(host), WSI_TOKEN_HOST);
// 处理带有片段的URL参数
char arg[256];
lws_hdr_copy_fragment(wsi, arg, sizeof(arg), WSI_TOKEN_HTTP_URI_ARGS, 0);
对于URL参数(如?name=value),libwebsockets提供了特殊处理,以安全处理可能的二进制数据:
c
char value[256];
lws_get_urlarg_by_name_safe(wsi, "name=", value, sizeof(value));
在处理URL参数时,始终使用长度感知的API如
lws_get_urlarg_by_name_safe(),而不是旧的lws_get_urlarg_by_name(),以正确处理二进制数据并避免嵌入NULL字节的安全问题。
参考资料:README.http_parser.md#L19-L33
提供HTTP内容
静态文件服务
Libwebsockets使得通过HTTP提供静态文件变得简单:
c
int lws_serve_http_file(struct lws *wsi,
const char *file,
const char *content_type,
const char *other_headers,
int other_headers_len);
库自动处理:
- 基于文件扩展名的内容类型检测
- 部分内容的范围请求
- ETags和条件请求(If-Modified-Since)
- 必要时的分块传输编码
参考资料:lws-http.h#L56-L79
动态内容生成
对于动态内容,libwebsockets提供了一种分块替换机制,允许在HTML模板中进行变量替换:
c
struct lws_process_html_args args;
struct lws_process_html_state state;
// 使用要替换的变量初始化状态
state.vars = my_vars;
state.count_vars = my_var_count;
state.replace = my_replacement_callback;
// 处理带有替换的HTML块
lws_chunked_html_process(&args, &state);
这对于在静态HTML页面中嵌入动态数据特别有用。
参考资料:lws-http.h#L121-L161
HTTP客户端功能
Libwebsockets中的HTTP客户端实现支持:
- 连接池和管道化:在可能的情况下重用连接以提高性能
- HTTP/2多路复用:通过单个连接进行多个请求
- Cookie处理:自动存储和应用Cookie
- 重定向处理:跟随HTTP重定向
- 客户端连接排队
一个关键的性能特性是客户端连接排队:
客户端请求
↓
现有连接? → 是 → 在领导者上排队 → 等待轮次 → 处理请求
↓
否
↓
创建新连接 → 成为领导者 → 处理请求
↓
完成交易
↓
队列中有更多? → 是 → 将连接传递给下一个
↓
否
↓
保持连接空闲
在创建带有LCCSCF_PIPELINE标志的客户端连接时,libwebsockets通过以下方式优化:
- 将多个请求排队到同一主机
- 重用TLS握手和TCP连接
- 对于HTTP/1,顺序处理请求
- 对于HTTP/2,在连接建立后并发处理请求
参考资料:lib/core-net/README.md#L3-L52
HTTP缓存支持
Libwebsockets包括客户端侧的HTTP Cookie和缓存管理:
c
// 在上下文创建信息中
info.http_nsc_filepath = "/path/to/cookie.jar";
info.http_nsc_heap_max_footprint = 32 * 1024; // 32KB缓存限制
info.http_nsc_heap_max_items = 20; // 最大20个缓存Cookie
对于每个客户端连接,启用缓存:
c
i.ssl_connection |= LCCSCF_CACHE_COOKIES;
这维护了一个带有LRU(最近最少使用)跟踪和基于Cookie寿命自动过期的Level 1堆缓存。
参考资料:README.http-cache.md#L1-L40
HTTP与WebSockets的集成
由于WebSockets从HTTP升级握手开始,HTTP实现与WebSocket协议紧密集成。典型流程是:
- 处理初始HTTP连接
- 解析HTTP头部,寻找WebSocket升级头部
- 如果存在升级头部,执行WebSocket握手
- 如果不是WebSocket升级,视为普通HTTP请求
这种集成允许单个服务器在同一个端口上高效处理HTTP和WebSocket连接。
示例:基本HTTP服务器
以下是一个使用libwebsockets设置HTTP服务器的简化示例:
c
// 定义HTTP请求的回调
static int
callback_http(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
switch (reason) {
case LWS_CALLBACK_HTTP:
// 请求URI在"in"中
const char *uri = (const char *)in;
// 根据URI提供文件
if (!strcmp(uri, "/")) {
return lws_serve_http_file(wsi, "index.html", "text/html", NULL, 0);
}
// 其他情况返回404
return -1;
default:
break;
}
return lws_callback_http_dummy(wsi, reason, user, in, len);
}
// 协议定义
static const struct lws_protocols protocols[] = {
{
"http-only", // 名称
callback_http, // 回调
0, // 每连接数据大小
0, // RX缓冲区大小
},
{ NULL, NULL, 0, 0 } // 终结符
};
// 创建和运行上下文
int main(void)
{
struct lws_context_creation_info info = {0};
info.port = 8000;
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
struct lws_context *context = lws_create_context(&info);
while (1) {
lws_service(context, 1000);
}
lws_context_destroy(context);
return 0;
}
高级HTTP特性
HTTP/2支持
Libwebsockets包括对HTTP/2的支持,具有:
- 流多路复用
- 服务器推送
- 头部压缩
- 优先级处理
HTTP/2通过使用TLS时的ALPN(应用层协议协商)自动协商,或可以显式启用。
长轮询
对于需要服务器到客户端更新但不使用WebSockets的应用程序,libwebsockets提供了优化的HTTP/2长轮询:
c
// 在HTTP/2流上启用长轮询
lws_h2_client_stream_long_poll(wsi);
这利用了HTTP/2更高效的连接处理来进行长连接请求。
结论
Libwebsockets中的HTTP实现为传统网页服务和高级应用提供了全面的基础。其与WebSockets的集成、高效的内存使用和性能优化,使其特别适合嵌入式系统、物联网设备和高性能服务器。
通过理解本指南中描述的架构和API,您将能够有效使用libwebsockets满足您的HTTP服务和客户端需求,无论是独立使用还是作为WebSocket启用应用的一部分。
11. 插件系统
来源: https://zread.ai/warmcat/libwebsockets/11-plugin-system
libwebsockets插件系统提供了一种灵活的方式,通过模块化组件来扩展库的功能,这些组件可以在运行时动态加载或直接构建到库中。这种架构允许核心功能与协议特定的实现清晰分离,使代码库更易于维护和扩展。
为什么使用插件?
libwebsockets中的插件提供了几个关键优势:
- 模块化:保持核心应用简洁,按需添加协议
- 动态加载:在运行时加载协议实现,无需重新编译
- 代码复用:在多个项目中共享协议实现
- 关注点分离:将协议特定逻辑与应用逻辑隔离
- 可扩展性:添加新功能而不修改库核心
来源:README.lws_plugins.md
插件架构
核心组件
插件系统的核心有两个主要结构:
- 插件头文件:所有插件共享一个通用的头文件结构,用于识别插件类型和版本
- 插件运行时:表示内存中动态加载的插件实例
系统支持不同类型的插件,其中协议插件是libwebsockets中最常见的用例。
来源:lws-protocols-plugins.h#L228-L242
插件头文件结构
每个libwebsockets插件必须有一个符合以下结构的头文件:
c
typedef struct lws_plugin_header {
const char *name; // 人可读的插件名称
const char *_class; // 插件类标识符
const char *lws_build_hash; // 在构建时设置为LWS_BUILD_HASH
unsigned int api_magic; // 设置为LWS_PLUGIN_API_MAGIC以进行兼容性检查
// 插件类特定数据紧随其后
} lws_plugin_header_t;
api_magic字段(设置为LWS_PLUGIN_API_MAGIC)确保插件与当前版本的libwebsockets兼容。
来源:lws-protocols-plugins.h#L228-L242
协议插件结构
对于WebSocket协议实现,libwebsockets提供了一种专门的插件类型:
c
typedef struct lws_plugin_protocol {
lws_plugin_header_t hdr; // 通用插件头文件
const struct lws_protocols *protocols; // 协议实现数组
const struct lws_extension *extensions; // 可选扩展
int count_protocols; // 协议数量
int count_extensions; // 扩展数量
} lws_plugin_protocol_t;
来源:lws-protocols-plugins.h#L246-L253
创建协议插件
让我们通过查看内置的"dumb increment"协议示例,来逐步创建一个简单的协议插件:
1. 定义你的协议数据结构
首先为你的协议定义会话级和虚拟主机级的数据结构:
c
// 会话级数据
struct pss__dumb_increment {
int number;
};
// 虚拟主机级数据
struct vhd__dumb_increment {
const unsigned int *options;
};
这些结构在不同范围内存储协议特定的数据。
来源:protocol_dumb_increment.c#L33-L40
2. 实现协议回调
协议回调函数处理各种WebSocket事件:
c
static int
callback_dumb_increment(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
// 将用户指针转换为你的会话级结构
struct pss__dumb_increment *pss = (struct pss__dumb_increment *)user;
// 基于reason的各种事件处理...
switch (reason) {
case LWS_CALLBACK_PROTOCOL_INIT:
// 初始化你的协议
break;
case LWS_CALLBACK_ESTABLISHED:
// 处理新连接
break;
// 其他事件处理程序...
}
return 0;
}
回调接收如连接建立、数据接收和连接关闭等事件。
来源:protocol_dumb_increment.c#L42-L91
3. 定义协议结构
创建一个lws_protocols结构来定义你的协议:
c
#define LWS_PLUGIN_PROTOCOL_DUMB_INCREMENT \
{ \
"dumb-increment-protocol", /* 协议名称 */ \
callback_dumb_increment, /* 回调函数 */ \
sizeof(struct pss__dumb_increment), /* 会话级数据大小 */ \
10, /* RX缓冲区大小 */ \
0, NULL, 0 /* ID,用户数据,TX数据包大小 */ \
}
来源:protocol_dumb_increment.c#L93-L100
4. 导出协议插件
最后,将你的协议作为插件导出:
c
LWS_VISIBLE const struct lws_protocols dumb_increment_protocols[] = {
LWS_PLUGIN_PROTOCOL_DUMB_INCREMENT
};
LWS_VISIBLE const lws_plugin_protocol_t dumb_increment = {
.hdr = {
"dumb increment", /* 插件名称 */
"lws_protocol_plugin", /* 插件类 */
LWS_BUILD_HASH, /* 构建哈希 */
LWS_PLUGIN_API_MAGIC /* API魔术数用于兼容性检查 */
},
.protocols = dumb_increment_protocols,
.count_protocols = LWS_ARRAY_SIZE(dumb_increment_protocols),
.extensions = NULL,
.count_extensions = 0,
};
这将导出插件,以便libwebsockets可以发现并加载它。
来源:protocol_dumb_increment.c#L104-L118
插件生命周期
插件加载流程:
正常运行... → lws_plugins_init()
↓
搜索插件目录 → 加载插件库
↓
检查API魔术数以确认兼容性 → 返回插件结构
↓
调用初始化回调 → 返回插件列表
↓
lws_plugins_destroy() → 调用销毁回调
↓
卸载插件库 → 清理完成
来源:lws-protocols-plugins.h#L300-L324
加载和使用插件
在运行时加载插件
要动态加载插件,使用lws_plugins_init()函数:
c
int lws_plugins_init(struct lws_plugin **pplugin, const char * const *dirs,
const char *_class, const char *filter,
each_plugin_cb_t each_cb, void *each_user);
参数:
pplugin:指向你的插件列表头的指针dirs:要搜索插件的目录路径数组_class:用于过滤的插件类(例如,"lws_protocol_plugin")filter:可选的名称过滤器each_cb:每个加载插件的可选回调each_user:回调的用户数据
来源:lws-protocols-plugins.h#L300-L311
卸载插件
当你完成使用插件时,使用以下函数卸载它们:
c
int lws_plugins_destroy(struct lws_plugin **pplugin, each_plugin_cb_t each_cb,
void *each_user);
来源:lws-protocols-plugins.h#L325-L333
构建插件
Libwebsockets支持两种构建插件的方法:
- 共享库 (
LWS_WITH_PLUGINS):将插件构建为可以动态加载的独立动态库 - 内置插件 (
LWS_WITH_PLUGINS_BUILTIN):将插件直接编译到主libwebsockets库中
选择取决于你的部署需求:
| 方法 | 优点 | 缺点 |
|---|---|---|
| 共享库 | - 动态加载 - 可以在不重新编译的情况下更新插件 - 减小可执行文件大小 | - 需要动态加载器支持 - 部署更复杂 |
| 内置 | - 部署更简单 - 无动态加载问题 - 保证插件可用 | - 可执行文件较大 - 更新插件需要重新编译 |
来源:plugins/CMakeLists.txt#L29-L57
CMake集成
插件构建系统与CMake集成。对于协议插件,一个宏简化了插件的创建:
cmake
create_plugin(protocol_name include_path
"main_source.c" "optional_source2.c" "optional_source3.c")
这个宏处理了构建插件作为共享库或内置组件的所有细节。
来源:plugins/CMakeLists.txt#L31-L83
提示 :在实现你自己的协议插件时,从复制一个现有的简单插件如
protocol_dumb_increment.c开始。它提供了一个良好的起点,并遵循所有必要的模式,以便与libwebsockets正确集成。
创建自定义插件类型
虽然协议插件是最常见的用例,但你可以通过以下步骤创建自己的插件类型:
- 定义一个以
lws_plugin_header_t开头的插件结构 - 为你的插件类型选择一个唯一的类标识符
- 实现插件的加载/卸载逻辑
例如:
c
// 定义你的自定义插件结构
typedef struct my_custom_plugin {
lws_plugin_header_t hdr; // 必须是第一个
// 你的自定义插件数据
void (*my_function)(void *user_data);
// 其他成员...
} my_custom_plugin_t;
// 从你的插件导出
LWS_VISIBLE const my_custom_plugin_t my_plugin = {
.hdr = {
"My Plugin",
"my_custom_plugin_class", // 唯一的类标识符
LWS_BUILD_HASH,
LWS_PLUGIN_API_MAGIC
},
// 初始化你的自定义成员
.my_function = my_plugin_function,
// 其他初始化...
};
然后使用标准的插件API和你的自定义类标识符加载这些插件。
来源:README.lws_plugins.md#L33-L49
结论
libwebsockets插件系统通过模块化组件提供了一种强大的方式来扩展库的功能。通过将协议实现分离到插件中,你可以创建更易于维护和灵活的应用程序,而无需修改核心代码。
无论你是在实现WebSocket协议、HTTP处理程序还是完全自定义的功能,插件系统都提供了一种一致且可靠的方法来实现模块化设计。
15. MQTT支持
来源: https://zread.ai/warmcat/libwebsockets/15-mqtt-support
MQTT(消息队列遥测传输)是一种轻量级的消息协议,专为受限设备和低带宽、高延迟的网络设计。Libwebsockets库提供了内置的MQTT客户端操作支持,使您的应用程序能够使用与库中其他部分相同的基于事件的架构高效地与MQTT代理通信。
Libwebsockets中的MQTT特性
Libwebsockets实现了MQTT 3.1.1客户端功能,具有以下特性:
| 特性 | 支持 |
|---|---|
| QoS等级 | 0(最多一次),1(至少一次) |
| 连接安全 | TLS/SSL支持 |
| 认证 | 用户名/密码 |
| 遗嘱消息 | 是 |
| 出生消息 | 是 |
| 消息保留 | 是 |
| AWS IoT集成 | 是 |
| 数据包大小 | 可配置 |
注意:当前实现不支持QoS等级2(精确一次)。
来源:lws-mqtt.h#L68-L74,lws-mqtt.h#L35-L42
开始使用MQTT
基本连接设置
要建立MQTT客户端连接,您需要:
- 使用
lws_mqtt_client_connect_param_t定义连接参数 - 使用
lws_client_connect_via_info创建连接 - 处理连接事件的回调
以下是设置基本连接参数的方法:
c
static const lws_mqtt_client_connect_param_t client_connect_param = {
.client_id = "lwsMqttClient", // 唯一的客户端标识符
.keep_alive = 60, // 保活间隔(秒)
.clean_start = 1, // 以干净会话开始
.will_param = { // 可选的遗嘱消息
.topic = "good/bye",
.message = "sign-off",
.qos = 0,
.retain = 0,
},
.username = "myUsername",
.password = "myPassword", // 可选的凭据
};
来源:minimal-mqtt-client.c#L40-L55
建立连接
要创建实际连接,请在您的连接信息中包含MQTT参数:
c
struct lws_client_connect_info i;
memset(&i, 0, sizeof i);
i.mqtt_cp = &client_connect_param; // 指向您的MQTT参数
i.address = "broker.example.com"; // MQTT代理地址
i.host = "broker.example.com";
i.protocol = "mqtt";
i.port = 1883; // 标准MQTT端口
i.method = "MQTT";
对于安全的MQTT连接,添加SSL选项并使用端口8883:
c
i.ssl_connection = LCCSCF_USE_SSL;
i.port = 8883; // 标准安全MQTT端口
来源:minimal-mqtt-client.c#L107-L127
MQTT消息流
Libwebsockets中的MQTT通信遵循以下一般顺序:
- 建立连接(CONNECT)
- 接收连接确认(CONNACK)
- 订阅主题(SUBSCRIBE)
- 接收订阅确认(SUBACK)
- 发布/接收消息(PUBLISH)
- 处理QoS确认(PUBACK,用于QoS 1)
来源:minimal-mqtt-client.c#L162-L296
使用MQTT操作
发布消息
要发布消息,使用lws_mqtt_client_send_publish()函数:
c
lws_mqtt_publish_param_t pub_param;
pub_param.topic = "my/topic";
pub_param.topic_len = strlen(pub_param.topic);
pub_param.qos = QOS0; // 或QOS1
const char *message = "Hello MQTT World!";
uint32_t msg_len = strlen(message);
lws_mqtt_client_send_publish(wsi, &pub_param, message, msg_len, 1);
对于大消息,您可以分块发送,将最后一个参数设置为0,直到最后一个块:
c
// 发送第一个块(300字节)
lws_mqtt_client_send_publish(wsi, &pub_param, buffer, 300, 0);
// 发送最后一个块
lws_mqtt_client_send_publish(wsi, &pub_param,
buffer + 300, remaining_len, 1);
来源:minimal-mqtt-client.c#L211-L239,lws-mqtt.h#L316-L345
订阅主题
要订阅主题,首先定义您的主题,然后调用lws_mqtt_client_send_subcribe():
c
// 定义带QoS等级的主题
static lws_mqtt_topic_elem_t topics[] = {
[0] = { .name = "sensors/temperature", .qos = QOS0 },
[1] = { .name = "sensors/humidity", .qos = QOS1 },
};
// 创建订阅参数
static lws_mqtt_subscribe_param_t sub_param = {
.topic = &topics[0],
.num_topics = 2, // 数组中的主题数量
};
// 发送订阅请求
lws_mqtt_client_send_subcribe(wsi, &sub_param);
来源:minimal-mqtt-client.c#L59-L67,minimal-mqtt-client.c#L200-L209
处理接收消息
接收消息通过LWS_CALLBACK_MQTT_CLIENT_RX回调传递:
c
case LWS_CALLBACK_MQTT_CLIENT_RX:
lwsl_user("MQTT消息接收\n");
// 'in'包含发布参数
lws_mqtt_publish_param_t *pub = (lws_mqtt_publish_param_t *)in;
// 访问主题和负载
char topic[256];
strncpy(topic, pub->topic, pub->topic_len);
topic[pub->topic_len] = '\0';
// 处理消息负载
lwsl_user("主题: %s, 负载: %.*s\n",
topic, pub->payload_len, (char *)pub->payload);
return 0;
来源:minimal-mqtt-client.c#L281-L290
处理QoS 1确认
对于QoS 1消息,您需要使用LWS_CALLBACK_MQTT_ACK回调处理确认:
c
case LWS_CALLBACK_MQTT_ACK:
lwsl_user("MQTT确认接收\n");
// 消息已成功送达
// 您现在可以清理与消息相关的资源
return 0;
如果消息未在规定时间内得到确认,您将收到重发回调:
c
case LWS_CALLBACK_MQTT_RESEND:
lwsl_user("MQTT重发请求\n");
// 重置您的消息状态并准备重发
return 0;
来源:minimal-mqtt-client.c#L247-L279
高级特性
AWS IoT集成
Libwebsockets内置支持AWS IoT,具有特殊的主题格式用于设备影子:
c
// 在连接参数中设置AWS IoT标志
client_connect_param.aws_iot = 1;
// 使用AWS IoT影子主题
char topic[256];
snprintf(topic, sizeof(topic),
LWS_MQTT_SHADOW_UNNAMED_SHADOW_TOPIC_FORMAT LWS_MQTT_SHADOW_UPDATE_STR,
"my-thing-name");
来源:lws-mqtt.h#L45-L66
连接重试和Ping管理
Libwebsockets可以自动管理连接重试和MQTT ping/pong,使用重试策略:
c
static const lws_retry_bo_t retry = {
.secs_since_valid_ping = 20, // 闲置20秒后发送PINGREQ
.secs_since_valid_hangup = 25, // 闲置25秒后挂断
};
// 添加到上下文创建信息
info.retry_and_idle_policy = &retry;
来源:minimal-mqtt-client.c#L35-L38,minimal-mqtt-client.c#L332
完整示例
以下是简化版的示例,展示了MQTT客户端的基本部分:
c
#include <libwebsockets.h>
// 连接参数
static const lws_mqtt_client_connect_param_t conn_params = {
.client_id = "my-client",
.keep_alive = 60,
.clean_start = 1
};
// 要订阅的主题
static lws_mqtt_topic_elem_t topics[] = {
{ .name = "test/topic", .qos = QOS0 }
};
static lws_mqtt_subscribe_param_t sub_param = {
.topic = &topics[0],
.num_topics = 1
};
// 回调函数
static int
mqtt_callback(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
switch (reason) {
case LWS_CALLBACK_MQTT_CLIENT_ESTABLISHED:
// 连接建立,订阅主题
lws_mqtt_client_send_subcribe(wsi, &sub_param);
break;
case LWS_CALLBACK_MQTT_SUBSCRIBED:
// 订阅确认,现在发布
lws_callback_on_writable(wsi);
break;
case LWS_CALLBACK_MQTT_CLIENT_WRITEABLE:
// 准备发布
lws_mqtt_publish_param_t pub = {
.topic = "test/topic",
.topic_len = 10,
.qos = QOS0
};
const char *message = "Hello MQTT!";
lws_mqtt_client_send_publish(wsi, &pub, message,
strlen(message), 1);
break;
case LWS_CALLBACK_MQTT_CLIENT_RX:
// 消息接收
lws_mqtt_publish_param_t *pub = (lws_mqtt_publish_param_t *)in;
printf("消息接收于主题: %.*s\n",
pub->topic_len, pub->topic);
break;
}
return 0;
}
// 协议定义
static const struct lws_protocols protocols[] = {
{
.name = "mqtt",
.callback = mqtt_callback,
.per_session_data_size = 0
},
LWS_PROTOCOL_LIST_TERM
};
Libwebsockets中的MQTT操作是非阻塞和事件驱动的。始终正确处理回调,并使用状态机模式管理MQTT操作的顺序(连接 → 订阅 → 发布 → 接收)。
限制和注意事项
- QoS等级支持:仅支持QoS 0和QoS 1;未实现QoS 2。
- 大消息处理:对于大负载,分块发送以避免缓冲区问题。
- 连接管理:库根据您的重试策略自动处理ping/pong。
- 线程安全:与其他libwebsockets API一样,MQTT函数不是线程安全的。始终从运行事件循环的线程中调用它们。
通过利用libwebsockets的MQTT支持,您可以轻松将物联网消息功能集成到您的应用程序中,同时保持与库中其他部分相同的基于事件的编程模型。
20. JSON解析(lejp)
来源: https://zread.ai/warmcat/libwebsockets/20-json-parsing-lejp
LEJP 简介
轻量级嵌入式 JSON 解析器(LEJP)是一款高性能的流式 JSON 解析器,专为资源受限环境设计。与传统将整个文档加载到内存中的 JSON 解析器不同,LEJP 以增量方式处理数据块,使其成为嵌入式系统、网络应用以及内存效率至关重要的场景的理想选择。
使 LEJP 突出的关键特性:
- 基于流的解析 - 以任意大小的块处理 JSON 数据
- 零堆分配 - 仅使用约 500 字节的栈内存
- 非递归设计 - 固定、可预测的栈使用
- 基于路径的匹配 - 轻松定位特定 JSON 元素
- 处理巨大负载 - 处理大于可用内存的 JSON 字符串
来源:lws-lejp.h#L25-L35, README.json-lejp.md#L10-L26
LEJP 入门
基本解析流程
使用 LEJP 的基本流程包括三个主要步骤:
- 初始化 解析器上下文,使用
lejp_construct() - 馈送 JSON 数据 到解析器,使用
lejp_parse() - 处理回调 触发的事件
- 清理 使用
lejp_destruct()
以下是一个展示如何设置和使用 LEJP 的最小示例:
c
#include <libwebsockets.h>
// 定义回调函数
static signed char
my_callback(struct lejp_ctx *ctx, char reason)
{
switch (reason) {
case LEJPCB_VAL_STR_END:
printf("字符串值: %s\n", ctx->buf);
break;
case LEJPCB_VAL_NUM_INT:
printf("整数: %s\n", ctx->buf);
break;
// 根据需要处理其他事件
}
return 0;
}
// 使用示例
void parse_json(const char *json_data, size_t len)
{
struct lejp_ctx ctx;
// 初始化解析器
lejp_construct(&ctx, my_callback, NULL, NULL, 0);
// 解析 JSON 数据
int result = lejp_parse(&ctx, (uint8_t *)json_data, len);
// 检查结果
if (result < 0 && result != LEJP_CONTINUE)
printf("解析失败: %s\n", lejp_error_to_string(result));
// 清理
lejp_destruct(&ctx);
}
来源:test-lejp.c#L88-L119, lejp.c#L68-L113
理解回调系统
LEJP 的回调系统是其事件驱动架构的核心。您的应用程序提供一个回调函数,该函数接收有关各种解析事件的通知。这种方法允许您在遇到 JSON 元素时做出反应,而无需维护复杂的状态。
回调原因
回调函数会根据不同的原因代码被调用,这些代码指示刚刚解析了什么:
| 回调原因 | 描述 | 关联数据 |
|---|---|---|
| LEJPCB_CONSTRUCTED | 解析器上下文创建 | 无 |
| LEJPCB_DESTRUCTED | 解析器上下文正在销毁 | 无 |
| LEJPCB_COMPLETE | 解析成功完成 | 无 |
| LEJPCB_FAILED | 解析失败 | 无 |
| LEJPCB_VAL_TRUE | 遇到布尔值 true | 无 |
| LEJPCB_VAL_FALSE | 遇到布尔值 false | 无 |
| LEJPCB_VAL_NULL | 遇到 null 值 | 无 |
| LEJPCB_PAIR_NAME | 键值对中的名称部分 | ctx->buf |
| LEJPCB_VAL_STR_START | 字符串值开始 | 无 |
| LEJPCB_VAL_STR_CHUNK | 字符串值的块 | ctx->buf(长度在 ctx->npos 中) |
| LEJPCB_VAL_STR_END | 字符串值结束 | ctx->buf(长度在 ctx->npos 中) |
| LEJPCB_VAL_NUM_INT | 整数值 | ctx->buf(作为文本) |
| LEJPCB_VAL_NUM_FLOAT | 浮点数值 | ctx->buf(作为文本) |
| LEJPCB_ARRAY_START | 数组开始 [ | 无 |
| LEJPCB_ARRAY_END | 数组结束 ] | 无 |
| LEJPCB_OBJECT_START | 对象开始 { | 无 |
| LEJPCB_OBJECT_END | 对象结束 } | 无 |
来源:lws-lejp.h#L87-L113, README.json-lejp.md#L52-L73
实现您的回调
一个典型的回调实现如下:
c
static signed char
json_callback(struct lejp_ctx *ctx, char reason)
{
// 检查值回调(具有 LEJP_FLAG_CB_IS_VALUE 标志)
if (reason & LEJP_FLAG_CB_IS_VALUE) {
switch (reason) {
case LEJPCB_VAL_STR_END:
printf("路径 '%s' 上的字符串值: %s\n", ctx->path, ctx->buf);
break;
case LEJPCB_VAL_NUM_INT:
printf("路径 '%s' 上的整数: %s\n", ctx->path, ctx->buf);
break;
case LEJPCB_VAL_TRUE:
printf("路径 '%s' 上的布尔值 TRUE\n", ctx->path);
break;
// 处理其他值类型
}
return 0;
}
// 处理结构化回调
switch (reason) {
case LEJPCB_OBJECT_START:
printf("路径 '%s' 上的对象开始\n", ctx->path);
break;
case LEJPCB_PAIR_NAME:
printf("属性名称: '%s'\n", ctx->path);
break;
case LEJPCB_COMPLETE:
printf("解析完成\n");
break;
// 处理其他事件
}
return 0; // 返回 0 以继续解析,或返回非零以中止
}
来源:test-lejp.c#L44-L86
基于路径的 JSON 导航
LEJP 维护一个当前的"路径"字符串,表示您在 JSON 结构中的位置。这一强大功能允许您轻松跟踪在嵌套 JSON 对象中的位置,并匹配您感兴趣的特定路径。
路径表示
路径是按照以下约定构建的:
- 对象属性用点分隔:
user.name - 数组元素用方括号表示:
users[] - 在数组内部时,索引会被跟踪:
users[0].name
来源:README.json-lejp.md#L90-L104
路径匹配
LEJP 最强大的功能之一是在构造解析器时指定您感兴趣的路径。这允许您高效地提取特定值,而无需在回调中编写复杂的条件逻辑:
c
// 定义我们感兴趣的路径
static const char * const json_paths[] = {
"user.name", // 匹配 user 对象中的 name 属性
"user.email", // 匹配 user 对象中的 email 属性
"settings.theme.*", // 匹配 settings.theme 下的任何属性
"items[].id" // 匹配 items 数组中任何元素的 id 属性
};
// 使用路径初始化解析器
lejp_construct(&ctx, my_callback, user_data, json_paths, LWS_ARRAY_SIZE(json_paths));
当在解析过程中匹配到这些路径之一时,ctx->path_match 字段包含您数组中匹配路径的索引(从 1 开始)。您可以在回调中使用这个索引来对不同路径进行不同处理:
c
static signed char
my_callback(struct lejp_ctx *ctx, char reason)
{
// 处理匹配路径的值事件
if ((reason & LEJP_FLAG_CB_IS_VALUE) && ctx->path_match) {
switch (ctx->path_match) {
case 1: // "user.name" 匹配
printf("用户名: %s\n", ctx->buf);
break;
case 2: // "user.email" 匹配
printf("邮箱: %s\n", ctx->buf);
break;
// 处理其他匹配
}
}
return 0;
}
来源:lejp.c#L147-L196
处理大型 JSON 字符串
LEJP 设计用于处理大于其内部缓冲区的字符串。当一个字符串超过缓冲区大小(默认 254 字节)时,它会分块传递:
- 首先,您会收到一个
LEJPCB_VAL_STR_START回调,指示一个字符串已开始 - 如果字符串大于缓冲区,您会收到一个或多个
LEJPCB_VAL_STR_CHUNK回调 - 最后,您会收到一个带有最后一个块的
LEJPCB_VAL_STR_END回调
以下是处理分块字符串的方法:
c
static char full_string[10240]; // 用于组装字符串的大缓冲区
static int string_pos = 0;
static signed char
my_callback(struct lejp_ctx *ctx, char reason)
{
switch (reason) {
case LEJPCB_VAL_STR_START:
// 当字符串开始时重置我们的位置
string_pos = 0;
memset(full_string, 0, sizeof(full_string));
break;
case LEJPCB_VAL_STR_CHUNK:
case LEJPCB_VAL_STR_END:
// 将此块追加到我们的缓冲区
if (string_pos + ctx->npos < sizeof(full_string)) {
memcpy(full_string + string_pos, ctx->buf, ctx->npos);
string_pos += ctx->npos;
// 如果这是结束,我们现在可以使用完整的字符串
if (reason == LEJPCB_VAL_STR_END) {
printf("完整字符串: %s\n", full_string);
}
}
break;
}
return 0;
}
来源:README.json-lejp.md#L76-L88
高级特性
处理嵌套数组和对象
LEJP 自动跟踪数组的嵌套级别和索引。ctx->ipos 字段指示您在数组嵌套中的深度,ctx->i[] 数组包含每个级别的索引计数器。
c
static signed char
my_callback(struct lejp_ctx *ctx, char reason)
{
if (reason == LEJPCB_ARRAY_START) {
printf("在嵌套级别 %d 开始数组\n", ctx->ipos);
}
else if (reason == LEJPCB_VAL_STR_END && ctx->ipos > 0) {
printf("级别 %d 的数组元素 %d: %s\n",
ctx->i[ctx->ipos-1], ctx->ipos, ctx->buf);
}
return 0;
}
默认情况下,LEJP 仅跟踪数组索引,但您可以通过在构造后设置 ctx->flags |= LEJP_FLAG_FEAT_OBJECT_INDEXES 来启用对象索引跟踪。
来源:README.json-lejp.md#L105-L117
动态切换回调
LEJP 允许您在解析过程中使用 lejp_change_callback() 更改回调函数。这在根据已解析内容处理不同解析模式时非常有用:
c
// 初始回调,查找 schema 类型
static signed char
initial_callback(struct lejp_ctx *ctx, char reason)
{
if (reason == LEJPCB_PAIR_NAME && !strcmp(ctx->path, "schema")) {
// 我们找到了 schema 属性,准备切换回调
}
else if (reason == LEJPCB_VAL_STR_END && !strcmp(ctx->path, "schema")) {
// 根据 schema 值切换到专用回调
if (!strcmp(ctx->buf, "user")) {
lejp_change_callback(ctx, user_schema_callback);
} else if (!strcmp(ctx->buf, "product")) {
lejp_change_callback(ctx, product_schema_callback);
}
}
return 0;
}
来源:lejp.c#L137-L145
性能和资源使用
LEJP 专为资源受限的环境设计:
- 内存使用:约 500 字节的上下文结构
- 栈使用:固定、非递归设计,具有可预测的栈需求
- 解析效率:单次解析,开销最小
在嵌入式系统中使用 LEJP 时,您可以通过在包含库之前定义这些宏来自定义缓冲区大小:
c
#define LEJP_MAX_DEPTH 10 // 最大嵌套深度(默认 16)
#define LEJP_MAX_PATH 128 // 最大路径长度(默认 192)
#define LEJP_STRING_CHUNK 128 // 字符串缓冲区大小(默认 254)
#include <libwebsockets.h>
来源:lws-lejp.h#L180-L195
错误处理
LEJP 在解析失败时提供详细的错误代码:
c
int result = lejp_parse(&ctx, json_data, len);
if (result < 0 && result != LEJP_CONTINUE) {
printf("JSON 解析失败: %s\n", lejp_error_to_string(result));
}
常见错误代码包括:
LEJP_REJECT_CALLBACK:您的回调返回了非零值LEJP_REJECT_UNKNOWN:一般解析错误LEJP_REJECT_MP_NO_OPEN_QUOTE:在名称中预期引号LEJP_REJECT_MP_STRING_UNDERRUN:字符串意外结束
来源:lejp.c#L29-L54, lws-lejp.h#L59-L82
结论
LEJP 提供了一种轻量级、高效的 JSON 解析方法,非常适合嵌入式系统、网络应用以及任何内存效率至关重要的场景。其流式设计使其能够处理任意大小的 JSON 数据,同时保持较小的内存占用。
通过理解其基于回调的方法和路径匹配功能,您可以从 JSON 文档中高效提取所需信息,而无需复杂的解析逻辑。无论您是在开发 IoT 设备、网络协议,还是只是需要一个快速的 JSON 解析器,LEJP 都提供了一个健壮的解决方案,可从最小的嵌入式系统扩展到高性能应用。
19. JWT实现
来源: https://zread.ai/warmcat/libwebsockets/19-jwt-implementation
JSON Web Tokens (JWT) 是一种紧凑且 URL 安全的方式来表示在两个 parties 之间传递的声明。如果您正在构建需要身份验证和授权功能的安全应用程序,libwebsockets 提供了一个健壮的 JWT 实现,其中内置了安全最佳实践。本指南将指导您如何使用 libwebsockets 中的 JWT。
libwebsockets 中的 JWT 支持简介
Libwebsockets 通过其 JOSE(JavaScript 对象签名和加密)模块提供全面的 JWT 支持。该实现专注于安全最佳实践,同时为常见的 JWT 使用场景提供了灵活性,包括:
- 使用各种签名算法生成 JWT
- JWT 验证和解析
- 通过 HTTP cookies 传输 JWT,并进行安全加固
JWT 功能经过精心设计,旨在防范常见的 JWT 实现安全漏洞。
开始使用 JWT
构建要求
要在您的 libwebsockets 项目中使用 JWT 功能,您需要在构建过程中启用 JOSE 模块:
cmake
-DLWS_WITH_JOSE=1 -DLWS_WITH_GENCRYPTO=1
这些选项启用了 JWT/JOSE 组件以及签名和验证操作所需的通用加密接口。
libwebsockets 中的 JWT 结构
Libwebsockets 的 JWT 实现使用标准 JWT 字段和一些自定义字段以增强安全性:
| 字段 | 标准 | 描述 |
|---|---|---|
| iss | 是 | 发行者,通常是您的域名(例如,"example.com") |
| aud | 是 | 受众,通常是一个 URL 路径(例如,"https://example.com/api") |
| iat | 是 | 发行令牌时的 Unix 时间戳 |
| nbf | 是 | 令牌在此时间戳之前不应被接受 |
| exp | 是 | 令牌在此时间戳之后过期 |
| sub | 是 | 主体,通常是用户名或用户标识符 |
| csrf | 否 | 用于 CSRF 保护的一个随机 16 字符十六进制令牌 |
| ext | 否 | 用于自定义数据的特定于应用的 JSON 对象 |
核心 JWT API
库提供了从低级原语到高级助手的多个层次的 API。以下是处理 JWT 的最重要的函数:
JWT 验证
c
int
lws_jwt_signed_validate(struct lws_context *ctx, struct lws_jwk *jwk,
const char *alg_list, const char *com, size_t len,
char *temp, int tl, char *out, size_t *out_len);
此函数针对加密密钥验证 JWT。它确保签名有效且使用的算法与 alg_list 中指定的算法之一匹配。如果验证成功,JWT 负载将被复制到 out 缓冲区。
JWT 创建
c
int
lws_jwt_sign_compact(struct lws_context *ctx, struct lws_jwk *jwk,
const char *alg, char *out, size_t *out_len, char *temp,
int tl, const char *format, ...);
此函数一步创建 JWT,从包含 JSON 负载的格式字符串到正确格式的紧凑 JWT。它处理使用提供的密钥和算法对 JWT 进行签名。
JWT 完整性检查
c
int
lws_jwt_token_sanity(const char *in, size_t in_len,
const char *iss, const char *aud, const char *csrf_in,
char *sub, size_t sub_len, unsigned long *exp_unix_time);
此函数对已验证的 JWT 负载执行重要的安全检查:
- 验证发行者是否符合预期值
- 验证受众是否正确
- 检查令牌的有效时间窗口(nbf 和 exp 声明)
- 如果提供,验证 CSRF 令牌
- 提取主体供应用使用
来源:lws-jws.h#L407-L529
在 HTTP 应用中使用 JWT
对于 Web 应用,libwebsockets 提供了方便的函数来通过 HTTP cookies 处理 JWT,并进行安全加固:
c
int
lws_jwt_sign_token_set_http_cookie(struct lws *wsi,
const struct lws_jwt_sign_set_cookie *i,
uint8_t **p, uint8_t *end);
int
lws_jwt_get_http_cookie_validate_jwt(struct lws *wsi,
struct lws_jwt_sign_set_cookie *i,
char *out, size_t *out_len);
这些函数简化了通过 HTTP cookies 创建和验证 JWT 的过程,应用最佳实践安全限制,如:
- 强制 HttpOnly 标志以防止 JavaScript 访问 cookies
- 使用
__Host-前缀以增强安全性 - 设置适当的 cookie 路径和安全属性
来源:lws-jws.h#L533-L598
实现示例
以下是如何创建和设置 JWT cookie 的简化示例:
c
struct lws_jwt_sign_set_cookie cookie_info = {
.jwk = my_signing_key, // 您的 JWK 签名密钥
.alg = "ES512", // 签名算法(例如,ES512)
.iss = "example.com", // 令牌发行者
.aud = "https://example.com/api", // 令牌受众
.cookie_name = "session", // Cookie 名称(将自动添加 __Host- 前缀)
.sub = "user123", // 主体(例如,用户名)
.extra_json = "\"role\":\"admin\"", // 可选的自定义声明
.expiry_unix_time = 1800 // 令牌生命周期(秒)(30 分钟)
};
// 在 HTTP 响应处理程序中:
uint8_t *p = buffer;
uint8_t *end = buffer + buffer_size;
int ret = lws_jwt_sign_token_set_http_cookie(wsi, &cookie_info, &p, end);
if (ret != 0) {
// 处理错误
}
要验证来自传入 cookie 的 JWT:
c
struct lws_jwt_sign_set_cookie cookie_info = {
.jwk = my_verification_key, // 您的 JWK 验证密钥
.iss = "example.com", // 预期发行者
.aud = "https://example.com/api", // 预期受众
.cookie_name = "session", // 要检查的 Cookie 名称
.csrf_in = provided_csrf_token // 请求中的 CSRF 令牌(如果使用 CSRF 保护)
};
char jwt_payload[2048];
size_t payload_len = sizeof(jwt_payload);
int ret = lws_jwt_get_http_cookie_validate_jwt(wsi, &cookie_info, jwt_payload, &payload_len);
if (ret == 0) {
// JWT 有效,cookie_info.sub 现在包含主体
// cookie_info.extra_json 包含任何自定义声明
// cookie_info.expiry_unix_time 包含过期时间
} else {
// 无效的 JWT
}
安全最佳实践
Libwebsockets 的 JWT 实现鼓励以下安全最佳实践:
1. 短期令牌
保持 JWT 有效期短,通常为 10-20 分钟。这减少了令牌在泄露后的滥用窗口。
使用短过期时间并实现客户端刷新机制,而不是使用长期令牌。这样,即使令牌被泄露,暴露窗口也会最小化。
2. CSRF 保护
通过在每个 JWT 中嵌入随机令牌来缓解跨站请求伪造(CSRF)攻击。当使用 JWT 生成页面时,此令牌包含在需要身份验证的链接或表单中。当用户提交表单或点击链接时,CSRF 令牌必须与他们的 JWT 中的令牌匹配。
3. 处理过期
由于 JWT 的生命周期较短,应用程序需要优雅地处理过期。客户端应跟踪 JWT 过期时间,并在过期前刷新页面或请求新令牌。
对于像 WebSockets 这样的长连接,服务器应验证当前时间是否仍在令牌的过期时间之前,对于每个敏感操作,并在其身份验证过期时考虑关闭连接。
结论
Libwebsockets 提供了一个全面且注重安全的 JWT 实现。通过遵循本指南中概述的 API 模式和安全建议,您可以在应用程序中添加健壮的身份验证和授权功能,同时避免常见的安全陷阱。
该实现很好地平衡了便利性和安全性,提供了自动处理安全最佳实践的高级 API,同时仍然提供低级 API 以供需要时进行自定义。
请记住,即使有了最好的 JWT 实现,安全也是一个整体关注的问题。始终使用 HTTPS,实现适当的密码存储,并考虑额外的安全措施,如速率限制和监控。
18. 内容安全策略
来源: https://zread.ai/warmcat/libwebsockets/18-content-security-policy
内容安全策略(Content Security Policy,CSP)是现代浏览器中实现的一种强大安全功能,它为Web应用程序提供了一层保护------本质上类似于"网页的selinux"。当服务器发送描述内容安全策略的特定头信息时,浏览器会严格执行这些规则,显著降低各种攻击的风险,尤其是跨站脚本(XSS)攻击。
在libwebsockets中,内容安全策略作为库的综合安全功能的一部分得到实现,允许开发者轻松地将这些保护措施应用于他们的Web应用程序。
资料来源:README.content-security-policy.md
为什么要使用内容安全策略?
现代Web应用程序经常从各种来源整合脚本,包括第三方提供商。这可能导致安全漏洞,攻击者可能会将恶意脚本注入文档对象模型(DOM)。CSP通过允许源服务器定义其提供的页面中合法的内容来解决此问题,其他所有内容都将由浏览器拒绝。
实际结果是,当配置正确时:
- 您的网站内容不会被他人iframe化
- 内联样式和脚本将被拒绝
- 字体、CSS、AJAX、WebSockets和图像等资源只能从特定来源加载
- 常见的XSS方法被中和,因为攻击者无法执行注入的脚本或从其他服务器加载JavaScript
资料来源:README.content-security-policy.md#L5-L38
libwebsockets中的CSP实现
libwebsockets通过其HTTP头处理系统提供内置支持以实现内容安全策略。该库附带了一组安全最佳实践头信息,可以通过单个选项启用。
默认实现
当您为vhost启用LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE选项时,libwebsockets会自动将以下安全头信息添加到所有HTTP响应中:
"content-security-policy": "default-src 'none'; img-src 'self' data: ; script-src 'self'; font-src 'self'; style-src 'self'; connect-src 'self' ws: wss:; frame-ancestors 'none'; base-uri 'none'; form-action 'self';""x-content-type-options": "nosniff""x-xss-protection": "1; mode=block""x-frame-options": "deny""referrer-policy": "no-referrer"
这是libwebsockets.org和warmcat.com网站(为了在文档中清晰起见,删除了一些图像源)使用的相同CSP配置。
资料来源:lib/roles/http/header.c#L289-L307
在您的应用程序中启用CSP
要启用libwebsockets应用程序中的内容安全策略和其他安全头信息,您需要:
在创建vhost时设置LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE选项:
c
struct lws_context_creation_info info;
memset(&info, 0, sizeof(info));
// ... 其他设置
info.options |= LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE;
vhost = lws_create_vhost(context, &info);
libwebsockets实现将自动将安全头信息应用于从该vhost提供的所有HTTP响应。
资料来源:lib/roles/http/header.c#L390-L402
对Web应用程序开发的启示
实施严格的内容安全策略需要调整您的Web应用程序开发实践。libwebsockets文档强调,"半心半意的内容安全策略没有什么价值"------您应该从限制性的策略(default-src 'none')开始,然后只允许绝对必要的内容。
Web应用程序所需的更改
当使用libwebsockets实施CSP时,您需要以下方式调整您的HTML、CSS和JavaScript:
1. 将内联样式移动到CSS文件
不允许在严格的内容安全策略中使用内联样式(例如,style="font-size:120%")。将所有样式移动到单独的CSS文件中:
html
<!-- 在CSP之前 -->
<div style="font-size:120%">大号文本</div>
<!-- 在CSP之后 -->
<div class="large-text">大号文本</div>
<!-- 在您的CSS文件中 -->
/* style.css */
.large-text {
font-size:120%;
}
2. 将内联脚本移动到外部文件
所有JavaScript都应该放置在外部.js文件中,并在页面的head部分加载:
html
<head>
<meta charset=utf-8 http-equiv="Content-Language" content="en"/>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script type='text/javascript' src="app.js"></script>
<title>我的安全应用程序</title>
</head>
3. 使用事件监听器代替事件处理程序
不要使用内联事件处理程序(例如,onclick),而使用JavaScript事件监听器:
html
<!-- 在CSP之前 -->
<button onclick="doSomething()">点击我</button>
<!-- 在CSP之后 -->
<button id="action-button">点击我</button>
并在您的JavaScript文件中:
javascript
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("action-button").addEventListener("click", function() {
doSomething();
}, false);
});
资料来源:README.content-security-policy.md#L40-L121
外部资源的最佳实践
在实施CSP时,请考虑外部资源的来源:
- 尽可能自托管:不要从CDN加载库,考虑自己托管它们以保持控制并减少隐私问题。
- 安全考虑:如果外部服务被破坏,可能会影响使用其资源的所有网站。自托管将您的暴露限制在自己的服务器。
- 性能影响:通过适当的缓存头信息,自托管资源并不一定会显著影响性能。
- 隐私优势:自托管可以防止第三方通过资源请求跟踪您的用户。
安全提示:使用严格的内容安全策略是防御XSS攻击最有效的防御措施之一。但是,它需要谨慎实施和测试。开始时,请启用"仅报告"模式的内容安全策略以识别问题,然后再强制执行策略。
资料来源:README.content-security-policy.md#L123-L148
CSP头信息实现细节
在底层,libwebsockets通过HTTP头处理代码中的pvo_hsbph结构实现安全头信息。当启用安全最佳实践选项时,库会遍历这些预定义的头信息并将它们添加到每个HTTP响应中。
实际的内容安全策略强制执行发生在浏览器中,而libwebsockets只是确保将适当的头信息包含在您的服务器发出的所有HTTP响应中。
资料来源:lib/roles/http/header.c#L390-L402
结论
通过libwebsockets实现内容安全策略为许多常见的Web漏洞提供了强大的防御,尤其是XSS攻击。虽然它需要调整您构建Web应用程序的方式,但安全收益是巨大的。
通过利用libwebsockets对安全最佳实践的内建支持,您可以轻松地将这些保护措施应用于您的应用程序,而无需手动管理复杂的头信息配置。库的方法鼓励一种以安全为重心的思维方式,从严格的策略开始,然后只允许绝对必要的外部例外。
对于更高级的用例,您还可以通过直接配置vhost头信息来实施自定义安全头信息,但内建的最佳实践为大多数应用程序提供了一个出色的起点。
17. ACME客户端(Let's Encrypt)
来源: https://zread.ai/warmcat/libwebsockets/17-acme-client-lets-encrypt
Libwebsockets 通过其 lws-acme-client 插件提供了自动证书管理环境(ACME)协议的强大实现。这使得您的 WebSockets 服务器可以自动获取、安装和续订 TLS 证书,无需手动干预,支持如 Let's Encrypt 等提供商。
什么是 ACME?
ACME 是一种支持自动证书颁发和域名验证的协议。最广为人知的实现是 Let's Encrypt,它通过该协议提供免费的 TLS 证书。
主要功能
- 零接触证书配置 - 自动获取证书,仿佛"从天而降"
- 自动续订 - 检测证书是否在两周内到期并自动续订
- 挑战支持 - 实现 tls-sni-01 和 http-01 挑战
- 多后端支持 - 兼容 OpenSSL 和 mbedTLS 后端
- 注重安全的设计 - 即使在降权后也支持仅限根目录的证书存储
集成要求
在使用 ACME 客户端之前,请确保您的设置满足以下要求:
- DNS 解析 - 您的域名必须解析到服务器的 IP 地址
- 端口访问 - 服务器必须在端口 443(用于 TLS-SNI 挑战)或端口 80(用于 HTTP 挑战)上可访问
- 文件系统访问 - 进程需要写入权限到证书存储目录(通常在启动时)
配置指南
对于 LWSWS(Libwebsockets Web 服务器)
将 ACME 客户端插件添加到您的虚拟主机配置中:
json
{
"vhosts": [{
"name": "example.com",
"port": "443",
"host-ssl-cert": "/etc/lwsws/acme/example.com.crt.pem",
"host-ssl-key": "/etc/lwsws/acme/example.com.key.pem",
"ignore-missing-cert": "1",
"ws-protocols": [{
"lws-acme-client": {
"auth-path": "/etc/lwsws/acme/auth.jwk",
"cert-path": "/etc/lwsws/acme/example.com.crt.pem",
"key-path": "/etc/lwsws/acme/example.com.key.pem",
"directory-url": "https://acme-v01.api.letsencrypt.org/directory",
"country": "US",
"state": "California",
"locality": "San Francisco",
"organization": "My Company Ltd",
"common-name": "example.com",
"email": "admin@example.com"
}
}]
}]
}
注意 使用
"ignore-missing-cert": "1",这是关键设置,允许服务器在证书尚不存在时启动。
对于 C API 集成
在创建虚拟主机时,设置 LWS_SERVER_OPTION_IGNORE_MISSING_CERT 选项并提供必要的 PVOs(每虚拟主机选项):
c
struct lws_context_creation_info info;
memset(&info, 0, sizeof(info));
/* 设置基本服务器选项 */
info.port = 443;
info.options |= LWS_SERVER_OPTION_IGNORE_MISSING_CERT;
// ... 其他选项
/* 设置 ACME 选项 */
const struct lws_protocol_vhost_options pvo_email = {
NULL, NULL, "email", "admin@example.com"
};
const struct lws_protocol_vhost_options pvo_common_name = {
&pvo_email, NULL, "common-name", "example.com"
};
const struct lws_protocol_vhost_options pvo_directory = {
&pvo_common_name, NULL, "directory-url",
"https://acme-v01.api.letsencrypt.org/directory"
};
const struct lws_protocol_vhost_options pvo_key_path = {
&pvo_directory, NULL, "key-path", "/etc/lwsws/acme/example.com.key.pem"
};
const struct lws_protocol_vhost_options pvo_cert_path = {
&pvo_key_path, NULL, "cert-path", "/etc/lwsws/acme/example.com.crt.pem"
};
const struct lws_protocol_vhost_options pvo_auth_path = {
&pvo_cert_path, NULL, "auth-path", "/etc/lwsws/acme/auth.jwk"
};
/* 设置 ACME 协议 */
struct lws_protocol_vhost_options pvo = {
NULL, NULL, "lws-acme-client", (void *)&pvo_auth_path
};
info.pvo = &pvo;
/* 创建上下文和虚拟主机 */
struct lws_context *context = lws_create_context(&info);
必需配置选项
| 选项 | 描述 |
|---|---|
| auth-path | ACME 客户端存储认证密钥的路径 |
| cert-path | 证书存储路径(应与 host-ssl-cert 匹配) |
| key-path | 私钥存储路径(应与 host-ssl-key 匹配) |
| directory-url | ACME 服务器目录的 URL |
| common-name | 您服务器的 DNS 名称(域名) |
| 证书的联系人电子邮件地址 |
可选配置选项
| 选项 | 描述 |
|---|---|
| country | 两字母国家代码 |
| state | 州或省名称 |
| locality | 城市 或地区名称 |
| organization | 组织或公司名称 |
ACME 目录 URLs
directory-url 选项指定使用哪个 ACME 服务器。Let's Encrypt 提供两个环境:
| 环境 | URL | 目的 |
|---|---|---|
| 测试 | https://acme-staging.api.letsencrypt.org/directory | 测试,更高的速率限制,不被浏览器信任 |
| 生产 | https://acme-v01.api.letsencrypt.org/directory | 真实证书,被浏览器信任 |
专业提示: always test your ACME setup with the staging environment first. Once everything is working correctly, switch to the production URL. This prevents hitting rate limits during development and testing.
Libwebsockets 中 ACME 的工作原理
Libwebsockets 中的 ACME 客户端插件实现了一个状态机,遵循 ACME 协议流程:
- 注册到 ACME 服务器
- 请求证书颁发
- 完成域名验证挑战(HTTP-01 或 TLS-SNI-01)
- 获取证书
- 安装证书
- 监控证书过期并自动续订
安全考虑
证书和密钥存储
该插件设计为即使在 libwebsockets 启动后降权也能安全工作:
- 在启动时(具有根权限),服务器打开证书和密钥路径的写入文件描述符
- 这些文件描述符在服务器降权后仍然保持打开
- 当证书需要续订时,ACME 客户端写入这些描述符
- 在下次启动时,服务器将更新后的文件复制到适当位置
这种方法允许在仅限根目录的安全存储证书,同时支持自动续订。
多虚拟主机使用同一证书
如果您有多个使用同一证书的虚拟主机,只需将 ACME 客户端插件附加到其中一个虚拟主机。当证书更新时,所有使用同一证书文件的虚拟主机将自动使用新证书。
故障排除
常见问题
证书未颁发
- 确保您的域名解析到您的服务器
- 检查端口 80/443 是否可从互联网访问
- 验证证书存储目录的权限
挑战失败
- 对于 HTTP-01 挑战:确保端口 80 可访问
- 对于 TLS-SNI 挑战:确保端口 443 可访问
- 检查防火墙设置
实现问题
- 在切换 OpenSSL 和 mbedTLS 后端时,删除认证密钥文件以便重新生成
结论
Libwebsockets 中的 ACME 客户端提供了自动 TLS 证书配置和续订的强大解决方案。通过实现此插件,您可以确保您的 WebSockets 服务器始终拥有有效、最新的 TLS 证书,而无需手动干预。
该实现遵循 IETF 标准,兼容 Let's Encrypt 和其他符合 ACME 标准的证书颁发机构,是生产环境中的可靠解决方案。
14. HTTP/2长轮询
来源: https://zread.ai/warmcat/libwebsockets/14-http-2-long-polling
libwebsockets中的HTTP/2长轮询提供了一种创建持久连接的机制,这些连接可以长时间保持开启状态而不会超时。这一特性对于需要服务器推送更新、实时通知或与客户端进行事件驱动通信的应用程序尤为重要。
什么是HTTP/2长轮询?
libwebsockets中的HTTP/2长轮询使得在特定条件下可以创建"永生"流,这些流不受正常超时限制。这些流允许服务器在任何时间向客户端推送数据,而无需客户端反复发送新请求。
HTTP/2长轮询的主要特点:
- 创建只读流(从客户端视角)
- 不受正常超时限制 - 连接可以无限期保持开启
- 在HTTP/2协议的流模型内工作
- 适用于服务器到客户端的推送通知
与传统的短生命周期HTTP连接不同,HTTP/2长轮询连接可以保持开启数小时甚至数天,非常适合用于仪表盘应用、实时监控或通知系统。
来源:README.h2-long-poll.md#L3-L5
工作原理
HTTP/2长轮询通过允许客户端流通过发送带有END_STREAM标志的零长度DATA帧来信号"半关闭"。这一特殊条件告诉服务器为服务器到客户端的通信无限期地维持该流。
包含至少一个永生流的网络连接在最后一个永生流关闭之前不会超时。这使得可以在不改变HTTP/2协议基本结构的情况下实现长生命周期连接。
由于这些连接无限期保持开启,建议实现额外机制以确认客户端仍然活跃。
来源:README.h2-long-poll.md#L7-L12
服务器端实现
在服务器上启用长轮询
要在服务器上启用HTTP/2长轮询,必须使用特定选项配置虚拟主机:
在C代码中:
在创建虚拟主机时,在info.options字段中设置LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL标志:
c
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL |
LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE;
在JSON配置中:
json
{
"h2-half-closed-long-poll": "1"
}
一旦启用,服务器将识别并维持客户端意图使用长轮询的永生流。
来源:README.h2-long-poll.md#L16-L24, minimal-http-server.c#L136-L139
服务器端行为
当启用HTTP/2长轮询时:
- 正常流按常规带有标准超时
- 客户端流可以通过发送带有
END_STREAM的零长度DATA帧信号转为永生模式 - 这些永生流存在于正常超时约束之外
- 服务器可以在这些流上随时向客户端发送数据
这种实现允许服务器维持连接并在需要时推送更新,而无需客户端定期重新连接。
来源:README.h2-long-poll.md#L26-L30
客户端实现
转换到长轮询模式
客户端需要显式将其建立的HTTP/2流转换为永生模式。libwebsockets为此提供了专门的API函数:
c
int lws_h2_client_stream_long_poll_rxonly(struct lws *wsi);
在HTTP/2连接建立后调用此函数,以将流转换为永生模式并向服务器发送END_STREAM信号。
来源:README.h2-long-poll.md#L32-L41, minimal-http-client.c#L105-L108
在客户端应用中启用长轮询
在最小客户端示例中,通过命令行参数启用长轮询:
c
if (lws_cmdline_option(a->argc, a->argv, "--long-poll")) {
lwsl_user("%s: 长轮询模式\n", __func__);
long_poll = 1;
}
启用后,客户端在连接建立后调用转换函数:
c
if (long_poll) {
lwsl_user("%s: 客户端进入长轮询模式\n", __func__);
lws_h2_client_stream_long_poll_rxonly(wsi);
}
来源:minimal-http-client.c#L241-L246, minimal-http-client.c#L105-L108
接收长轮询数据
使用长轮询时,客户端需要以不同方式处理传入数据。在示例应用中,客户端打印接收到的数据:
c
if (long_poll) {
char dotstar[128];
lws_strnncpy(dotstar, (const char *)in, len, sizeof(dotstar));
lwsl_notice("长轮询接收: %d '%s'\n", (int)len, dotstar);
}
来源:minimal-http-client.c#L148-L155
示例实现
libwebsockets包括演示HTTP/2长轮询的示例应用:
服务器示例
minimal-http-server-h2-long-poll示例演示了一个支持永生HTTP/2流的服务器。该服务器:
- 启用必要的HTTP/2长轮询选项
- 无限期保持连接开启
- 每分钟向客户端发送一次时间戳
服务器示例的关键组件:
c
// 设置计划回调,每分钟运行一次
lws_sul_schedule(lws_get_context(pss->wsi), 0, &pss->sul, sul_cb,
60 * LWS_US_PER_SEC);
这个计划回调标记连接有挂起数据,然后请求可写回调以将时间戳发送给客户端。
来源:minimal-http-server.c#L49-L50, minimal-http-server.c#L13-L17
客户端示例
常规的minimal-http-client可以使用-l --long-poll标志连接到服务器并转换为永生模式:
bash
./minimal-http-client -l --long-poll
客户端:
- 连接到服务器
- 转换为永生模式
- 接收并显示服务器发送的时间戳
这创建了一个稳定的连接,保持开启而不会超时,展示了HTTP/2长轮询的永生特性。
来源:README.h2-long-poll.md#L45-L54
最佳实践
在使用libwebsockets实现HTTP/2长轮询时,考虑以下最佳实践:
- 实现活动监控:由于连接无限期保持开启,实现某种方式以确认客户端仍然活跃
- 资源管理:注意维持许多长生命周期连接的资源影响
- 错误处理:计划网络中断,并在客户端实现重新连接逻辑
- 安全考虑:与任何长生命周期连接一样,确保适当的身份验证和授权
结论
libwebsockets中的HTTP/2长轮询提供了一种强大的机制,用于在服务器和客户端之间维持持久连接。通过利用HTTP/2协议的流模型,它实现了高效的服务器到客户端推送通信,而无需客户端反复重新连接。
这一特性对于需要实时更新、通知或持续数据流的应用程序尤为宝贵,同时保持了HTTP/2协议的优势。
9. 库结构概览
来源: https://zread.ai/warmcat/libwebsockets/9-library-structure-overview
libwebsockets库的结构经过精心设计,以支持其广泛的功能集,同时保持代码的可维护性和可扩展性。了解库的组织结构有助于理解其架构,并帮助您更有效地使用和扩展库。
目录结构
libwebsockets的源代码组织在以下主要目录中:
核心目录:
lib/- 核心库实现core/- 核心网络和协议处理roles/- 协议角色(HTTP、WebSocket、MQTT等)event-libs/- 事件循环集成secure-streams/- Secure Streams实现plugins/- 插件系统tls/- TLS/SSL支持
辅助目录:
include/- 公共头文件plugins/- 协议插件minimal-examples/- 最小示例应用test-apps/- 测试应用程序READMEs/- 文档和README文件
核心组件
核心网络层
lib/core/ 目录包含库的核心网络功能:
- 连接管理
- 事件循环集成
- 协议抽象
- 内存管理
协议角色
lib/roles/ 目录包含各种协议实现:
http/- HTTP/1.x和HTTP/2实现ws/- WebSocket协议处理mqtt/- MQTT客户端支持raw/- 原始套接字支持
每个角色实现特定协议的处理逻辑,同时共享核心网络基础设施。
事件循环集成
lib/event-libs/ 目录包含与各种事件循环库的集成:
libuv/- libuv集成libevent/- libevent集成libev/- libev集成glib/- glib集成sdevent/- systemd事件循环集成uloop/- OpenWrt事件循环集成
这些集成允许libwebsockets与现有应用程序的事件循环无缝协作。
Secure Streams
lib/secure-streams/ 目录包含Secure Streams API的实现:
- 协议处理程序(HTTP/1、HTTP/2、WebSocket、MQTT、Raw)
- 策略管理
- 状态机实现
- 代理模式支持
公共API
include/ 目录包含所有公共API头文件:
libwebsockets.h- 主要公共APIlws-http.h- HTTP相关APIlws-client.h- 客户端APIlws-secure-streams.h- Secure Streams APIlws-mqtt.h- MQTT API- 等等
插件系统
plugins/ 目录包含可选的协议插件:
- 协议实现作为可加载插件
- 支持动态加载和静态链接
- CMake集成用于构建管理
示例和测试
minimal-examples/ 目录包含大量最小示例,演示各种功能:
- WebSocket客户端和服务器
- HTTP服务器
- Secure Streams使用
- MQTT客户端
- 等等
这些示例是学习如何使用库的宝贵资源。
构建系统
libwebsockets使用CMake作为其构建系统:
- 模块化配置选项
- 跨平台支持
- 依赖管理
- 测试集成
文档
READMEs/ 目录包含详细的文档:
- 功能特定的README文件
- API文档
- 使用指南
- 最佳实践
架构原则
libwebsockets的设计遵循几个关键原则:
- 模块化 - 功能被组织成清晰的模块
- 可扩展性 - 通过插件系统支持扩展
- 性能 - 针对高并发和低延迟优化
- 可移植性 - 跨平台支持
- 安全性 - 内置安全最佳实践
结论
理解libwebsockets的库结构有助于:
- 更有效地使用库的功能
- 找到相关的API和示例
- 扩展库以满足特定需求
- 调试和故障排除
通过熟悉库的组织结构,您可以更好地利用libwebsockets提供的强大功能集。
6. 热议内容
来源: https://zread.ai/warmcat/libwebsockets/6-whats-the-buzz
Libwebsockets 继续巩固其在 C 生态系统中最通用且最强大的网络库之一的地位,近期的发展既展示了技术革新,也带来了令人惊喜的合作。
AI 进入开发流程
或许最引人注目的近期发展是 AI 辅助编码在 libwebsockets 代码库中的应用。在最近一次名为 "ws 客户端:分块处理 RX" 的提交中,项目负责人 Andy Green 与 Google 的 Gemini 2.5 Pro 合作,生成了一个补丁。
Green 的提交信息揭示了 AI 辅助开发的潜力和当前局限:
"这个补丁实际上是由 Gemini 2.5 + fixdiff 生成的。获得这个结果很困难,花了几天时间加上编写 fixdiff。如果我自己来做可能会更快,但不久的将来出于各种原因,情况可能就不再是这样了。"
这项实验是主要 C 库公开整合 AI 生成代码到其代码库的首次实例之一,在提交历史中标记为"共同开发者:Gemini 2.5 Pro"。最近还合并了使用相同 AI 合作方法的第二次提交,表明这可能会成为更常见的开发模式。
4.4 版本发布及日益普及
Libwebsockets 最近发布了 v4.4,继续保持定期更新的传统。该库的采用率持续显著增长------根据项目文档,libwebsockets 现在已被用于"数千万台设备和全球数千名开发者"。
最值得注意的是,Google 已将 libwebsockets 整合到 Android 源代码中,显著扩大了其在移动开发中的影响力。这家主要科技公司的信任票强调了该库的可靠性和性能特点。
安全流日益普及
项目两年前引入的安全流 API 继续获得关注,作为传统 websocket 接口(WSI)的更高级抽象。安全流通过将协议和端点配置移至单独的 JSON 策略文件,简化了连接性,使开发者能够专注于有效负载处理,而非连接细节。
主分支现在提供了从传统低级方法过渡到安全流的示例,旧示例已移至 ./minimal-examples-lowlevel。Andy Green 强调了这种方法如何显著降低代码复杂性,同时提供高级功能:
"安全流的寿命长于单个 wsi,因此 SS 可以自行协调重试。基于 SS 的用户代码通常比 wsi 层显著更小、更易于维护。"
安全流的一个特别强大的方面是它们可以被序列化并通过通用连接(如 UART)传输,使甚至没有网络堆栈的小型设备(如 Raspberry Pi Pico)也能通过代理与远程端点交互。
嵌入式显示创新
对于 IoT 和嵌入式开发者来说,libwebsockets 现在提供了一个 HTML + CSS + JPEG + PNG 显示堆栈,允许开发者使用网络技术驱动 EPD、TFT 或 OLED 显示屏。该功能针对资源受限环境如 ESP32,提供以下能力:
- 远程 JPEG 和 PNG 渲染
- HTML 和 CSS 渲染
- 带有伽马校正的 RGBA 合成
- 错误扩散
- 用于堆栈不足设备的行缓冲渲染
这一创新弥合了网络技术与嵌入式显示系统之间的差距,可能使嵌入式项目的 UI 开发变得更加容易。
社区增长与挑战
随着超过 404 名贡献者在 11 年多的开发中贡献了大约 15% 的代码库,libwebsockets 依然是一个充满活力的开源社区。然而,项目并非没有成长中的烦恼,近期问题凸显了:
项目还在扩展其 TLS 支持,AWS 工程师贡献 以添加 AWS Libcrypto(AWS-LC)作为除 OpenSSL 和 mbedTLS 之外的另一个 TLS 后端选项。这一添加将为 AWS Graviton 2/3 和带 AVX-512 指令的 Intel x86-64 等平台带来 CPU 特定优化。
未来展望
随着 libwebsockets 的快速演进,项目似乎在探索多个创新方向:
- 进一步的 AI 合作开发和漏洞修复
- 扩展安全流作为首选 API
- 更大的嵌入式系统支持
- 改进的跨平台兼容性
- 增强的 TLS 后端选项
随着更多开发者发现这个轻量级但强大的网络库,其在嵌入式系统和云基础设施中的影响力持续增长,巩固了其在现代 C 网络生态系统中的关键地位。
7. 大家都在说
来源: https://zread.ai/warmcat/libwebsockets/7-what-people-are-saying
libwebsockets社区对这个轻量级C网络库在多种应用中的影响力有着高度评价。从嵌入式系统工程师到Web服务架构师,开发人员一直在分享他们使用这个多功能工具的经验,这个工具在经过十多年的发展后仍在不断演进。
从嵌入式设备到企业解决方案
Libwebsockets在资源受限的环境中获得了显著的关注,这些环境对效率至关重要。正如一位开发者在Hacker News上提到的:
"我们一直在我们的物联网产品中使用libwebsockets进行实时监控。其最小的占用空间和性能对我们那些需要维持持久连接的电池供电设备来说至关重要。"
该库已经进入了许多生产系统,开发人员对其在高连接场景下的可扩展性赞不绝口。它在嵌入式Linux设备中的存在尤为引人注目,一位用户评论道:
"对于在有限资源下工作的C开发者来说,libwebsockets提供了WebSocket功能,而无需更大的框架的臃肿。"
学习曲线讨论
尽管libwebsockets功能强大,但新接触这个库的开发者对于入门表达了复杂的感受。在Issue #997中,一位用户指出:
"我注意到libwebsockets提供的示例非常详尽且功能齐全。作为一个新接触这个库的人来说,很难在合理的时间内从这些示例中掌握API。即使是一个hello world客户端,也需要大量的阅读。"
这一观点在论坛讨论中得到了共鸣,开发人员表示,示例的全面性虽然展示了库的能力,但对于寻求简单实现的人来说可能过于复杂。
对此,项目负责人Andy Green承认了这一观点,同时解释了设计理念:
"我认为没有完美的解决方案......目前入门的代价是你从一个(非常)功能性的示例开始,然后将其精简和修改以更适合你的需求。这种从一开始就知道它已经工作,并且能够逐步确认它仍然在工作的模式,实际上是非常强大的,尽管一开始代价较高。"
近期功能演进
社区对库的近期发展尤为关注。一些开发者赞赏其不断扩展的功能集,现在包括HTTP/2支持、MQTT客户端功能以及与各种事件循环的集成。
在Hacker News上的一次讨论中,对于范围的扩大出现了分歧意见:
"这只是C++缺乏包管理器的典型结果。每个库起初都很小,添加自己的供应商工具,添加自己的依赖项,最终变成了boost。"
然而,也有人辩护这种扩展:
"公平地说,这个项目在让库被他人使用方面做了很多努力......"
生产部署经验
在生产环境中部署libwebsockets的组织分享了关于其可靠性的宝贵见解。该库本身就是libwebsockets.org的支柱,展示了维护者对其作品的信心。
最近的议题报告显示了与实际部署挑战的积极互动。例如,Issue #3424展示了一位开发者解决HTTP连接测试失败的经过,而Issue #3426讨论了与mbedTLS的HTTP摘要认证。
这些实用讨论展示了该库在多种场景中的积极使用和测试,从安全通信到高性能服务器实现。
与现代工具的集成
开发者社区注意到了libwebsockets与新技术的适应性。最近的提交显示了与Gemini 2.5等AI工具的合作,提交信息如:
"ws客户端:分块处理RX - 这个补丁实际上是由Gemini 2.5 + fixdiff生成的"
这种实验性的开发方法引起了那些对AI辅助编码高性能C库感兴趣的贡献者的兴趣。
此外,正在进行的工作以支持AWS-LC(AWS Libcrypto),正如Issue #3368所展示的,显示了该库继续演进以支持现代加密后端。
性能评价
在性能指标方面,开发者普遍对libwebsockets的能力持积极态度,尤其是与更高级别的替代方案相比。正如一位用户所指出的:
"我们将实时市场数据服务的Node.js WebSocket实现切换为libwebsockets,显著降低了延迟和资源使用。"
该库在连接管理方面的高效处理能力以及能够扩展到数千个并发连接的能力,使其成为构建高并发系统的开发者的首选解决方案。
社区支持与响应性
用户强调了项目维护的响应性。议题线程显示了维护者的积极互动,包括详细的技术讨论和及时修复报告的漏洞。项目负责人Andy Green经常被看到对技术问题提供深入回应,展示了其对支持用户的承诺。
前景展望
随着libwebsockets的不断演进,社区似乎渴望更简化的入门体验,同时保持库在性能和多功能性方面的核心优势。最近的提交和议题讨论表明正在进行的工作以改进文档、修复边缘案例漏洞并增强平台兼容性。
凭借其效率、灵活性以及持续发展,libwebsockets在C网络库生态系统中仍然是一个重要的参与者,得到了一个重视其在关键任务应用中可靠性能的社区的支持。
8. 关于团队
来源: https://zread.ai/warmcat/libwebsockets/8-about-the-team
了解libwebsockets背后的智慧
当你使用强大而轻量级的C网络库libwebsockets时,很容易忘记每一行代码背后都有一位热情的开发者(或一个开发团队),他们致力于让网络更加互联、高效和安全。让我们揭开帷幕,认识那些推动这一重要网络工具包发展的人们。
Andy Green:创始人兼主要维护者
libwebsockets的掌舵人是Andy Green (@lws-team),他是项目的创始人和主要维护者。Andy在2010年末启动了libwebsockets项目,首次公开宣布是在2010年11月1日,简述为"libwebsockets - HTML5 Websocket服务器库,用C语言编写"。
Andy一直引领这个项目走过了十多年的发展历程,从WebSocket技术初露锋芒的早期,到如今支持从嵌入式系统到高性能服务器的成熟库。
凭借在嵌入式系统和网络方面的背景,Andy在warmcat.com的技术博客展示了他从FPGA和硬件设计到网络协议和安全的深厚专业知识。这种广博的知识体现在libwebsockets的设计理念中:既轻量到足以适应资源受限的环境,又强大到足以支撑企业级应用。
核心贡献者
虽然Andy领导开发,但libwebsockets也受益于一群专注的贡献者,他们帮助维护和扩展该库:
- Orgad Shaneh (@orgads):来自AudioCodes,Orgad对代码库进行了重大改进,尤其是在平台兼容性方面。
- Denis Osvald (@dosvald):一位关键贡献者,擅长事件循环实现和跨平台支持。
- Joakim Söderberg (@JoakimSoderberg):对项目的构建系统和文档做出了宝贵贡献。
- Patrick Gansterer (@paroga):为库提供了重要的修复和增强。
拥抱AI协作
libwebsockets团队中最有趣的近期发展之一是与AI的合作日益增多。最近的提交显示,Andy Green与Gemini 2.5 Pro共同作为开发者:
"这个补丁实际上是由Gemini 2.5 + fixdiff生成的......如果我自己来做会更快,但不久的将来,出于这样或那样的原因,情况可能会有所不同。" - 来自最近的提交
这代表了开源开发的有趣演变,AI正成为协作伙伴而不仅仅是工具。团队对这种做法持务实态度,仍然仔细审查AI生成的代码,同时认可其不断增强的能力。
社区驱动的发展
除了核心团队,libwebsockets的繁荣还得益于其积极参与的用户社区,他们报告问题、提出功能建议并贡献代码。活跃的GitHub问题区展示了用户和维护者之间的持续对话,Andy亲自回应了许多技术问题。
该库的演进由多种环境中的实际用例指导:
- 资源有限的嵌入式系统
- 高性能Web服务器
- IoT设备
- 跨平台应用
- 需要安全和可扩展性的企业系统
开发理念
libwebsockets背后的团队遵循几个关键原则,塑造了该库:
- 设计轻量:核心库保持小巧高效,适合嵌入式系统。
- 模块化架构:如事件循环实现(libuv、libev、libevent)等特性模块化,以提高灵活性。
- 跨平台兼容性:确保代码在Linux、Windows、macOS和各种嵌入式环境中都能运行。
- 安全优先:谨慎实现协议,并集成强大的TLS。
- 最小依赖:核心库外部依赖少,便于集成。
参与其中
libwebsockets团队欢迎新贡献者。无论你是报告bug、提出功能建议、改进文档还是提交代码,你的贡献都有助于让这个库变得更好。
与团队联系的最佳途径是:
- GitHub Issues 用于报告bug和提出功能请求
- libwebsockets网站 提供文档和资源
随着libwebsockets在我们日益互联的世界中不断演进,团队仍致力于维护一个既强大又易于使用的库,确保WebSocket技术能够惠及各类开发者,从从事IoT项目的爱好者到构建关键任务应用的企业。