差不多是为了后续某个功能插件的开发,于是开了这么个坑。之后还可以学习下相关知识,~~同时由于考试腾不出太多时间学习新知识所以拿旧项目顶一下,~~于是就有了这篇文章。。

Step1.查找b站弹幕的http请求

随便点开一个b站的直播间,打开f12,点击网络,刷新下,找有没有弹幕的相关请求包。

之后可以发现“msg”这个包

点开,从内容看,应该就是我们要找的弹幕包了(这里先略去具体的分析过程)
然后就是http请求包的具体分析了。


从浏览器中可以看到请求的网址,消息头和相关参数,下一步我们就要用c++去模拟请求了。

step2.WinInet库的简单使用

虽然用c++模拟请求时可以直接用底层的socket去发送请求,但为了方便,所以还是去直接使用相关库了。wininet库有点类似python的request库,这里就简单介绍使用wininet库去请求了。
首先是使用wininet库必须包含的头文件:

1
2
3
4
#include <Windows.h>
#include <wininet.h>

#pragma comment(lib,"wininet.lib")

这里本来想贴一个之前学习时对我帮助挺大的一个网站的,结果找不到了,只能凭着自己的记忆写了。。
首先,我们看一眼刚才的请求包,得知请求的网址是http://api.live.bilibili.com/ajax/msg,接下来我们用wininet的函数将网址分解。
这里简单贴一段示例代码,看完应该就知道这个函数怎么用了:

展开查看
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
void CrackUrl()
{
URL_COMPONENTS uc;
char Scheme[1000];
char HostName[1000];
char UserName[1000];
char Password[1000];
char UrlPath[1000];
char ExtraInfo[1000];

uc.dwStructSize = sizeof(uc);
uc.lpszScheme = Scheme;
uc.lpszHostName = HostName;
uc.lpszUserName = UserName;
uc.lpszPassword = Password;
uc.lpszUrlPath = UrlPath;
uc.lpszExtraInfo = ExtraInfo;

uc.dwSchemeLength = 1000;
uc.dwHostNameLength = 1000;
uc.dwUserNameLength = 1000;
uc.dwPasswordLength = 1000;
uc.dwUrlPathLength = 1000;
uc.dwExtraInfoLength = 1000;

InternetCrackUrl("http://hoge:henyo@www.cool.ne.jp:8080/masapico/api_sample.index", 0, 0, &uc);

printf("scheme: '%s'\n", uc.lpszScheme);
printf("host name: '%s'\n", uc.lpszHostName);
printf("port: %d\n", uc.nPort);
printf("user name: '%s'\n", uc.lpszUserName);
printf("password: '%s'\n", uc.lpszPassword);
printf("url path: '%s'\n", uc.lpszUrlPath);
printf("extra info: '%s'\n", uc.lpszExtraInfo);

printf("scheme type: ");
switch(uc.nScheme)
{
case INTERNET_SCHEME_PARTIAL:
printf("partial.\n");
break;
case INTERNET_SCHEME_UNKNOWN:
printf("unknown.\n");
break;
case INTERNET_SCHEME_DEFAULT:
printf("default.\n");
break;
case INTERNET_SCHEME_FTP:
printf("FTP.\n");
break;
case INTERNET_SCHEME_GOPHER:
printf("GOPHER.\n");
break;
case INTERNET_SCHEME_HTTP:
printf("HTTP.\n");
break;
case INTERNET_SCHEME_HTTPS:
printf("HTTPS.\n");
break;
case INTERNET_SCHEME_FILE:
printf("FILE.\n");
break;
case INTERNET_SCHEME_NEWS:
printf("NEWS.\n");
break;
case INTERNET_SCHEME_MAILTO:
printf("MAILTO.\n");
break;
default:
printf("%d\n", uc.nScheme);
}
}
然后是代码:
1
2
3
4
5
6
7
8
9
10
11
#define URL_STRING L"http://api.live.bilibili.com/ajax/msg"//Bilive API

TCHAR szHostName[128];
TCHAR szUrlPath[256];
URL_COMPONENTS crackedURL = { 0 };
crackedURL.dwStructSize = sizeof(URL_COMPONENTS);
crackedURL.lpszHostName = szHostName;
crackedURL.dwHostNameLength = ARRAYSIZE(szHostName);
crackedURL.lpszUrlPath = szUrlPath;
crackedURL.dwUrlPathLength = ARRAYSIZE(szUrlPath);
InternetCrackUrl(URL_STRING, (DWORD)URL_STRING, 0, &crackedURL);
之后是和服务器建立连接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HINTERNET hInternet = InternetOpen(L"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/15.0.849.0 Safari/535.1", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (hInternet == NULL)
return -1;

HINTERNET hHttpSession = InternetConnect(hInternet, crackedURL.lpszHostName, crackedURL.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
if (hHttpSession == NULL) {
InternetCloseHandle(hInternet);
std::cout << GetLastError();
return -2;
}

HINTERNET hHttpRequest = HttpOpenRequest(hHttpSession, L"POST", crackedURL.lpszUrlPath, NULL, L"", NULL, 0, 0);
if (hHttpRequest == NULL) {
InternetCloseHandle(hHttpSession);
InternetCloseHandle(hInternet);
return -3;
}
这三个函数从名字应该就能基本理解它们的作用了,我自己也不是特别精通就不讲了,感觉学过socket编程的话应该不难理解emm。。 然后就是向服务器发送请求了。
1
2
3
4
5
6
7
8
9
10
11
12
13
#define _HTTP_ARAC L"Content-Type: application/x-www-form-urlencoded\r\n"
char _HTTP_POST[] = "roomid=5322&csrf_token=&csrf=&visit_id=";//roomid parameters.

DWORD dwRetCode = 0;
DWORD dwSizeOfRq = sizeof(DWORD);
if (!HttpSendRequest(hHttpRequest, _HTTP_ARAC, 0, _HTTP_POST, sizeof(_HTTP_POST)) ||
!HttpQueryInfo(hHttpRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &dwRetCode, &dwSizeOfRq, NULL)
|| dwRetCode >= 400) {
InternetCloseHandle(hHttpRequest);
InternetCloseHandle(hHttpSession);
InternetCloseHandle(hInternet);
return -4;
}
经过实验,发现在基本的请求头的基础上必须要加上content-type,服务器才能正确的返回json数据,参数方面只需要roomid,其他参数都留空就可以正常使用。HttpSendRequest()就是发送http请求的函数了,应该不难看懂吧。 最后就是接收服务器返回的数据了
1
2
3
4
5
6
7
8
9
10
11
12
13
#define READ_BUFFER_SIZE 1024
std::string strRet = "";
BOOL bRet = FALSE;
char szBuffer[READ_BUFFER_SIZE + 1] = { 0 };
DWORD dwReadSize = READ_BUFFER_SIZE;
while (true){
bRet = InternetReadFile(hHttpRequest, szBuffer, READ_BUFFER_SIZE, &dwReadSize);
if (!bRet || (0 == dwReadSize)){
break;
}
szBuffer[dwReadSize] = '\0';
strRet.append(szBuffer);
}
代码应该不难看懂吧,不过最初写这段代码的时候我被坑过。虽然InternetReadFile()可以指定自己要接收的字节数,但实际每次接收的数据不一定是你写的字节数,最后总会有几个字节是乱码。所以必须用到函数给出的接收到的字节数,在后面手动加一个'\\0'才行。 这样,我们就得到了想要的数据并储存到string里了。

step3.项目的c++实现

这里json数据的解析我用的是CJsonObject,用法参考我上篇转载的博文。
这里有个需要注意的地方:我们每次的请求实际上返回的是最近的10条弹幕的数据,而我们要的是持续的弹幕姬,所以我的做法是循环进行请求,每次请求后与上次的请求进行比较,打印出不同的数据,来达到想要的效果。

1
2
3
4
5
6
7
8
9
typedef struct {
int uid;
std::string name;
std::string time;
std::string text;
} DM_DATA;

DM_DATA old_list[10] = { 0 };
DM_DATA new_list[10] = { 0 };

这里简单定义一个结构体,并认为:假如弹幕的发送者uid,发送时间,发送内容都相同的话,就认为这是同一条弹幕,就不打印。如果有人在一秒内发送多条相同的弹幕那我也没办法啦╮( ̄▽ ̄)╭不过这种情况并不常见而且也没多大影响。

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
neb::CJsonObject oJson(strRet);

for (int i = 0; i < 10; i++) {
oJson["data"]["room"][i].Get("uid", new_list[i].uid);
if (new_list[i].uid == 0)break;
oJson["data"]["room"][i].Get("nickname", new_list[i].name);
oJson["data"]["room"][i].Get("timeline", new_list[i].time);
oJson["data"]["room"][i].Get("text", new_list[i].text);
}

for (int j = 0; j < 10; j++) {
int k = 1;
for (int i = 0; i < 10; i++) {
if (old_list[i].uid == new_list[j].uid &&
old_list[i].name == new_list[j].name &&
old_list[i].time == new_list[j].time &&
old_list[i].text == new_list[j].text)k = 0;
}
if (k)std::cout << "[" << new_list[j].name << "]" << new_list[j].text << std::endl;
}
for (int i = 0; i < 10; i++) {
old_list[i].uid = new_list[i].uid;
old_list[i].name = new_list[i].name;
old_list[i].time = new_list[i].time;
old_list[i].text = new_list[i].text;
}
Sleep(3000);

neb::CJsonObject oJson(strRet);这条语句用来处理刚刚接收的json数据,然后下面的三个循环应该很好理解,第一个循环用来把接收到的数据存入新数组,第二个循环进行新数组与旧数组的比较,如不相同则打印,第三个循环把新数组的内容存入旧数组中。最后的Sleep(3000);用来防止请求过快被服务器ban。
最后我们需要的就是循环了,由于有点偷懒的原因,所以最后就加一句goto label;,把label:放在HttpSendRequest 的前面就可以了。
这样程序的设计就基本完成了。不过还需要注意的一点是,服务器返回的数据是utf8编码的,而大多数中文的windows默认是GBK编码,所以直接转换数据会乱码。最开始的时候我从网上copy了个utf8转gbk的函数,不过现在有更简单的方法,直接在代码的最前面加上一句system("chcp 65001");把控制台的编码换成utf8就ok了。

最后放个我在github上的这个项目的旧版本吧:https://github.com/panedioic/CPPDanmaku


参考资料:
[[1]【python】b站直播弹幕获取][2]
[2]: https://blog.csdn.net/qq_43017750/article/details/88041247 “【python】b站直播弹幕获取”
by 猫先生的早茶
[3]WinInet编程详解[[2]获取bilibili直播弹幕的WebSocket协议][3]
[3]: https://blog.csdn.net/xfgryujk/article/details/80306776 “获取bilibili直播弹幕的WebSocket协议”
by 炒鸡嗨客协管徐

by skilledprogrammer
[4C++实现Http Post请求][5]
[5]: https://www.cnblogs.com/lidabo/p/3346620.html “C++实现Http Post请求”
by DoubleLi
[[5]WinInet使用详解][6]
[6]: https://blog.csdn.net/analogous_love/article/details/72515002 “WinInet使用详解” (想起来这对代码大致就是以这篇文章的代码为模板写的,所以推荐这篇文章!)
by analogous_love