概述

        WebSocket协议是现代Web开发中不可或缺的一部分,它允许客户端和服务器之间建立持久的连接,实现双向实时通信。与传统的HTTP请求不同,WebSocket提供了一种全双工的通信通道,使得数据可以在任意方向上传输,而无需等待对方请求或者应答。

        WebSocket是在HTML5中引入的一种新协议,旨在替代轮询等技术来实现客户端与服务器间的实时交互。它通过HTTP或HTTPS协议发起一个特殊的请求,一旦连接建立成功,就可以绕过HTTP协议直接进行数据交换。其主要的工作流程可以参考下图。

        握手阶段:客户端发起一个HTTP请求,这个请求与普通的GET请求差不多。但它包含了一些额外的头字段,比如:Upgrade、Connection、Sec-WebSocket-Key等,表明客户端希望将现有的TCP连接升级为WebSocket连接。

        连接建立:如果服务器同意升级,则会返回一个状态码为101(Switching Protocols)的响应,并且同样带有Upgrade、Connection等头字段。

        数据传输:一旦握手完成,双方就可以开始发送和接收数据帧。每个帧都包含一个头部,指示数据是否为二进制还是文本形式,以及是否为消息的一部分等信息。

libwebsockets库

        libwebsockets是一个开源的C语言库,用于实现WebSocket和HTTP服务器及客户端。它非常灵活,支持多种协议,适合用于开发高性能的应用程序和服务。libwebsockets的主要功能如下:

        1、WebSocket服务器和客户端:支持WebSocket协议的完整实现,可以创建WebSocket服务器和客户端。

        2、HTTP服务器:支持HTTP/1.x和HTTP/2协议,可以创建高性能的HTTP服务器。

        3、TLS/SSL支持:支持使用OpenSSL进行加密,确保通信的安全性。

        4、事件驱动模型:采用非阻塞的I/O模型,适合处理大量并发连接。

        5、数据压缩:支持使用zlib进行数据压缩,减少带宽消耗。

        6、自定义协议:允许用户定义自己的协议,而不仅仅限于WebSocket和HTTP。

        使用libwebsockets库开发WebSocket客户端主要有六步,其工作流程可以参考下图。

        1、初始化库和上下文。首先需要初始化libwebsockets库,使用lws_create_context函数创建一个上下文对象,该对象包含了库运行所需的配置信息。

        2、创建连接。使用lws_client_connect函数创建一个WebSocket连接到指定的服务器,服务器的URL由调用方给出。

        3、注册回调函数。为了处理连接的不同阶段,我们需要注册回调函数。这些回调函数会在特定的事件发生时被调用,比如:连接建立、连接断开、数据接收等。一般在lws_context_creation_info结构体的callbacks字段中,可以指定回调函数。

        4、发送和接收数据。一旦连接建立,我们便可以开始发送和接收数据。发送数据通常在LWS_CALLBACK_CLIENT_WRITEABLE回调中进行,使用lws_send或lws_write函数,而接收数据则在LWS_CALLBACK_CLIENT_RECEIVE回调中进行。

        5、处理事件循环。libwebsockets库使用事件循环来处理网络I/O操作,我们需要在一个无限循环中调用lws_service函数来处理事件。

        6、清理资源。当不再需要连接时,应该释放所有由libwebsockets分配的资源,通常通过调用lws_context_destroy函数来销毁上下文。

        根据上面的工作流程,我们不难写出WebSocket客户端的示例代码,具体如下。

#include <libwebsockets.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define WEB_SOCKET_MSG                  "Hello From Hope Wisdom"

static lws_context *s_pContext = nullptr;

// WebSocket客户端回调函数
static int OnCallbackWebSocket(struct lws *wsi, enum lws_callback_reasons reason, 
    void *user, void *in, size_t len)
{
    switch(reason)
    {
    case LWS_CALLBACK_CLIENT_ESTABLISHED:
        // 连接已建立
        printf("Connected!\n");
        break;
    case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
        // 连接错误
        printf("Connection error: %s\n", (char *)in);
        break;
    case LWS_CALLBACK_CLIENT_CLOSED:
        // 连接关闭
        printf("Disconnected.\n");
        break;
    case LWS_CALLBACK_CLIENT_RECEIVE:
        // 接收数据
        printf("Received: %.*s\n", (int)len, (const char *)in);
        break;
    case LWS_CALLBACK_CLIENT_WRITEABLE:
        // 当连接可写时,可以发送数据
        if (lws_send(wsi, WEB_SOCKET_MSG, strlen(WEB_SOCKET_MSG), LWS_SEND_TEXT) < 0)
        {
            printf("Send failed!\n");
        }
        break;
    default:
        break;
    }

    return 0;
}

static int init_lws()
{
    struct lws_context_creation_info info;
    lws_zeroize(&info, sizeof(info));
    info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_VALIDATE_UTF8;
    // 设置回调函数
    info.callbacks = &OnCallbackWebSocket;
    // 设置端口(客户端不需要监听端口)
    info.port = LWS_NO_LISTEN;
    info.name = "Client";

    // 创建上下文
    s_pContext = lws_create_context(&info);
    if (!s_pContext)
    {
        printf("Failed to create context\n");
        return -1;
    }

    return 0;
}

static int connect_to_server(const char *uri)
{
    struct lws *wsi = nullptr;
    wsi = lws_client_connect(s_pContext, uri, nullptr, LWS_CLIENT_CONNECT);
    if (!wsi)
    {
        printf("Failed to connect to %s\n", uri);
        return -1;
    }

    return 0;
}

int main(int argc, char **argv)
{
    if (argc < 2)
    {
        printf("Usage: %s <websocket-uri>\n", argv[0]);
        return -1;
    }

    // 初始化libwebsockets库
    if (init_lws() < 0)
    {
        return -1;
    }

    // 连接到WebSocket服务器
    if (connect_to_server(argv[1]) < 0)
    {
        return -1;
    }

    // 主事件循环
    while (lws_service(s_pContext, 0))
    {
        // 自动处理连接的读写
    }

    // 释放资源
    lws_context_destroy(s_pContext);
    return 0;
}

Boost.Beast扩展

        Boost.Beast,可简称为Beast,是Boost库中的一个子项目。它是一个现代C++网络库,主要用于构建高性能的网络应用程序和服务。Beast是专门为C++11及更高版本设计的,它利用了现代C++语言的特性,比如:移动语义、智能指针等,以提高软件整体性能。

        Beast的主要组件包括:基础的网络抽象(比如:Stream、Buffer)、HTTP(包括:HTTP/1.x、HTTP/2)、WebSockets等。其主要特点如下。

        1、基于Boost.Asio。Beast是基于Boost.Asio的高级网络库,因此它可以无缝集成到任何使用Boost.Asio的项目中。Beast提供了异步IO模型,允许非阻塞的操作,这对于构建高并发的网络服务非常重要。

        2、高性能。设计用于高性能的应用场景,支持高效的内存管理,避免不必要的内存拷贝。提供了零拷贝机制,即在传输数据时不进行内存复制,而是直接使用原始数据缓冲区。

        3、易于使用。提供了简洁的API,使得编写网络应用程序变得更加直观和容易。支持错误处理机制,使得捕获和处理网络错误变得更加容易。

        4、协议支持。支持多种网络协议,包括:HTTP/1.x、HTTP/2、WebSockets等。另外,还提供了HTTP和WebSocket的客户端和服务器实现。

        5、跨平台。Beast可以在多个操作系统上运行,包括:Windows、Linux、MacOS等。它具有良好的跨平台兼容性,使得开发者可以编写一次代码并在不同平台上运行。

        使用Beast库编写WebSocket客户端比较简单,示例代码如下。

#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <iostream>
#include <string>

using namespace std;

namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = net::ip::tcp;

int main()
{
    try {
        // 创建I/O上下文和解析器
        net::io_context ioc;
        tcp::resolver resolver(ioc);

        // 解析服务器地址和端口
        auto endpoints = resolver.resolve("echo.websocket.org", "80");

        // 创建并连接socket
        tcp::socket socket(ioc);
        beast::get_lowest_layer(socket).connect(endpoints);

        // 设置HTTP请求
        http::request<http::string_body> req{http::verb::get, "/", 11};
        req.set(http::field::host, "echo.websocket.org");
        req.set(http::field::upgrade, "websocket");
        req.set(http::field::connection, "Upgrade");
        // 随机的Base64编码的WebSocket密钥
        req.set(http::field::sec_websocket_key, "dGhlIHNhbXBsZSBub25Nl29rZXk=");
        req.set(http::field::sec_websocket_version, "13");

        // 使用websocket::stream进行握手  
        websocket::stream<tcp::socket> ws{ioc};
        ws.assign(std::move(socket));

        // 发送HTTP升级请求并接收响应来完成握手  
        http::response<http::string_body> res;  
        ws.handshake(req, res);

        // 发送消息到服务器
        beast::error_code ec;
        ws.write(net::buffer("Hello, Hope Wisdom"), ec);

        // 接收消息
        while (true) 
        {
            beast::flat_buffer receive_buffer;
            websocket::opcode op;
            ws.read(op, receive_buffer);

            ws.consume(receive_buffer.data().size());
            if (op == websocket::opcode::close)
            {
                break;
            }

            cout << "Received: " << beast::buffers_to_string(receive_buffer.data()) << endl;
        }

        // 关闭连接
        ws.close(websocket::close_code::normal);
    }
    catch (exception const& e)
    {
        cerr << "Error: " << e.what() << endl;
    }

    return 0;
}

        在上面的示例代码中,我们首先创建了一个I/O上下文io_context和一个DNS解析器resolver,解析了服务器的地址和端口。接着,我们创建了一个TCP套接字,并使用解析结果中的端点列表连接到服务器。随后,设置了HTTP请求,以发起WebSocket的握手请求,其中包括了必要的头部信息,比如:Upgrade、Connection、Sec-WebSocket-Key、Sec-WebSocket-Version。这些头部信息用于告知服务器希望将连接升级为WebSocket连接,并提供了握手所需的密钥和版本号。

        为了执行WebSocket握手,我们创建了一个stream<socket>对象ws,并将之前的TCP套接字socket赋值给它。然后,调用ws.handshake函数来发送HTTP升级请求,并接收响应以完成握手过程。

        握手完成后,我们通过ws.write函数发送了一条文本消息到服务器。随后,进入一个无限循环,不断接收来自服务器的消息。每次接收到消息后,都会检查是否为关闭帧。如果是,则退出循环。否则,打印接收到的消息。

        最后,在退出循环时,我们会释放资源,关闭WebSocket连接和TCP套接字。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐