发布于 

C++20 Asio With Boost 获取 B 站徽章(直播间牌子)

本文介绍了如何使用 C++20 with Asio (Boost 版本) 完成一个简单的客户端用于获取 B 站徽章。

强烈推荐昨天发现的一个视频 《Why C++20 is the Awesomest Language for Network Programming》,可以去油管上搜一下,总时长一个小时,比较长但是讲的很好,听的巨舒服,上次有这种感觉还是听那个 c10k 问题的视频。

总之先看一下程序执行的效果:

1
2
3
➜  bili-medal git:(master) ./build/main {my_cookie}
当前登录账号的 mid 为 29248492
当前登陆账号所佩戴的直播间徽章为 "ASAKI", 等级为 15 级

贴一下主要逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
awaitable<void> start(asio::io_context &ctx, const std::string &cookie)
{
const std::string host = "api.bilibili.com";

// 解析域名
auto [e1, endpoint] = co_await tcp::resolver(ctx).async_resolve(host, "https", use_nothrow_awaitable);

// ssl_ctx 用于建立 https 连接
ssl::context ssl_ctx {ssl::context::sslv23};
ssl_socket socket {ctx, ssl_ctx};

static_assert(std::is_same_v<std::decay_t<decltype(socket.next_layer())>, tcp::socket>);

// 建立 tcp 连接
ssl_ctx.set_default_verify_paths();
auto [e2, _] = co_await asio::async_connect(socket.lowest_layer(), endpoint, use_nothrow_awaitable);

// https 握手,建立 https 连接
auto [e3] = co_await socket.async_handshake(ssl::stream_base::client, use_nothrow_awaitable);

std::string mid;
{
// 创建并发送一个 api 请求到 B 站服务器,获得返回结果并解析 mid 字段
auto request = make_request(http::verb::get, host, "/x/web-interface/nav", cookie);
auto response = co_await async_send_request(socket, request);
auto data = json::parse(beast::buffers_to_string(response.body().data()));
mid = data["data"]["mid"].dump();
}
std::cout << "当前登录账号的 mid 为 " << mid << std::endl;

std::string medal_id, medal_name, medal_level;
{
// 创建并发送一个 api 请求到 B 站服务器,获得返回结果并解析直播间徽章
auto request = make_request(http::verb::get, host, "/x/space/acc/info?mid="+mid, cookie);
auto response = co_await async_send_request(socket, request);
auto data = json::parse(beast::buffers_to_string(response.body().data()));
medal_id = data["data"]["fans_medal"]["medal"]["medal_id"].dump();
medal_level = data["data"]["fans_medal"]["medal"]["level"].dump();
medal_name = data["data"]["fans_medal"]["medal"]["medal_name"].dump();
}
std::cout << "当前登陆账号所佩戴的直播间徽章为 " << medal_name << ", 等级为 " << medal_level << " 级" << std::endl;
}

整体流程我觉得是比较清晰的,基本上对 https 请求过程有所了解的话应该能容易能理解这里做了什么。

其中 async_resolve async_connect async_handshake 都是 Asio 提供的协程版本的异步函数,通过使用这些函数我们可以不必设置回调函数而是可以以同步方式书写代码,非常好用。

make_requestasync_send_request 则是我们自己定义的两个函数,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
http::request<http::empty_body> make_request(http::verb method, std::string host, std::string target, std::string cookie)
{
http::request<http::empty_body> request;

request.method(method);
request.set(http::field::host, host);
request.target(target);
request.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
request.set(http::field::cookie, "SESSDATA=" + cookie);

return request;
}

awaitable<http::response<http::dynamic_body>> async_send_request(ssl_socket &socket, http::request<http::empty_body> request)
{
auto [e1, wbytes] = co_await http::async_write(socket, request, use_nothrow_awaitable);

http::response<http::dynamic_body> response;
beast::flat_buffer buffer;
auto [e2, rbytes] = co_await http::async_read(socket, buffer, response, use_nothrow_awaitable);

co_return response;
}

逻辑也比较简单,因为是 api 调用所以我们通过 async_write 发送请求之后直接通过 async_read 读取返回结果,注意这里的返回值 awaitable<> 是一个协程包装器类型,这使得我们的 async_send_request 函数实际成为了一个协程工厂,这样我们就可以通过 co_await 来调用了

另外对于返回的数据进行处理的部分则是使用了 nlohmann_json 完成

综上我们就实现了一个简单的客户端用于通过 cookie 获取用户所佩戴的直播间牌子的功能,这里可以看到我们实际上使用协程并不能够对程序的效率有多大的提高(单线程客户端,还是顺序执行的过程),但是更大的意义在于我们用同步方式写异步代码使得代码的可读性非常高,这是非常酷的一件事,在大型项目(尤其是服务端开发)中十分重要。