1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. libwebsockets 的 编译 和 使用
2.1 编译
先到网站 libwebsockets 下载源码包,解压后,编译源码目录下的文件 cross-arm-linux-gnueabihf.cmake
,配置交叉编译环境(假定为 ARM 架构交叉编译 libwebsockets
):
#
# CMake Toolchain file for crosscompiling on ARM.
#
# This can be used when running cmake in the following way:
# cd build/
# cmake .. -DCMAKE_TOOLCHAIN_FILE=../cross-arm-linux-gnueabihf.cmake
#
set(CROSS_PATH /path/to/cross-compiler)
# Target operating system name.
set(CMAKE_SYSTEM_NAME Linux)
# Name of C compiler.
set(CMAKE_C_COMPILER "${CROSS_PATH}/bin/arm-linux-gnueabihf-gcc")
set(CMAKE_CXX_COMPILER "${CROSS_PATH}/bin/arm-linux-gnueabihf-g++")
# Where to look for the target environment. (More paths can be added here)
set(CMAKE_FIND_ROOT_PATH "${CROSS_PATH}")
# Adjust the default behavior of the FIND_XXX() commands:
# search programs in the host environment only.
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# Search headers and libraries in the target environment only.
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
其中,CROSS_PATH
设置为实际的交叉编译器目录。接下来编辑 CMakeLists.txt
,按需配置 libwebsockets
支持的功能特性(主要是配置 options
)。如果要编译全部的调试代码,打开 _DEBUG
宏:
#if (LWS_MBED3)
set(CMAKE_C_FLAGS "-D_DEBUG ${CMAKE_C_FLAGS}")
#endif()
笔者这里的源码版本,只有在 LWS_MBED3
开启时,才编译所有的调试信息代码,为了调试方便,这里注释掉 CMakeLists.txt
两行。注意,开启 _DEBUG
宏并不是意味着启用了所有调试信息,它只是将所有的调试代码编译进去了,要启用所有的调试信息,还需要通过接口 lws_set_log_level()
开启相应的调试信息。
接下来建立一个编译目录 build,然后进行编译:
$ mkdir build
$ cd build
$ cmake .. -DCMAKE_INSTALL_PREFIX=`pwd`/_install \
-DCMAKE_TOOLCHAIN_FILE=../cross-arm-linux-gnueabihf.cmake \
-DWITHOUT_EXTENSIONS=1 -DWITH_SSL=0
$ make -j8 # 按实际的处理器个数设定 -j 参数
$ make install
编译生成的所有文件都位于 build 目录下,供程序使用的文件安装在 build/_install
目录下:
$ cd _install
$ tree
.
├── bin
│ ├── libwebsockets-test-client
│ ├── libwebsockets-test-echo
│ ├── libwebsockets-test-fraggle
│ ├── libwebsockets-test-fuzxy
│ ├── libwebsockets-test-ping
│ ├── libwebsockets-test-server
│ ├── libwebsockets-test-server-extpoll
│ └── libwebsockets-test-server-pthreads
├── include
│ ├── libwebsockets.h
│ └── lws_config.h
├── lib
│ ├── cmake
│ │ └── libwebsockets
│ │ ├── LibwebsocketsConfig.cmake
│ │ ├── LibwebsocketsConfigVersion.cmake
│ │ ├── LibwebsocketsTargets.cmake
│ │ └── LibwebsocketsTargets-release.cmake
│ ├── libwebsockets.a
│ ├── libwebsockets.so -> libwebsockets.so.9
│ ├── libwebsockets.so.9
│ └── pkgconfig
│ └── libwebsockets.pc
└── share
└── libwebsockets-test-server
├── favicon.ico
├── leaf.jpg
├── libwebsockets.org-logo.png
├── lws-common.js
└── test.html
8 directories, 23 files
2.2 使用
程序代码通过头文件 include/libwebsockets.h
导入 libwebsockets
的接口。下面通过 libwebsockets
来构建一个 webserver
。
2.2.1 构建运行上下文
主要构建 server 监听套接字:
#include <libwebsockets.h>
// 按需自定义的 web server 数据
struct web_server_data {
...
};
struct lws_protocols lws_protos[] = {
{ "ws", web_server_callback, sizeof(struct web_server_data), 3 * 1024 * 1024 },
{ "http", lws_callback_http_dummy, 0, 0 },
{ NULL, NULL, 0 }
};
struct lws_http_mount http_mount = {
/* .mount_next */ NULL, /* linked-list "next" */
/* .mountpoint */ "/", /* mountpoint URL */
/* .origin */ "web", /* serve from dir */
/* .def */ "test.html", /* default filename */
/* .protocol */ NULL,
/* .cgienv */ NULL,
/* .extra_mimetypes */ NULL,
/* .interpret */ NULL,
/* .cgi_timeout */ 0,
/* .cache_max_age */ 0,
/* .auth_mask */ 0,
/* .cache_reusable */ 0,
/* .cache_revalidate */ 0,
/* .cache_intermediaries */ 0,
/* .origin_protocol */ LWSMPRO_FILE, /* files in a dir */
/* .mountpoint_len */ 1, /* char count */
};
struct lws_context *context;
struct lws_context_creation_info ctx_info = { 0 };
ctx_info.port = 8000;
ctx_info.iface = NULL;
ctx_info.protocols = lws_protos;
ctx_info.gid = -1;
ctx_info.uid = -1;
ctx_info.options = LWS_SERVER_OPTION_DISABLE_IPV6;
ctx_info.mounts = &http_mount;
context = lws_create_context(&ctx_info);
lws_create_context()
struct lws_context *context = NULL;
...
context = lws_zalloc(sizeof(struct lws_context));
...
if (lws_plat_init(context, info))
goto bail;
...
if (!lws_check_opt(info->options, LWS_SERVER_OPTION_EXPLICIT_VHOSTS))
// 创建 libwebsocket server [套接字 + lws]
if (!lws_create_vhost(context, info)) {
lwsl_err("Failed to create default vhost\n");
return NULL;
}
...
return context;
2.2.2 事件处理循环
// 事件处理循环:
// . 客户端连接建立、断开
// . 和客户端通信
while (1) {
lws_service(context, 1000);
msleep(1);
}
libwebsockets
预定义的了一些通知信息,在事件处理循环里传递给 web_server_callback()
处理:
// 建立客户端连接上下文
lws_service()
lws_plat_service()
lws_plat_service_tsi()
...
// 读取 server 监听套接字状态, 看 是否有连接进来
// 读取 client 套接字状态, 看 是否有数据可读
n = poll(pt->fds, pt->fds_count, timeout_ms);
...
/* any socket with events to service? */
for (n = 0; n < pt->fds_count && c; n++) { // 处理所有 poll fd 事件
...
m = lws_service_fd_tsi(context, &pt->fds[n], tsi);
...
// 超时处理
if (context->last_timeout_check_s != now) { // 秒级精度
context->last_timeout_check_s = now; // poll fd 时间时候, 更新 context 时间
...
// 超时监测:
// lws_set_timeout(wsi, reason, secs) 添加的超时监测列表
wsi = context->pt[tsi].timeout_list;
while (wsi) {
/* we have to take copies, because he may be deleted */
wsi1 = wsi->timeout_list;
tmp_fd = wsi->sock;
if (lws_service_timeout_check(wsi, (unsigned int)now)) { // 如果监测到超时对象, 关闭它并释放资源
/* he did time out... */
if (tmp_fd == our_fd)
/* it was the guy we came to service! */
timed_out = 1; // 标记监测到超时
/* he's gone, no need to mark as handled */
}
wsi = wsi1;
}
}
...
switch (wsi->mode) {
...
case LWSCM_SERVER_LISTENER: // server 监听套接字
...
...
n = lws_server_socket_service(context, wsi, pollfd); // 客户端连接上下文的建立
...
switch (wsi->mode) {
...
case LWSCM_SERVER_LISTENER:
#if LWS_POSIX
/* pollin means a client has connected to us then */
do {
...
/* listen socket got an unencrypted connection... */
clilen = sizeof(cli_addr);
...
accept_fd = accept(pollfd->fd, (struct sockaddr *)&cli_addr, &clilen); // 获取客户端连接套接字句柄
...
} ;
#endif
...
}
...
goto handled;
// 其它事件处理,这里不细表
}
...
}
// 完成客户端握手
lws_service()
...
lws_plat_service_tsi()
...
n = poll(pt->fds, pt->fds_count, timeout_ms);
...
for (n = 0; n < pt->fds_count && c; n++) { // 处理所有 poll fd 事件
...
m = lws_service_fd_tsi(context, &pt->fds[n], tsi);
...
switch (wsi->mode) {
...
case LWSCM_WS_SERVING:
...
...
n = lws_read(wsi, (unsigned char *)eff_buf.token, eff_buf.token_len);
...
switch (wsi->state) {
...
case LWSS_HTTP_HEADERS:
...
last_char = buf;
if (lws_handshake_server(wsi, &buf, len))
/* Handshake indicates this session is done. */
goto bail;
}
...
}
...
}
lws_handshake_server(wsi, &buf, len)
...
while (len--) { // 解析 http 信息,完成和客户端的握手
...
if (wsi->protocol->callback)
// 给回调 web_server_callback() 发 LWS_CALLBACK_ESTABLISHED 通知
if (wsi->protocol->callback(wsi, LWS_CALLBACK_ESTABLISHED,
wsi->user_space,
...
}
int web_server_callback(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:
...
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
...
break;
case LWS_CALLBACK_CLOSED:
...
break;
default:
break;
}
return 0;
}
3. websocket 协议
websocket
协议工作在应用层
,基于 TCP 套接字
实现。websocket
涉及的协议包括 RFC6455 和 RFC7936 。知乎博文 WebSocket 协议完整解析 是一个不错的参考。