【C++】libwebsockets库的简易教程

说在前面

最近很长一段时间,都有在嵌入式上进行websocket通信的需求。
查了很多资料,现在C++可用的ws第三方库不多,尤其是在较老的嵌入式开发环境中,既要支持C99和SSL,又需要轻量级不依赖第三方库,基本上就只剩下libwebsockets这个库了。
但是libwebsockets库是纯C开发,没有C++的特性,所以很多逻辑非常抽象,设计思路也很诡异,与之前接触的很多三方模块差异太大。我扒了源码的demo,又从官方git、wiki上找了一点资料,才勉强搞清楚了一个简单的ws客户端的大致生命周期流程,写了个简单的client端


编译libwebsockets

  • [github地址]https://github.com/warmcat/libwebsockets
  • 使用版本 v4.0-stable
  1. 安装cmake
    libwebsockets的编译部署是基于cmake,所以需要事先安装cmake,可以从源或者cmake官网下载到二进制文件或者源码 [cmake官网]https://cmake.org

    • x84 Linux ,使用相关命令从源里直接获取cmake二进制包。例如Ubuntu:
      sudo apt-get install cmake
      
    • 如果是嵌入式arm的Linux,一般推荐下载cmake源码自行编译(本文不再赘述编译部署方式)
    • win系统,直接下载msi/exe安装包,一键安装即可
  2. 编译
    以Linux为例

    • 进入libwebsockets源码目录
    • 创建build目录
      mkdir build
      
    • cmake编译
      cd build
      cmake ..
      make
      
    • 在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警告即可
    • 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 
#include 
#include 
#include 
#include 

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);
        lws_callback_on_writable(_wsi);
        break;
    case LWS_CALLBACK_CLIENT_WRITEABLE:
        //调用lws_callback_on_writeable,会触发一次此reason
        if (stop)
            break;
        recvSum++;
        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://www.unruly.online: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].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.context = context;      //设置上下文
    ci.address = host.c_str(); //设置目标主机IP
    ci.port = port;            //设置目标主机服务端口
    ci.path = path.c_str();    //设置目标主机服务PATH
    ci.host = ci.address;      //设置目标主机IP
    ci.origin = 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_ALLOW_INSECURE) : 0;

    lws_client_connect_via_info(&ci); //使连接信息生效

    //进入消息循环
    while (!isBreak)
    {
        lws_service(context, 500);
    }
    printf("ws disconnect\n");
    return 0;
}


libwebsockets也还在学习阶段,代码只实现了简单的客户端通信,如果有其他问题,可以评论,我会在空闲时间回复解答

PS:
libwebsockets的接口非常底层,很多逻辑需要自己封装实现
libwebsockets不是线程安全的库,如果想要将其进行封装为类,那么线程、信号量、锁的控制一定要非常严谨

你可能感兴趣的:(C++,网络,websocket,c++)