使用libwebsockets建立简单的websocket连接
简介
接手了一个C++项目,项目里面 websocket 采用的框架是纯C实现的libwebsockets库。目前有些问题就出现在这个库的使用上,所以需要赶紧熟悉这套框架。
项目里既有作为服务端的使用,也有作为客户端的使用,这里先分享下libwebsockets作为客户端的使用。
创建上下文lws_context
struct lws_context_creation_info content_info;
memset( &content_info, 0, sizeof(content_info) );
content_info.port = CONTEXT_PORT_NO_LISTEN;
content_info.protocols = client_callback;
lws_context *content = lws_create_context( &content_info);
以上为创建libwebsockets的上下文content,在使用libwebsockets之前必须创建一个lws_context_creation_info上下文。上下文content会管理附加到这个上下文的所有wsi,wsi可以理解为和websocket服务端的一个websocket连接。
lws_context_creation_info结构用于描述创建上下文的所需的信息,对于客户端来说,该结构常用字段如下:
- port:表示监听端口,很明显这是作为服务端需要填充的字段,客户端填充CONTEXT_PORT_NO_LISTEN(-1)即可
- protocols:这是一个指针,指向一个
struct lws_protocols类型的数组,struct lws_protocols中描述了上下文的协议名称等信息,其中非常重要的是callback字段,这是一个函数指针。当这个上下文有事件发生时(websocket连接成功、可读、可写、定时器),都会通过注册到callback字段的回调函数通知应用,应用在这个回调函数中处理业务。
客户端的上下文创建相对简单。当然,如果需要启用SSL进行加密传输的话还需要设置:
- ssl_ca_filepath:验证服务端证书的CA证书或者证书链路径
- ssl_cert_filepath:如果是双向认证,这里指定客户端证书的路径
- ssl_private_key_filepath :如果是双向认证,这里指定客户端证书私钥的路径
- options:设置LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT,初始化SSL库
创建Websocket连接
struct lws_client_connect_info connect_info;
memset(&connect_info, 0, sizeof(connect_info));
connect_info.ssl_connection = 0; // 不进行加密传输
connect_info.context = content;
connect_info.address = "127.0.0.1";
connect_info.port = 9996;
connect_info.path = "/";
connect_info.host = "127.0.0.1:9996";
lws *wsi = lws_client_connect_via_info(&connect_info);
lws_client_connect_via_info接口会根据lws_client_connect_info提供的信息创建websocket。
这里需要注意的是,lws_client_connect_via_info创建连接时,就会对前面设置的回调函数client_callback进行同步回调。
对客户端来说,lws_client_connect_via_info中常用的用于创建websocket连接的字段如下:
- ssl_connection:如果不进行加密通信,则赋值为0;反之,则赋值为非0,非0的具体含义由
lws_client_connect_ssl_connection_flags枚举定义。加密时常使用LCCSCF_USE_SSL。 - context:指定使用哪个
lws_context 上下文,即通过lws_create_context返回的上下文对象。 - address:指定要连接的地址。
- path:路径。
- host:http中的host。
- userdata:提供给应用放置私有数据的字段
- pwsi:该字段类型为
struct lws**即是一个指针的指针。lws_client_connect_via_info返回一个struct lws类型的指针,表示一个创建的websocket连接。除了通过返回值获取到struct lws*之外,还可以将你的struct lws*类型的指针的地址赋值给pwsi,lws_client_connect_via_info完成创建后会修改这个指针的指针,最后你的struct lws*类型指针将会指向创建的websocket连接。
轮询上下文lws_context
libwebsockets 中轮询上下文这个设计是刚看代码时最觉得一头雾水的地方,也是比较容易出错的地方,但这也是libwebsockets监听各种事件的关键所在。
int n = 0 ;
while (n >= 0) {
n = lws_service(context, 500);
}
这里需要循环调用lws_service接口,第一个参数表示要轮询的上下文;第二个参数表示轮询的超时时间。
轮询lws_service时,libwebsockets会检查被轮询的上下文所管理的连接上是否有新事件或数据需要处理,如果有事件或未处理的数据,就会回调在创建上下文时设置的回调函数,这里是client_callback,且是一个同步回调,即轮询的线程是哪个线程,就会使用哪个线程回调。
轮询的超时时间表示,如果在这次轮询的超时时间内没有任何事件发生,那么会从lws_service超时返回,应用可以在lws_service返回后处理其他业务。这里在调用lws_service 500ms内,如果这个上下文管理的连接都没有事件发生,500ms后将会从lws_service超时返回;相对应的,轮询期间如果有事件发生,那么在回调完client_callback后,便会从lws_service返回。
lws_service进行轮询时返回值表示发生事件的数量;如果返回值小于0,则表示有错误发生,这可以用于是否继续进行轮询的判断。
已经知道,真正处理各种网络事件的地方是在创建上下文时所设置的回调函数处,我们来看看回调函数。
回调函数
static int client_callback(struct lws *wsi,
enum lws_callback_reasons reason,
void *user,
void *in, size_t len)
当上下文中的连接有事件需要通知应用时,libwebsockets会回调client_callback。
struct lws *wsi,表示是哪个连接发生了事件,需要通知应用enum lws_callback_reasons reason,表示发生的是什么事件,下面会着重介绍下对于客户端来说比较常用的事件类型void *user,该字段是创建Websocket连接时设置的私有数据,这是C/C++惯用的伎俩void *in,如果事件有数据,那么in就是指向数据的指针size_t len,如果事件有数据,那么len表示数据长度
client_callback将会被轮询线程同步回调。回调事件的种类非常多,这里介绍下作为websocket客户端最常用到的几种回调,详细的事件种类可以参考lws_callback_reasons枚举定义。
回调事件类型
- LWS_CALLBACK_PROTOCOL_INIT:每个上下文
struct lws_context_creation_info初始化成功之后,libwebsockets都会通过该事件通知应用。 - LWS_CALLBACK_CLIENT_CONNECTION_ERROR:当建立连接失败时,libwebsockets会使用这个事件通知应用。同时失败原因可以通过
void *in获取,当然,在使用前需要做一下判空。 - LWS_CALLBACK_CLIENT_ESTABLISHED:当连接成功连接websocket后,会使用该事件通知应用。
- LWS_CALLBACK_CLIENT_RECEIVE:某个连接有数据时,会使用该事件通知应用。数据在
void *in指向的地址。 - LWS_CALLBACK_CLIENT_WRITEABLE:当连接可写时,会使用该事件通知应用写数据。
- LWS_CALLBACK_CLOSED:当websocket连接关闭时会使用该事件通知应用。
- LWS_CALLBACK_WSI_DESTROY:当连接
struct lws *wsi被销毁时,会使用该事件通知应用。
并不是每次websocket关闭都会有LWS_CALLBACK_CLOSED事件通知,但是在销毁某个连接时,libwebsockets一定会使用LWS_CALLBACK_WSI_DESTROY通知应用连接正在被销毁,所以当应用收到某个连接的LWS_CALLBACK_WSI_DESTROY通知时,应该将和这个连接绑定的私有数据中,该释放的资源释放掉,而不是在LWS_CALLBACK_CLOSED中释放,否则会引起内存泄漏。
如何主动关闭websocket连接?可以通过使回调函数client_callback返回-1的方式关闭当前产生回调的连接,正常情况下回调函数应该返回0。
写数据
int lws_write(struct lws *wsi,
unsigned char *buf, size_t len,
enum lws_write_protocol wp)
libwebsockets会使用LWS_CALLBACK_CLIENT_WRITEABLE通知应用可以使用lws_write向对端写数据。这里需要注意的是你不能在LWS_CALLBACK_CLIENT_WRITEABLE之外的地方写数据。
何时会收到LWS_CALLBACK_CLIENT_WRITEABLE的通知呢?当你的应用准备好了要向对端写数据后,你应该调用lws_callback_on_writable。
int lws_callback_on_writable(struct lws *wsi)
lws_callback_on_writable可以不用在回调函数中调用,但是当你对同一个wsi,多线程调用lws_callback_on_writable时你需要做好线程同步。
对某个wsi调用lws_callback_on_writable就是通知libwebsockets,应用打算发数据给对端了,如果这个wsi可以无阻塞的写包时,libwebsockets会使用LWS_CALLBACK_CLIENT_WRITEABLE通知应用,此时应用就可以通过这个回调发数据了。
// ...
case LWS_CALLBACK_CLIENT_WRITEABLE:{
unsigned char buff[LWS_PRE + 256] = {0};
auto *msg = (char *) &buff[ LWS_PRE ];
auto len = sprintf( msg, "hello ");
lws_write( wsi, &buff[ LWS_PRE ], len, LWS_WRITE_TEXT );
}break;
// ...
上面简单示例了一个收到LWS_CALLBACK_CLIENT_WRITEABLE通知后,向对端写数据的代码。这里需要留意的是,buff的前LWS_PRE个Bytes不能用于放置我们要发送的数据,这部分空间是libwebsockets需要使用的,我们的数据应该从buff的LWS_PRE字节开始放置。
总结
作为一个纯C实现的 websocket 协议库(还支持其他协议),libwebsockets 具有占用资源少,性能高的优点,但是想要在多线程环境下很好的使用libwebsockets,还需要下一些功夫进行深入的了解。
欢迎大家关注
微信订阅号:zl.rs
更多推荐




所有评论(0)