斗鱼弹幕抓取

因为平时无聊的时候喜欢看下斗鱼之类的直播平台,所以顺便会思考下斗鱼是怎么让获取弹幕的。这里记录下过程。

抓包分析

了解到斗鱼有个专门的弹幕服务器,http://danmu.douyutv.com, 端口的话,8601,8602,12601,12602都是弹幕端口。

这里我用wireshark对其进行抓包。结果如下图:
抓包

一开始我没完全看懂这个包内容具体的意思,参考了下知乎的答案后才明白。
这个内容分为4部分。

  1. 第一部分用黑色框住的,是后3个部分内容的总字节长度。这里是5b,即后3个部分内容的字节长度为91,另外第一部分自己会占4个字节,所以这个包的data长度为95,即橙色框部分。
  2. 第二部分用红色框住的,斗鱼的规则是这个内容和第一部分一样。占4个字节。
  3. 第三部分用绿色框住的,斗鱼的规则是向服务器发送的包,这部分固定为0xb1,0x02,0x00,0x00 这4个字节,从服务器发回来的包固定为0xb2,0x02,0x00,0x00 这4个字节
  4. 最后一部分就是发送的具体内容了。

抓包得知,如果需要连上斗鱼的弹幕服务器,需要让弹幕服务器发送2个包,

  1. 一种是上图的loginreq包。
    “type@=loginreq/username@=auto_KRLJbE8mZM/password@=1234567890123456/roomid@=/“
    这个账号密码随便填都行,roomid即房间号了,如果房间号是昵称的话,需要转换成id。
  2. 另一种是joingroup包。
    “type@=joingroup/rid@=/gid@=/“
    因为每个房间因为人数原因,人数多房间的会将弹幕分流,分成几个group。这里需要选择进入哪个group,我最后实验看不同的group的弹幕其实是一样的,所以它是为了把用户均衡下,分到不同的group,避免都连上同一个而负载很高。

然后就可以获得弹幕服务器发回来的弹幕了,如下图:
抓包弹幕


抓取弹幕

然后开始来抓取弹幕,我这里用Java来抓取,这里截取部分主要流程代码。

先获得和弹幕服务器的socket连接,我这里把地址和端口写到配置文件了。

1
socket = new Socket(Config.getAddress(),Integer.parseInt(Config.getPort()));

然后向服务器发送那2个包。

1
2
sendMessage("type@=loginreq/username@=auto_KRLJbE8mZM/password@=1234567890123456/roomid@=" + roomId + "/");
sendMessage("type@=joingroup/rid@=" + roomId + "/gid@=" + groupId + "/");

这里roomId如果是昵称的话,需要转成id,我是去房间通过feedback_report_button这个id来得到的,也有其他方式。

1
2
3
4
5
6
7
String html = httpClient.doGet("http://www.douyutv.com/" + roomName);
Document doc = Jsoup.parse(html);
Element element = doc.select("#feedback_report_button").first();
String href = element.attr("href");
if (href != null) {
return href.split("=")[1];
}

gid的获取,我参考的github的一个项目。

  1. 首先去对应的斗鱼房间抓取页面,取到server_config的字符串
  2. 进行URL解码,得到一组ip, port
  3. 向其中任意的一个ip,port发送loginreq包,这个loginreq包和之前的不一样。
  4. 这个loginreq包中,devid是随机UUID,需要替换掉’-‘,字母全大写
    rt是时间戳,这里必须是以秒为单位
    ver是版本号
    vk我不懂是什么意思,我参考的那个项目是这么做的: rt + 一串固定字符串 + devid的MD5编码
  5. 发送loginreq后会收到type@=setmsgroup包,里面就有gid
1
2
3
4
5
6
7
8
9
10
11
public void sendReq() throws IOException {
String roomId = roomInformation.getRoomId();
String devid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
String rt = String.valueOf(System.currentTimeMillis() / 1000);
String ver = "20150526";
String magic = "7oE9nPEG9xXV69phU31FYCLUagKeYtsF";
String vk = MD5Utils.MD5(rt + magic + devid);

sendMessage("type@=loginreq/username@=/ct@=2/password@=/roomid@=" + roomId +
"/devid@=" + devid + "/rt@=" + rt + "/vk@=" + vk + "/ver@=" + ver + "/");
}

然后就可以从socket的inputstream来获取弹幕了,接着对获取的字符串进行解析,我这里只解析了弹幕,没有解析礼物那些。为了方便,我直接用log4j来存储弹幕了。

1
2
3
4
5
6
7
8
9
public void parseResponse(String response) {
String REGEX ="type@=chatmessage/.*/content@=(.*)/snick@=(.*?)/";
Pattern pattern = Pattern.compile(REGEX);
Matcher matcher = pattern.matcher(response);

if (matcher.find()) {
logger.info(matcher.group(2) + ": " + matcher.group(1));
}
}

然后,还需要不停的向服务器发送心跳包,否则服务器过段时间会关闭我们的连接。我这里每隔60秒发送一个心跳包。

1
sendMessage("type@=keeplive/tick@=70/");

最后,贴下获取的弹幕图:

弹幕

我抓取弹幕的过程大概就是这样。

代码地址: 代码参考