【C++】libwebsockets库的简易教程
说在前⾯
最近很长⼀段时间,都有在嵌⼊式上进⾏websocket通信的需求。
查了很多资料,现在C++可⽤的ws第三⽅库不多,尤其是在较⽼的嵌⼊式开发环境中,既要⽀持C99和SSL,⼜需要轻量级不依赖第三⽅库,基本上就只剩下libwebsockets这个库了。
但是libwebsockets库是纯C开发,没有C++的特性,所以很多逻辑⾮常抽象,设计思路也很诡异,与之前接触的很多三⽅模块差异太⼤。我扒了源码的demo,⼜从官⽅git、wiki上了⼀点资料,才勉强搞清楚了⼀个简单的ws客户端的⼤致⽣命周期流程,写了个简单的client 端
编译libwebsockets
[github地址]
使⽤版本 v4.0-stable
1.
1. x84 Linux ,使⽤相关命令从源⾥直接获取cmake⼆进制包。例如Ubuntu:
sudo apt-get install cmake
2. 如果是嵌⼊式arm的Linux,⼀般推荐下载cmake源码⾃⾏编译(本⽂不再赘述编译部署⽅式)
3. win系统,直接下载msi/exe安装包,⼀键安装即可
2. 编译
以Linux为例
1. 进⼊libwebsockets源码⽬录
2. 创建build⽬录
mkdir build
3. cmake编译
cd build
cmake ..
make
4. 在cmake … 命令执⾏过程中,会检测系统的openssl模块
如果需要使⽤wss,则需要提前安装openssl,Ubuntu可以直接__sudo apt-get install libssl-dev__;
或者下载openssl源码安装,并设置OPENSSL_ROOT_DIR环境变量,来指定openssl的根⽬录位置__export
OPENSSL_ROOT_DIR=[openssl的⽬录]__
如果不需要wss,直接忽略cmake过程中的OPENSSL NOT FOUND警告即可
5. make之后,会在build⽬录下⽣成include⽬录和lib⽬录,这个就是libwebsockets的头⽂件和库⽂件。
libwebsockets的周期流程
核⼼思想:
1. 回调函数:libwebsockets的回调函数(lws_callbacks.h)
typedef int lws_callback_function(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
lws在初始化配置时,需要定⼀个回调函数,lws会通过该回调函数返回给开发者当前的所有状态:初始化、连接建⽴、连接失败、数据读写等等,⽽状态类型通过枚举reason来反馈。
2. 消息循环
当开发者将所有的参数配置结束后,需要循环调⽤lws_service,来反复进⾏lws内部逻辑并触发回调函数。这个循环就是消息循环。
基本流程:
1. 处理协议(定义回调函数)
2. 配置lws_context_creation_info参数
3. 创建lws_context
4. 配置连接信息lws_client_connect_info
5. 进⼊消息循环
6. 通过回调函数实时获取ws状态并进⾏下⼀步操作(发送、接受、断开、异常控制)
简易客户端代码
范例代码
#include <pthread.h>
#include <iostream>
#include <string.h>
#include <string>
#include <libwebsockets.h>
static lws *wsi = NULL;
static bool established = false;
static bool isBreak = false;
static bool stop = false;
//分析ws地址
//parameters :
//@ _url [in]完整的url
//@ _protocol [out]协议字符串 ws/wss
//@ _host [out]主机地址
//@ _port [out]端⼝,如果没有端⼝号,返回-1
//@ _path [out]url的path部分
int UnmarshalURL(const char *_url, std::string &_protocol, std::string &_host, int &_port, std::string &path)
{
std::string url(_url);
int pslash = url.find("//", 0);
if (pslash >= 0)
{
_protocol = url.substr(0, pslash - 1);
url = url.substr(pslash + 2);
}
pslash = url.find(':');
if (pslash < 0)
{
//没有端⼝号
_port = -1;
pslash = url.find('/', 0);
if (pslash < 0)
{
{
//没有path
_host = url;
}
else
{
//有path
_host = url.substr(0, pslash);
path = url.substr(pslash);
}
}
else
{
//有端⼝号
_host = url.substr(0, pslash);
url = url.substr(pslash + 1);
pslash = url.find('/', 0);
if (pslash < 0)
{
//没有path
_port = atoi(url.c_str());
}
else
{
//有path
_port = atoi(url.substr(0, pslash).c_str());
path = url.substr(pslash);
}
}
return 0;
}
//记录接收10次服务器返回
static int recvSum = 0;
// lws消息回调函数
int ws_callback(lws *_wsi, enum lws_callback_reasons _reasons, void *_user, void *_in, size_t _len)
{
printf("CALLBACK REASON: %d\n", _reasons);
//发送或者接受buffer,建议使⽤栈区的局部变量,lws会⾃⼰释放相关内存
//如果使⽤堆区⾃定义内存空间,可能会导致内存泄漏或者指针越界
char buffer[2560];
memset(buffer, 0, 2560);
switch (_reasons)
{
case LWS_CALLBACK_CLIENT_ESTABLISHED:
//连接成功时,会触发此reason
printf("established\n");
//调⽤⼀次lws_callback_on_writeable,会触发⼀次callback的LWS_CALLBACK_CLIENT_WRITEABLE,之后可进⾏⼀次发送数据操作 lws_callback_on_writable(_wsi);
break;
case LWS_CALLBACK_CLIENT_CLOSED:
// 客户端主动断开、服务端断开都会触发此reason
isBreak = true; // ws关闭,发出消息,退出消息循环
printf("ws closed\n");
break;
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
//连接失败、异常
printf("connect error\n");
break;
case LWS_CALLBACK_CLIENT_RECEIVE:
//获取到服务端的数据
memcpy(buffer, _in, _len);
printf("recv: %s\n", buffer);
usleep(1 * 1000 * 1000);
usleep(1 * 1000 * 1000);
lws_callback_on_writable(_wsi);
break;
case LWS_CALLBACK_CLIENT_WRITEABLE:
//调⽤lws_callback_on_writeable,会触发⼀次此reason
if (stop)
break;
为什么现在都用cmakerecvSum++;
if (recvSum >= 10)
{
stop = true;
printf("will close\n");
//使⽤lws_close_reason来准备断开连接的断开信息
lws_close_reason(_wsi, LWS_CLOSE_STATUS_GOINGAWAY, NULL, 0); //当callback中return⾮0 时,则会主动断开websocket
return -1;
}
sprintf(buffer, "send data %02d\0", recvSum);
printf("%s\n", buffer);
int len = lws_write(_wsi, buffer, strlen(buffer), LWS_WRITE_TEXT);
printf("write len=%d\n", len);
break;
default:
break;
}
return 0;
}
//程序输⼊参数范例
//exe ws://172.31.234.19:4455/ws/demo
//exe wss://line:3344/ws/demo
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("cmd parameters error!\n");
return -1;
}
char *url = argv[1];
printf("des URL:%s\n", url);
std::string protocol; //ws/wss协议
std::string host; //主机IP
int port; //端⼝
std::string path; //path
//解析URL的参数
UnmarshalURL(url, protocol, host, port, path);
bool ssl = protocol == "wss" ? true : false; //确认是否进⾏SSL加密
//lws初始化阶段
struct lws_context_creation_info info; //websocket 配置参数
struct lws_context *context; //websocket 连接上下⽂
struct lws_client_connect_info ci; //websocket 连接信息
//建议初始化全部置为0
memset(&info, 0, sizeof(info));
memset(&ci, 0, sizeof(ci));
struct lws_protocols lwsprotocol[2];
//ws处理协议初始化时,建议将所有内存空间置0
memset(&lwsprotocol[0], 0, sizeof(lws_protocols));
memset(&lwsprotocol[1], 0, sizeof(lws_protocols));
lwsprotocol[0].name = "ws-client";
lwsprotocol[0].callback = ws_callback; //设置回调函数
lwsprotocol[0].user = NULL;
lwsprotocol[0].tx_packet_size = 5120;
lwsprotocol[0].tx_packet_size = 5120;
lwsprotocol[0].rx_buffer_size = 5120;
lwsprotocol[1].name = NULL;
lwsprotocol[1].callback = NULL;
info.protocols = lwsprotocol; //设置处理协议
info.port = CONTEXT_PORT_NO_LISTEN; //作为ws客户端,⽆需绑定端⼝
//ws和wss的初始化配置不同
info.options = ssl ? LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT : 0; //如果是wss,需要做全局SSL初始化
context = lws_create_context(&info); //创建连接上下⽂
if (context == NULL)
{
printf("create context error\n");
return -1;
}
//初始化连接信息
ci.address = host.c_str(); //设置⽬标主机IP
ci.port = port; //设置⽬标主机服务端⼝
ci.path = path.c_str(); //设置⽬标主机服务PATH
ci.host = ci.address; //设置⽬标主机IP
ci.pwsi = &wsi; //设置wsi句柄
ci.userdata = NULL; //userdata 指针会传递给callback的user参数,⼀般⽤作⾃定义变量传⼊
ci.protocol = lwsprotocol[0].name;
//ws/wss需要不同的配置
ci.ssl_connection = ssl ? (LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK | LCCSCF_AL LOW_INSECURE) : 0;
lws_client_connect_via_info(&ci); //使连接信息⽣效
//进⼊消息循环
while (!isBreak)
{
lws_service(context, 500);
}
printf("ws disconnect\n");
return 0;
}
libwebsockets也还在学习阶段,代码只实现了简单的客户端通信,如果有其他问题,可以评论,我会在空闲时间回复解答
PS:
libwebsockets的接⼝⾮常底层,很多逻辑需要⾃⼰封装实现
libwebsockets不是线程安全的库,如果想要将其进⾏封装为类,那么线程、信号量、锁的控制⼀定要⾮常严谨
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论