libwebsockets客户端使用及踩坑
背景
使用的lws的版本是v4.3.3,依赖openssl 1.1.1t
初始化和连接
下述代码是一个简单的初始化时创建上下文的例子,callback是跟着子协议走的。所以在创建上下文时callback就已经准备好了
struct lws_protocols protocols[] = {
{"notification",
callback_function,
0, 4096, 0, NULL, 0},
{NULL, NULL, 0, 0} // terminator
};
struct lws_context_creation_info context_info;
memset(&context_info, 0, sizeof(context_info));
context_info.port = CONTEXT_PORT_NO_LISTEN;
context_info.protocols = protocols;
context_info.fd_limit_per_thread = 2 + 4;
// use ssl
context_info.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
context_info.options |= LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW;
context_ = lws_create_context(&context_info);
准备连接的结构体,除了必要的参数外,需要注意,这里边也有SSL相关选项
struct lws_client_connect_info connect_info;
memset(&connect_info, 0, sizeof(connect_info));
...
connect_info.ssl_connection = LCCSCF_USE_SSL; // LCCSCF_USE_SSL or 0
lws* wsi = lws_client_connect_via_info(&connect_info);
连接返回的wsi参数需要检查,可能存在极端的无法连接的返回为空的场景,需要能正确处理此时连接的未创建成功。
关于特殊业务需求
由于特殊业务需求,要求客户端不要使用lws自带的ping,由客户端自己组装业务的ping,这就需要禁用lws自带的ping功能。
下述代码关闭了自带的ping功能,和禁用了有效性确认,避免自己的断开,由业务决定断开。
lws_retry_bo_t retry_policy;
context_info.timeout_secs = 0xffff;
retry_policy.secs_since_valid_hangup = 0xffff;
retry_policy.secs_since_valid_ping = 0xffff;
context_info.retry_and_idle_policy = &retry_policy;
关于代理
在调试时如何设置代理进行抓包测试
context_info.ss_proxy_address = proxy_address.c_str();
context_info.ss_proxy_port = std::stoi(proxy_port);
context_info.http_proxy_address = proxy_address.c_str();
context_info.http_proxy_port = std::stoi(proxy_port);
在创建上下文时,设置这些参数即可
核心事件循环和事件分发
lws的事件循环是lws_service
函数,保证在线程中循环调用
lws_service(context_, DEFAULT_LWS_LOOP_MS); // DEFAULT_LWS_LOOP_MS useless
实际上第二个参数意义不大,lws内部会控制timeout时间。
执行lws_service
的线程决定了回调时所在的线程,如果不引入特殊逻辑的话,可以尽量保证关于lws的操作在同一线程内,减少锁的使用。
消息的收发
下面是回调函数的定义
int callback_function(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len);
如果来了消息,使用in参数和len参数即可。用user参数和wsi保证可以获取到会话相关信息。reason就是需要关注的事件类型了。
一般就在这个回调中解析事件类型,并处理消息的收发。
进行一个消息的发
lws有个特殊点,写消息时需要注意写的时机,并非任何时机都可写的,如果在不可写入的时机写入可能存在风险。
写入的时机在callback触发LWS_CALLBACK_CLIENT_WRITEABLE
事件时,触发该事件就可以安全写入,在这个事件处理写消息任务。
这个事件的触发依赖开发者的主动调用lws_callback_on_writable
, 调用后callback会触发LWS_CALLBACK_CLIENT_WRITEABLE
事件,然后在该事件内写消息即可。
我的处理是有一个写消息的队列,当队列有消息需要写入处理时,触发一下该事件。
std::lock_guard<std::mutex> lock(mutex_);
if ((connected_lws_ && !waiting_request_list_.empty())) {
lws_callback_on_writable(connected_lws_);
}
在callback回调中关注LWS_CALLBACK_CLIENT_WRITEABLE
事件,触发时处理写入操作。
case LWS_CALLBACK_CLIENT_WRITEABLE: {
auto lws_controller = static_cast<LWSController*>(user);
if (lws_controller)
lws_controller->HandleNextRequest(wsi);
break;
}
这里是把所有的写入抽象成了请求队列,当可写时,从请求队列中取出并消费,当然如果有多线程访问队列的话是要加锁的。
进行一个消息的收
消息的接收就没有什么特殊的地方了,当收到LWS_CALLBACK_CLIENT_RECEIVE
事件时,处理收到的消息即可
case LWS_CALLBACK_CLIENT_RECEIVE: {
auto lws_controller = static_cast<LWSController*>(user);
if (lws_controller)
lws_controller->EventDispatch(wsi, (char*)in, len);
break;
}
定时器的利用
当然推荐引入更加好用的定时器组件,但是引入其他的定时器一方面引入了额外的代码逻辑要和lws配合,另一方面就是涉及线程切换的问题,需要关注lws所在的线程和定时器线程是否会有引发线程不安全的逻辑。
如果不想引入更多线程或者不想处理线程切换等问题,那么可以考虑使用lws自带的timer能力,虽然这个能力有限,还需一部分小改造。
实现lws线程内部的定时器,需要用到他回调中的LWS_CALLBACK_TIMER
事件和触发这个事件的函数
case LWS_CALLBACK_TIMER: {
auto lws_controller = static_cast<LWSController*>(user);
if (lws_controller) {
bool timeout_need_to_close = lws_controller->HandleNextTimer(wsi);
if (timeout_need_to_close) {
SPDLOG_ERROR("Handle timeout timer need to close web socket.");
return -1;
}
}
break;
}
如果callback返回不为0,那么连接会被断开,并触发LWS_CALLBACK_CLIENT_CLOSED
,利用这点实现了一个简单的定时器,任何超时都会触发断开,后续可以处理重连
说一下定时器的限制,一个wsi同时只能有一个定时器存在,新设置的定时器会覆盖之前的设置,前一个定时直接消失在虚空,并从此刻开始计时新的计时器
lws_set_timer_usecs(wsi, timeout_usces);
通过上述函数进行定时器的设置,结合callback来看,确实其实无法设置多个定时器,没有足够的信息区分不同定时器。
因此设计Timer
类型如下
class Timer {
public:
// Return value of callback means if need to close web socket. For timeout.
// Set return true if you need to close web socket when the timer trigger.
Timer(const std::string& timer_id, bool ac, std::chrono::system_clock::time_point tp, std::function<bool()> callback)
: id(timer_id),
active(ac),
timeout_tp(tp),
timeout_cb(callback) {
}
std::string id;
bool active = true;
std::chrono::system_clock::time_point timeout_tp = std::chrono::system_clock::now();
std::function<bool()> timeout_cb;
};
id用于区分timer,active用于定时器的取消和关闭(取消定时器时,不会直接清理Timer对象,而是设置active,处理timer时直接忽视active为false的timer,可以减少操作储存Timer容器的次数)。
而timer_callback
的返回值就是为了上述可以处理超时断开准备的。
std::priority_queue<
TimerPtr,
std::vector<TimerPtr>,
TimerCompare
> timer_queue_;
用优先级队列存储timer,这里应该定义一个超时时间较短的优先级高的对比函数,保证快到期的timer位于队列头部,方便插入新的timer时进行对比,来决定是否需要重设lws_set_timer_usecs
超时时间。
如果维护了多个Timer,还可以通过再加一个容器方便进行timer查询和失效。
连接断开与资源回收
需要注意!只有确保连接已经断开(收到LWS_CALLBACK_CLIENT_CLOSED
)后才可以安全的清理资源,如果提前销毁连接资源,可能会引发异常Bug。
一般这种断开的来源为客户端主动调用关闭,和服务器的断开请求。
服务器断开请求或者是异常断网,都会主动触发LWS_CALLBACK_CLIENT_CLOSED
事件,此时在回调中处理该事件,可以直接清理资源,不要自己主动清理资源,要在LWS_CALLBACK_CLIENT_CLOSED
事件中处理资源清理!
case LWS_CALLBACK_CLIENT_CLOSED: {
// clean the web socket and handle reconnect only client received this.
auto lws_controller = static_cast<LWSController*>(user);
if (lws_controller)
lws_controller->CleanWebSocket(wsi);
break;
}