Bilibili直播弹幕抓取(1):WebSocket

Bilibili直播弹幕抓取(1):WebSocket

转载自https://ihomura.cn/2018/05/14/Bilibili%E7%9B%B4%E6%92%AD%E5%BC%B9%E5%B9%95%E6%8A%93%E5%8F%96-1-WebSocket/

前言

最近有一个学长去分析了B站直播弹幕WebSocket协议,我算是跟风去分析了一波。

其实协议本身并不复杂,就是JSON罢了,但是分析的过程稍微有些曲折,这里算是记录一下在这个过程中学到了什么吧。

WebSocket

WebSocket 之前在看 Socket.io 的时候我就了解过一些,不过那时候我还没有自学计网,连HTTP协议都还是一脸懵逼,所以根本没看懂。现在写过一些网络编程后再看 WebSocket 就很自然了。

为什么需要 WebSocket

WebSocket 是随着 HTML5 一起提出来的,但是它本身不是基于 HTTP 协议的。

我们知道传统的 HTTP 协议中,服务器对 Request 作出相应的 Response,如果没有 Request 服务器是不能主动发出 Response 的,毕竟 HTTP 是无状态的。

但是随着 HTML5 游戏的兴起,直播产业的蓬勃发展等等,前端对实时性的要求越来越高,同时服务器也希望有主动推送消息的能力。为了解决这个需求,基于现有的 HTTP 协议有三种办法。

轮询

这种方式是最直观的,隔一定时间就向服务器发报文询问当前最新状态,比如:

1
2
3
4
5
6
7
8
9
10
11
12
$(document).ready(function(){
    setInterval(function(){
        $.ajax({
            type : 'POST',
            url : url,
            data : data,
            success : function(){
                // do something here
            }
        });
    }, 500);
});

这种方法好处是实现起来非常简单,但是缺点是非常致命的

  • 会对服务器造成非常大的压力
  • 当没有数据的时候带宽都浪费在传输 Header 上了

为了尝试克服这些缺点就出现了长轮询和流技术。

长轮询

长轮询其实本质上还是轮询。但是不同的是,如果没有消息的话服务器不会立即返回,而是会等待一段时间,如果有足够的消息或者超时则立即返回。

贴一段 CTBX 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Bot::_tgbot_start_polling() {
    _tgbot_thread =  std::move(std::thread([this]() {
        TgBot::TgLongPoll longpoll(_tgbot, 100, 10); // 最多 100 条消息,超时时间 10 秒
        try {
            while(_polling)
                longpoll.start();
        }
        catch (const TgBot::TgException& e) {
            logging::error(u8"Bot", "LongPoll错误,原因:" + std::string(e.what()));
        }
        catch (const std::exception& e) {
            logging::error(u8"Bot", "LongPoll错误,原因:" + std::string(e.what()));
        }
    }));
}

这里的 LongPoll 就是一个长轮询,忽略网络因素的话它在下面这两种情况会返回(接收到服务器的 Response )

  • Bot 有 100 条消息待接收。
  • 从接收到 Request 后过去了 10 秒

可以看出长轮询有效克服了短轮询的一些缺点。

另一种技术是利用 iframe 实现的长连接,这种方法比较 hack,这里直接展示一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
    <head>
        <!-- 省略 -->
    </head>
    <body>
        <script>
            function doSomething(parameters){
                // iframe 返回的 javascript 会调用这个
            }
        </script>
        <iframe id='poll_iframe' src='someURL' style='display:none;'></iframe>
        <script>
            $(document).ready(function(){
                setInterval(function(){
                    var frame = document.querySelector('#poll_iframe');
                    frame.src = frame.src;
                }, 500);
            });
        </script>
    </body>
</html>

原理非常简单,就是不断更新 iframe 的 src 来保持长连接,然后服务器返回 javascript 脚本调用相应的函数。

实际上,在 HTTP/1.1 中长连接模型代替 1.0 中的短连接模型成为了默认选项,所以这种技术的意义可能并不是很大了,而且另一个致命的问题是在加载的时候浏览器的小圈会一直转,逼死强迫症。

什么是 WebSocket

其实上面这几种技术有一个共同的名称就是 Comet,它们的出发点无非就是想让服务器有新消息的时候尽快通知前端,但是 HTTP 协议设计的时候可没这么想过,所以就有了 WebSocket。

WebSocket 本质上就是两个 Socket 的双向通信,前端绑定一个地址和端口,后端绑定一个地址和端口然后就能双向通信了,所以 WebSocket 是一种和 HTTP 完全不同的协议,不过二者都是基于 TCP。

和 HTTP 联系

虽然 WebSocket 是一种完全不同的协议,不过建立 WebSocket 的时候还是需要 HTTP 帮忙, 比如下面是一个经典的握手请求:

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

可以看出这里有几个字段比较特殊,一个是 Connection,它被设置为了 Upgrade 表示客户端希望升级协议,而要升级的协议就是 Upgrade 字段中指明的 WebSocket。

然后这里还有一个 Sec-WebSocket-Key 字段,它要求服务器计算后返回一个 Sec-WebSocket-Accept 字段表明接受 WebSocket 连接。

此外 Sec-WebSocket-Protocol 字段用于选择子协议,它是可选的,但是在请求中应该只出现一次。

最后 Sec-WebSocket-Version 字段根据 RFC 现在固定是 13,之前的都应该被废弃。

Origin 虽然可以不设置,但出于安全考虑应该被设置。

下面是服务器的回应:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

首先注意到的是服务器回应了 101 状态码表示切换协议,同时正如上面提到的,包含了 Sec-WebSocket-Accept 字段表明接受 WebSocket,同时 Sec-WebSocket-Protocol 表示使用子协议 chat。

这里要强调一点,到目前为止都是 HTTP 协议的内容,接下来才是 WebSocket 的主场。

升级协议后客户端就可以打开一个 WebSocket 用于全双工通信了,比如:

1
ws = new WebSocket( "ws://someURL:port");

如果要处理信息的话可以设置相应的回调函数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function open(){
    // 当连接建立的时候调用
}

function message(evt){
    // 有消息的时候调用
}

function close(){
    // 连接被关闭的时候调用
}

function error(){
    // 发生错误的时候调用
}
ws.onopen = open;
ws.onmessage = message;
ws.onclose = close;
ws.onerror = error;
帧结构

侯捷老师曾经说过:

源码之前,了无奥秘

虽然说明了 WebSocket 的起源和特点,但是分析一个协议的话,明白它的帧结构才算是“了无奥秘”,下面是 WebSocket 的帧结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

具体的分析可以参考 RFC,这里只挑重点讲。

FIN

如果设置为 1 就表明当前是最后一片。

opcode

这个参数决定了下面的负载(payload)如何被翻译,这里只讲几个重要的取值

  • %x1 负载是文本
  • %x2 负载是二进制
  • %x9 表示 ping
  • %xA 表示 pong

从中可以看出,WebSocket 的负载可以是文本也可以是二进制,这为传输提供了良好的灵活性,同时为了防止连接因为长时间空闲被关闭,WebSocket 也提供了 ping-pong 来保持连接。

Mask

表示负载数据是否被掩码,如果设置为 1,那么负载数据应该按照后面的 Masking-key 解码。

Payload data

负载数据实际上包含扩展数据和应用数据,这里不再赘述。

调试

最后回到我们的正题: Bilibili 直播弹幕的抓取。

刚才提到了两点:

  • WebSocket 基于 TCP,区别于 HTTP 是一种新的协议。
  • WebSocket 的帧有文本和二进制两种格式。

浏览器的控制台一般只能抓到 HTTP 包,虽然实际上大部分浏览器已经支持调试 WebSocket 了,但是就 Chrome 来说,它只支持查看文本帧,而 Bilibili 的弹幕是通过二进制帧传输的,用 Chrome 调试的话就会出现下面这样的情况:

可以看到这里出现了刚才提到的 opcode 和 mask,由于 opcode 被设置为 2,所以 Chrome 不会显示负载数据的内容。

所以下一篇文章会介绍如何更好的抓包。

参考资料

RFC6455

WebSocket Wikipedia

Comet:基于 HTTP 长连接的“服务器推”技术

HTTP/1.x 的连接管理

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
根据引用\[1\]和引用\[2\]的内容,我们可以得知在bilibili上是没有办法直接查看弹幕的发送者的。虽然B站可以屏蔽某个用户发送的弹幕,但这并不意味着数据接口里有用户信息。因此,目前无法通过bilibili弹幕接口来查询弹幕的发送者。 #### 引用[.reference_title] - *1* [Python脚本如何在bilibili中查找弹幕发送者!你学会了吗!](https://blog.csdn.net/weixin_43881394/article/details/106618048)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [如何在bilibili中查找弹幕发送者](https://blog.csdn.net/dlpu_fan/article/details/106387156)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [[python]bilibili弹幕发送者查询器软件](https://blog.csdn.net/weixin_42515056/article/details/114017982)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值