(来自老VPS写的博文,第一次没有经验被种木马了55~,图没救回来)
网上搜索的时候发现,很多文档和博文都是过时的,目前各大直播网站都已经升级到使用HTML5
中的WebSocket
协议,以前的方法很明显是行不通的。
# 前言
(上个星期面试了一下百度,总共过了二面,但是发现被面试官微信拉黑了,那应该是凉凉了。不过说来倒是,自己的第一次面试太紧张了,面试官出了一道很简单的题都紧张的写错了几次,然而自己懂的东西也展现不出来。确实非常难受,知道自己是这样的人,遇到了陌生人就非常的不习惯,自己总是最慢热的那一个)
最近有点烦躁,但是该玩的还是要玩的,本文内容是通过Wireshark结合虎牙前端源码抓取直播弹幕流,是综合设计课程的内容中我自己的实现,还是非常有趣的。
# 寻找WebSocket
地址
我使用的是Chrome浏览器
,打开开发者工具中的NetWork
标签栏,并选中WS(Websocket)
过滤
图已丢失(请脑补)
可以看得到虎牙直播同时打开了数条WebSocket
连接,我们选中其中一个,并查看其Frame
流。
图已丢失(请脑补)
全部都是二进制帧,看来虎牙对消息进行了加密(不太可能)或者编码。而我从帧流和弹幕流对比中基本确认弹幕流用的是哪一条websocket
流,然后使用WireShark
抓包分析锁定了就是这一条。(后来看源码发现弹幕流有四条websocket
,虎牙前端会选择其中一条)
图已丢失(请脑补)
抓包发现还可以看到有意义的ASCII字符,说明这不是加密,而是某一种通信结构体的编码。弹幕流这种消息如果加密的话将要消耗大量的客户端和服务端资源,而如果使用常用的RPC通信框架,比如Netty,则可以对消息结构体进行压缩(二进制流和字符流的区别)和跨平台通信(类比JSON)。
# 分析源码
分析源码的步骤需要一点技巧,而我一开始没有什么经验。
还是开发者工具,点开Performance
标签页并开始记录。记录几秒后停止然后查看事件日志。
图已丢失(请脑补)
发现一个在vplayerUI.js
的p()
函数频繁被调用,于是进入函数内部查看。
function p(t) {
f++;
m++;
I += t.data.size;
var e = new FileReader;
e.onload = r;
e.readAsArrayBuffer(t.data)
}
该函数使用FileReader
读取了一个二进制流,读取完毕后调用r
函数,那我们在看r
是什么函数。
function r() {
var t = this.result;
if (localStorage.__wup > 1) {
Taf.Util.jcestream(t, 32)
}
var e = new Taf.JceInputStream(t);
var i = new HUYA.WebSocketCommand;
i.readFrom(e);
switch (i.iCmdType) {
case HUYA.EWebSocketCommandType.EWSCmd_RegisterRsp:
e = new Taf.JceInputStream(i.vData.buffer);
var r = new HUYA.WSRegisterRsp;
r.readFrom(e);
if (g) {
console.log("%c<<<<<<< %crspRegister", st("#0000E3"), st("#D9006C"), r)
}
v.dispatch("WSRegisterRsp", r);
break;
case HUYA.EWebSocketCommandType.EWSCmdS2C_RegisterGroupRsp:
e = new Taf.JceInputStream(i.vData.buffer);
var n = new HUYA.WSRegisterGroupRsp;
n.readFrom(e);
if (g) {
console.log("%c<<<<<<< %crspregisterGroup", st("#0000E3"), st("#D9006C"), n)
}
v.dispatch("WSRegisterGroupRsp", n);
break;
case HUYA.EWebSocketCommandType.EWSCmdS2C_UnRegisterGroupRsp:
e = new Taf.JceInputStream(i.vData.buffer);
var s = new HUYA.WSUnRegisterGroupRsp;
s.readFrom(e);
if (g) {
console.log("%c<<<<<<< %crspunRegisterGroup", st("#0000E3"), st("#D9006C"), s)
}
v.dispatch("WSUnRegisterGroupRsp", s);
break;
case HUYA.EWebSocketCommandType.EWSCmd_WupRsp:
var a = new Taf.Wup;
a.decode(i.vData.buffer);
var o = TafMx.WupMapping[a.sFuncName];
if (o) {
var l = new o;
var u = a.newdata.get("tRsp") ? "tRsp" : "tResp";
a.readStruct(u, l, o);
K(l);
if (g && (a.sServantName.toLowerCase() != "videogateway" || S) && !TafMx.NoLog[a.sFuncName]) {
console.log("%c<<<<<<< %crspWup:%c " + a.sFuncName, st("#0000E3"), st("black"), st("#0000E3"), a.sServantName, l)
}
if (a.iRequestId > 0) {
l.iRequestId = a.iRequestId
}
v.dispatch(a.sFuncName, l)
} else {
v.dispatch(a.sFuncName);
if (a.sFuncName != "OnUserHeartBeat") {
console.info("收到未映射的 WupRsp,sFuncName=" + a.sFuncName)
}
}
break;
case HUYA.EWebSocketCommandType.EWSCmdS2C_MsgPushReq:
e = new Taf.JceInputStream(i.vData.buffer);
var d = new HUYA.WSPushMessage;
d.readFrom(e);
var c = d.iUri;
logUtils.addUri(c);
e = new Taf.JceInputStream(d.sMsg.buffer);
var h = TafMx.UriMapping[d.iUri];
if (h) {
var p = new h;
p.readFrom(e);
K(p);
if (g && !TafMx.NoLog[c.toString()]) {
console.log("%c<<<<<<< %crspMsgPush, %curi=" + c, st("#0000E3"), st("black"), st("#8600FF"), p)
}
v.dispatch(c, p)
} else if (U) {
console.info("收到未映射的 WSPushMessage uri=" + pushMsg.iUri)
}
break;
case HUYA.EWebSocketCommandType.EWSCmdS2C_HeartBeatAck:
console.log("%c<<<<<<< rspHeartBeat: " + Date.now(), st("#0000E3"));
break;
case HUYA.EWebSocketCommandType.EWSCmdS2C_VerifyCookieRsp:
e = new Taf.JceInputStream(i.vData.buffer);
var f = new HUYA.WSVerifyCookieRsp;
f.readFrom(e);
var m = f.iValidate == 0;
if (!m) {
vplayer.trigger("verifyCookieFail")
}
logUtils.addLog("VerifyCookie校验" + (m ? "通过!" : "失败!"));
if (g) {
console.log("%c<<<<<<< %cVerifyCookie", st("#0000E3"), st("#D9006C"), "校验" + (m ? "通过!" : "失败!"), f)
}
break;
case HUYA.EWebSocketCommandType.EWSCmdS2C_MsgPushReq_V2:
e = new Taf.JceInputStream(i.vData.buffer);
var d = new HUYA.WSPushMessage_V2;
d.readFrom(e);
for (var I = 0, w = d.vMsgItem.value.length; I < w; I++) {
var y = d.vMsgItem.value[I];
var c = y.iUri;
var h = TafMx.UriMapping[c];
if (h) {
var p = new h;
var e = new Taf.JceInputStream(y.sMsg);
p.readFrom(e);
K(p);
if (g && !TafMx.NoLog[c.toString()]) {
console.log("%c<<<<<<< %crspMsgPushV2, %curi=" + c, st("#0000E3"), st("black"), st("#8600FF"), p)
}
v.dispatch(c, p)
} else if (U) {
console.info("收到未映射的 WSPushMessage_V2 uri=" + c)
}
}
break;
default:
console.log("%c<<<<<<< Not matched CmdType: " + i.iCmdType, st("#red"))
}
}
这个函数已经很明显了,就是解析消息体的函数,可以看到解析过程为
- 得到
arrayBuffer
- 新建
new Taf.JceInputStream
- 根据
EWebSocketCommandType
的值选择使用哪一种消息体解析 - 调用该消息体的
readFrom
从流中读取数据并转换为对象。 - 广播,渲染。。。
通过debugger
,发现弹幕流队形的消息体和EWebSocketCommandType
是如下关系
EWSCmdS2C_MsgPushReq_V2
,WSPushMessage_V2
EWSCmdS2C_MsgPushReq
,WSPushMessage
# Taf/Tars
一开始的我没有意识到这是一个通信框架,于是用Java的转写这段解析代码,我成功的转写了解析WSPushMessage
的函数,然而发现太麻烦了,同事转写的过程中发现该结构体非常规律,有readString
、readInt64
、readInt32
、readStruct
以及更加复杂的结构体函数。
我意识到了这是很可能是一个框架,于是开始百度搜索Taf
,然后发现没有什么结果,然后在在Github搜索,终于发现了一个叫TARS (opens new window)的RPC框架,他是腾讯的一套RPC框架,前身就是TAF。我克隆了该仓库中的流处理库Tars.Node.Stream (opens new window)。
不过发现其结构体函数和旧版的Taf有很大的不同,虽然可以成功解析消息,但是不能直接复制虎牙的源码的,需要全部转写。于是我又复制了Huya上旧版的TAF源码,并且包装成库。
const Taf = {};
// ...TAF 源码
export default Taf
然后把虎牙的消息结构体也包装成库。
import Taf from "./Taf"
const Huya = {};
export default Huya
// 其他结构体
Huya.MessageNotice.prototype._clone = function () {
return new Huya.MessageNotice
}
Huya.MessageNotice.prototype._write = function (t, e, i) {
t.writeStruct(e, i)
}
Huya.MessageNotice.prototype._read = function (t, e, i) {
return t.readStruct(e, true, i)
}
Huya.MessageNotice.prototype.writeTo = function (t) {
t.writeStruct(0, this.tUserInfo)
t.writeInt64(1, this.lTid)
t.writeInt64(2, this.lSid)
t.writeString(3, this.sContent)
t.writeInt32(4, this.iShowMode)
t.writeStruct(5, this.tFormat)
t.writeStruct(6, this.tBulletFormat)
t.writeInt32(7, this.iTermType)
t.writeVector(8, this.vDecorationPrefix)
t.writeVector(9, this.vDecorationSuffix)
t.writeVector(10, this.vAtSomeone)
t.writeInt64(11, this.lPid)
t.writeVector(12, this.vBulletPrefix)
t.writeString(13, this.sIconUrl)
t.writeInt32(14, this.iType)
t.writeVector(15, this.vBulletSuffix)
}
Huya.MessageNotice.prototype.readFrom = function (t) {
this.tUserInfo = t.readStruct(0, false, this.tUserInfo)
this.lTid = t.readInt64(1, false, this.lTid)
this.lSid = t.readInt64(2, false, this.lSid)
this.sContent = t.readString(3, false, this.sContent)
this.iShowMode = t.readInt32(4, false, this.iShowMode)
this.tFormat = t.readStruct(5, false, this.tFormat)
this.tBulletFormat = t.readStruct(6, false, this.tBulletFormat)
this.iTermType = t.readInt32(7, false, this.iTermType)
this.vDecorationPrefix = t.readVector(8, false, this.vDecorationPrefix)
this.vDecorationSuffix = t.readVector(9, false, this.vDecorationSuffix)
this.vAtSomeone = t.readVector(10, false, this.vAtSomeone)
this.lPid = t.readInt64(11, false, this.lPid)
this.vBulletPrefix = t.readVector(12, false, this.vBulletPrefix)
this.sIconUrl = t.readString(13, false, this.sIconUrl)
this.iType = t.readInt32(14, false, this.iType)
this.vBulletSuffix = t.readVector(15, false, this.vBulletSuffix)
}
// 其他结构体
# 尝试解析
开始使用TAF对消息体进行解析,只需要改造虎牙的源码即可,保留解析弹幕流部分,其他全部跳过。
我这里测试的时候,截取了开发者工具中收到的一条ArrayBuffer
进行测试,解析成功了。
import Taf from "../pojo/Taf"
import Huya from "../pojo/Huya"
// 此处t为socket响应,t.data为socket的ArrayBuffer Body
export default (t) => {
const tarsInputStream = new Taf.JceInputStream(t.data);
const webSocketCommand = new Huya.WebSocketCommand();
webSocketCommand.readFrom(tarsInputStream);
switch (webSocketCommand.iCmdType) {
case Huya.EWebSocketCommandType.EWSCmdS2C_MsgPushReq: {
const vData = new Taf.JceInputStream(webSocketCommand.vData.buffer);
const wsPushMessage = new Huya.WSPushMessage();
wsPushMessage.readFrom(vData);
const sMsg = new Taf.JceInputStream(wsPushMessage.sMsg.buffer);
if (wsPushMessage.iUri !== 1400)
console.debug("not a message");
const messageNotice = new Huya.MessageNotice();
try {
messageNotice.readFrom(sMsg);
}catch (e) {
console.error(e.message)
}
printf(messageNotice)
break;
}
case Huya.EWebSocketCommandType.EWSCmdS2C_MsgPushReq_V2: {
const vData = new Taf.JceInputStream(webSocketCommand.vData);
const wsPushMessage = new Huya.WSPushMessage_V2();
wsPushMessage.readFrom(vData);
for (let i = 0; i < wsPushMessage.vMsgItem.value.length; i++) {
const wsMsgItem = wsPushMessage.vMsgItem.value[i];
if (1400 !== wsMsgItem.iUri)
console.debug("not a message");
const messageNotice = new Huya.MessageNotice();
const sMsg = new Taf.JceInputStream(wsMsgItem.sMsg);
try {
messageNotice.readFrom(sMsg);
}catch (e) {
console.error(e.message)
}
printf(messageNotice)
}
break;
}
default:
console.debug("%c<<<<<<< Not matched CmdType: " + webSocketCommand.iCmdType)
}
}
// 打印
function printf(messageNotice) {
console.log(messageNotice.tUserInfo.sNickName + ": " + messageNotice.sContent)
}
// LOL骚男直播间弹幕
// 我xx怂: 骚哥加油我要发家致富了
# 尝试连接websocket
继续寻找源码,在刚开始载入页面的时候,我截到了一个相关的e()
函数。(省略大部分费时费力的查找和检验步骤,这就是弹幕流的websocket
连接函数)
function e() {
if (G.useHttps) {
var t = utils.getDns();
delete localStorage.wssdns
} else {
var t = utils.getIps();
delete localStorage.wssips
}
var i = t.length;
if (i == 0)
d();
var r = false;
var n = [];
var e = function(t) {
var e = t.currentTarget;
var i = e.ip;
e.onopen = e.onclose = e.onerror = undefined;
if (!r) {
r = true;
u = (G.useHttps ? "wss://" : "ws://") + i;
d(e);
c()
} else {
G.wsIps.push(i);
e.close()
}
};
var s = function(t) {
var e = t.currentTarget.ip;
if (n.indexOf(e) == -1) {
n.push(e)
}
if (n.length == i && !r) {
d()
}
};
for (var a = 0; a < i; a++) {
var o = t[a];
if (ISDEBUG && !ENV.online && G.useHttps) {
o += ":4434"
}
var l = new WebSocket((G.useHttps ? "wss://" : "ws://") + o);
l.ip = o;
l.onopen = e;
l.onclose = s;
l.onerror = s
}
}
通过打断点发现里面的t
变量储存了websocket
连接的地址,至于怎么获得的我就懒得找了。然后各个函数重新看一遍,发现虎牙会对这个数组的地址只选择一条连接,我们再查看onopen
时候调用的函数。
var e = function(t) {
var e = t.currentTarget;
var i = e.ip;
e.onopen = e.onclose = e.onerror = undefined;
if (!r) {
r = true;
u = (G.useHttps ? "wss://" : "ws://") + i;
d(e);
c()
} else {
G.wsIps.push(i);
e.close()
}
};
这里没有什么意义,就是选择一条地址进行连接,注意到一条函数c
,下一步我们会继续追踪这条函数c
。
我们先开始转写连接的代码,由于Node端和Browser端的环境不一样,Node需要引入另外的WebSocket库。
for (let url of URLS) {
if(hadOne)
break;
const client = new WebSocketClient(`ws://${url}`, "", "https://www.huya.com", {
"Pragma": "no-cache",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/70.0.3538.77 Chrome/70.0.3538.77 Safari/537.36",
"Cache-Control": "no-cache",
"Cookie": "xxx"
});
client.onerror = (error) => {
console.log('Connect Error: ' + error.toString());
};
client.onopen = () => {
console.log('WebSocket Client Connected');
// client._connection.extensions.push("permessage-deflate")
};
client.onclose = a => {
console.log('Connection Closed ' + a.reason);
};
client.onmessage = receive;
}
// WebSocket Client Connected
这里引入了一个第三方WebSocket库,可以自由选择。转写代码的同时模拟了浏览器请求,防止被封。这里注意注释的代码,是必须要加上的,这里用注释强调一下,是一个坑。
# 追踪初始化
接上之前追踪到的c
函数,
function c() {
console.log("=== WebSocket Connected ===");
logUtils.addLog("websocket连接成功");
G.loginRegister = false;
G.wsurl = u.replace(G.useHttps ? "wss://" : "ws://", "");
if (!G.wsconnected && G.reConnectTimes == 0) {
G.wsConnectTime = Date.now() - G.wsConnectTime
}
v.connected = true;
G.wsconnected = true;
P();
v.dispatch("WEBSOCKET_CONNECTED");
H = Date.now();
clearInterval(t);
t = setInterval(b, 1e3)
}
该函数检验连接状态并执行一条关键函数v.dispatch("WEBSOCKET_CONNECTED");
,继续追踪。
this.dispatch = function(t, e) {
// t = "WEBSOCKET_CONNECTED", e = undefined
C = t;
var i = k[t];
if (i instanceof Array) {
for (var r = 0, n = i.length; r < n; r++) {
var s = i[r];
if (typeof s === "function") {
s(e)
}
}
if (i.length == 0) {}
} else {}
C = null;
i = N[t];
if (i instanceof Array) {
for (var r = 0, n = i.length; r < n; r++) {
v.removeListener(t, i[r])
}
delete N[t]
}
return this
}
这里可以看到会用t
的值访问k
对象,然后对里面的函数逐个执行。这就是一个状态广播函数了。我们要继续找到k
对象中t
键对应了什么值。这里不能直接寻找声明位置了,因为声明函数的时候k = {}
。我们需要找到这个键值对生成的地方。
u.addListener("WEBSOCKET_CONNECTED", o);
经过开发者工具的debug,找到了该条命令,其中u
就是上面this.dispatch
的this
类型的实例,那我们直接追踪o。
function o() {
if (G.isReplay) {
G.hasVideo = true;
v()
} else {
_();
c()
}
if (!o.isInited) {
logUtils.addLog("ws初始化完成");
o.isInited = true;
window.connectWebSocketEnd && connectWebSocketEnd();
__vTime("WebSocket连接成功:" + (Date.now() - G.beginTime));
if (!G.isReplay) {
G.vplayer.setTafHandler(u)
}
Event.fireEvent(Event.WEBSOCKET_INITED)
}
}
可以见到此函数为WebSocket
初始化后的函数,注意_()
和c()
。这两个函数为具体的初始化函数。
function _() {
var t = _.lastTime || 0;
var e = Date.now();
if (e - t < 1e3)
return;
_.lastTime = e;
G.livingInfoTime = e;
var i = new HUYA.GetLivingInfoReq;
i.tId = G.userId;
i.lTopSid = G.topsid;
i.lSubSid = G.subsid;
i.lPresenterUid = G.presenterUid;
i.sTraceSource = ENV.ref;
u.sendWup("liveui", "getLivingInfo", i);
logUtils.addLog("getLivingInfoReq")
}
function c() {
if (!G.wsconnected || !d.isInited)
return;
var t = Date.now();
if (t - s < 1e3) {
return
}
s = t;
if (!c.isInited) {
c.isInited = true
}
var e = new HUYA.LiveLaunchReq;
e.tId = G.userId;
e.tLiveUB.eSource = HUYA.ELiveSource.WEB_HUYA;
if (G.useHttps) {
e.bSupportDomain = 1
}
logUtils.addLog("send doLaunch");
u.sendWup("liveui", "doLaunch", e)
}
我们转写该初始化函数并尝试通信。注意上面有些提前生成的变量,通过打断点获取到需要的变量内容并储存下来,在转写的时候会用得到
// 地址数组
let URLS = ["b6831c13-ws.va.huya.com", "b6831c14-ws.va.huya.com", "3d809126-ws.va.huya.com", "3d809124-ws.va.huya.com"];
// 用户信息(未登录的),注意是一个消息结构体,所以需要不能直接把储存下载的JSON直接用上,需要转回虎牙的结构体
let userId = Object.assign(new Huya.UserId(), JSON.parse('{"lUid":0,"sGuid":"3adxxxx7c5f6b","sToken":"","sHuYaUA":"webh5&1xxxx&websocket","sCookie":"vpxxxx11","iTokenType":0}'))
// 以下为直播房间相关id
const topsid = 95431869;
const subsid = 2516077608;
const presenterUid = 900821317;
// _
const buffer1 = (() => {
let getLivingInfoReq = new Huya.GetLivingInfoReq();
getLivingInfoReq.tId = userId;
getLivingInfoReq.lTopSid = topsid;
getLivingInfoReq.lSubSid = subsid;
getLivingInfoReq.lPresenterUid = presenterUid;
getLivingInfoReq.sTraceSource = "lol/0/1/1";
let wup = new Taf.Wup();
wup.setServant("liveui");
wup.setFunc("getLivingInfo");
wup.setRequestId(-1);
wup.writeStruct("tReq", getLivingInfoReq);
const webSocketCommand = new Huya.WebSocketCommand();
webSocketCommand.iCmdType = Huya.EWebSocketCommandType.EWSCmd_WupReq;
webSocketCommand.vData = wup.encode();
const o = new Taf.JceOutputStream;
webSocketCommand.writeTo(o);
return o.getBuffer();
})();
// c
const buffer2 = (() => {
let liveLaunchReq = new Huya.LiveLaunchReq;
liveLaunchReq.tId = userId;
liveLaunchReq.tLiveUB = new Huya.LiveUserbase()
liveLaunchReq.tLiveUB.eSource = Huya.ELiveSource.WEB_HUYA;
liveLaunchReq.bSupportDomain = 1
let wup = new Taf.Wup();
wup.setServant("liveui");
wup.setFunc("doLaunch");
wup.setRequestId(-1);
wup.writeStruct("tReq", liveLaunchReq);
const webSocketCommand = new Huya.WebSocketCommand();
webSocketCommand.iCmdType = Huya.EWebSocketCommandType.EWSCmd_WupReq;
webSocketCommand.vData = wup.encode();
const o = new Taf.JceOutputStream;
webSocketCommand.writeTo(o);
return o.getBuffer();
})();
然后改写socket
的开启函数
client.onopen = () => {
console.log('WebSocket Client Connected');
client._connection.extensions.push("permessage-deflate")
client.send(buffer1)
client.send(buffer2)
};
// 1002
// 1010
这里出现过两个错误,
1002
RSV标志位错误,这里的RSV是WebSocket帧中的标志位有3位,报错可以通过Wireshark抓包把发出去的帧改成合适的值。另一种情况是,检查请求头部和帧头部其他值有没有出错。
- 1010
这里协商了请求头的
extensions
值没有对应。检查了一下请求,我加上了client._connection.extensions.push("permessage-deflate")
还有一个client_max_window_bits
加不加好像都可以。最后再尝试连接,就成功了。同时收到了对应的结构体响应,我们按照上方解析步骤解析就行了。
但是这里出现了问题,初始化步骤初始化完后,websocket
没有推送弹幕流。说明还有申请包需要推送,但是源码太乱了,以及它通过广播来触发事件,让我追踪很不方便。另外,Chrome还经常卡死,我感觉优化没做好,我的机子配置很好了。
那我们通过WireShark
来抓哪一个是申请包。通过弹幕流推送的开始网上找本机的发包,我找到了一条可疑的包。
图已丢失(请脑补)
我把该包原封不动发出去,下面为十六进制流转换成ArrayBuffer
的代码
import Taf from "../pojo/Taf"
import Huya from "../pojo/Huya"
import Tars from "@tars/stream";
// 改包内容
const str = "00101d000025090002060e6c6976653a393030383231333137060e636861743a39303038323133313716002c3600"
// 转换成ArrayBuffer
const bytes = parseHexString(str);
const arrayBuffer = byteToUint8Array(bytes).buffer
function parseHexString(str) {
var result = [];
for (let i = 0; i < str.length; i += 2) {
result.push(parseInt(str[i] + str[i + 1], 16))
}
return result;
}
function byteToUint8Array(byteArray) {
var uint8Array = new Uint8Array(byteArray.length);
for (var i = 0; i < uint8Array.length; i++) {
uint8Array[i] = byteArray[i];
}
return uint8Array;
}
buffer3
即是,我们发送出去
client.onopen = () => {
console.log('WebSocket Client Connected');
client._connection.extensions.push("permessage-deflate")
client.send(buffer1)
client.send(buffer2)
client.send(buffer3)
};
// 4
同时补充一下websocket.onmessage
的receive函数
(t) => {
const tarsInputStream = new Taf.JceInputStream(t.data);
const webSocketCommand = new Huya.WebSocketCommand();
webSocketCommand.readFrom(tarsInputStream);
console.log(webSocketCommand.iCmdType);
}
// 22
// 22
// 22
// 22
// 22
// 22
这里一直受到22的推送,22就是弹幕流的结构体,说明这个包就是发送包。我们把这个包解析出来,代码和上面一样的。
const tarsInputStream = new Taf.JceInputStream(arrayBuffer);
const webSocketCommand = new Huya.WebSocketCommand();
webSocketCommand.readFrom(tarsInputStream);
console.log(webSocketCommand.iCmdType);
// 16
我们找到16对应的结构体就是wsRegisterGroupReq
了,我们把该结构体解析后重新封装一下,关键代码是wsRegisterGroupReq.vGroupId
这个数组里面的值,我们往里面添加"chat:" + presenterUid
一个字符串即可。
// sendRegisterGroup
const buffer3 = (() => {
// presenterUid 为上面的部分房间信息
const chat = "chat:" + presenterUid;
const wsRegisterGroupReq = new Huya.WSRegisterGroupReq;
wsRegisterGroupReq.vGroupId.value.push(chat);
const jceOutputStream = new Taf.JceOutputStream;
wsRegisterGroupReq.writeTo(jceOutputStream);
const webSocketCommand = new Huya.WebSocketCommand;
webSocketCommand.iCmdType = Huya.EWebSocketCommandType.EWSCmdC2S_RegisterGroupReq;
webSocketCommand.vData = jceOutputStream.getBinBuffer();
const jceOutputStream2 = new Taf.JceOutputStream;
webSocketCommand.writeTo(jceOutputStream2);
return jceOutputStream2.getBuffer();
})();
这里return
一个和之前截获到的包一样的ArrayBuffer
,在连接开始之后发送,既可以不断收到房间推送的弹幕。
# 结语
这里说的虽然很简单,但是看源码和抓包都非常费神费力,收发的包非常多,前端源码也很庞大。本文只是从走过的多条岔路中整理的一条直路。但是还是非常有趣的,学到了WebSocket以及RPC框架的不少知识。