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

5 篇文章 0 订阅
1 篇文章 0 订阅

说在前面

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


编译libwebsockets

  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 <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);
        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不是线程安全的库,如果想要将其进行封装为类,那么线程、信号量、锁的控制一定要非常严谨

  • 12
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是C++简易教程,包括基本语法、数据类型、流程控制、函数、类和对象等方面的内容。 ## 基本语法 C++程序的基本结构如下: ```cpp #include <iostream> using namespace std; int main() { // 程序代码 return 0; } ``` 其中: - `#include <iostream>`用于引入输入输出相关的头文件。 - `using namespace std;`用于指定命名空间。 - `int main()`是C++程序的入口函数。 - `return 0;`表示程序正常退出。 ## 数据类型 C++中的数据类型包括基本数据类型和复合数据类型,常用的数据类型如下: - 整型:`int`、`short`、`long`、`unsigned int`等。 - 浮点型:`float`、`double`、`long double`等。 - 字符型:`char`。 - 布尔型:`bool`。 - 指针型:`int*`、`char*`等。 - 引用型:`int&`、`char&`等。 ## 流程控制 C++中的流程控制语句包括: - 条件语句:`if`、`else if`、`else`。 - 循环语句:`for`、`while`、`do-while`。 - 跳转语句:`break`、`continue`、`return`。 - 开关语句:`switch`、`case`、`default`。 ## 函数 C++中的函数定义如下: ```cpp 返回值类型 函数名(参数列表) { // 函数体 return 返回值; } ``` 其中: - 返回值类型:表示函数返回的数据类型。 - 函数名:表示函数的名称。 - 参数列表:表示函数接受的参数列表。 - 函数体:表示函数的具体实现。 - 返回值:表示函数返回的值。 ## 类和对象 C++中的类和对象是面向对象编程的基础,类定义如下: ```cpp class 类名 { public: // 成员函数声明 private: // 成员变量声明 }; ``` 其中: - 类名:表示类的名称。 - 成员函数声明:表示类的成员函数。 - 成员变量声明:表示类的成员变量。 类的成员函数和成员变量可以使用`public`、`private`、`protected`等访问修饰符来限制访问权限。类的对象定义如下: ```cpp 类名 对象名; ``` 其中: - 类名:表示类的名称。 - 对象名:表示对象的名称。 对象可以调用类的成员函数和成员变量,例如: ```cpp class Student { public: void setName(string name) { this->name = name; } string getName() { return name; } private: string name; }; int main() { Student student; student.setName("Tom"); cout << student.getName() << endl; // 输出 "Tom" return 0; } ``` 以上是C++简易教程,希望能够对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值