概述
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客户端的示例代码,具体如下。
cpp
#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客户端比较简单,示例代码如下。
cpp
#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套接字。