文章目录

一、教程1、相关依赖2、获取Cookie3、自定义GET方法,让每次请求都带上cookie3、定义获取弹幕服务器信息 和 使用4、定义websocket监听类,处理监听到的事件4.1先新建几个常量,后面方便使用。4.2定义WebsocketListener 监听类定义 封包 / 解包 方法定义WebsocketListener 封包方法定义 WebsocketListener 创建鉴权包方法定义 WebsocketListener 创建心跳包方法定义 WebsocketListener 的解包方法

4.3 实现WebsocketListener 的 onOpen方法。4.4 实现WebsocketListener 的 onMessage方法,接收服务器消息

二、我的代码实现1、创建BiliRequest.java2、创建WebsocketListener.java 监听器3、使用

一、教程

如果只想要代码实现,直接看第二部分。

1、相关依赖

fastjson2用于解析JSON字符串,可自行替换成别的框架。 hutool-core用于解压zip数据,可自行替换成别的框架。

com.alibaba.fastjson2

fastjson2

2.0.40

cn.hutool

hutool-core

5.8.21

2、获取Cookie

2023年9月B站如果不登录,获取到的弹幕消息是经过脱敏的,获取不到用户名和用户ID。 获取方式: 电脑浏览器登录B站,按F12去网络请求里把B站Cookie值全部复制出来。

3、自定义GET方法,让每次请求都带上cookie

private String get(String url) throws IOException, NoSuchAlgorithmException, InvalidKeyException {

String bodyStr = JSONObject.toJSONString(dataMap);

HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();

con.setRequestMethod(method.code);

/**----------设置请求头------------------------------------------------------------------**/

con.setRequestProperty("User-Agent", "Mozilla/5.0");

con.setRequestProperty("Accept", "application/json");application/json。

con.setRequestProperty("Content-Type", "application/json");

con.setRequestProperty("Cookie", cookie);

// 获取响应结果

try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))){

// 返回响应结果

return bufferedReader.lines().collect(Collectors.joining("\n"));

}

}

3、定义获取弹幕服务器信息 和 使用

该方法可以获得弹幕服务器信息和检验你是否登录的token。

public JSONObject getDanmuInfoData(int roomid) throws IOException, NoSuchAlgorithmException, InvalidKeyException {

//获取直播间真实ID ,因为存在短ID

String result = get("https://api.live.bilibili.com/room/v1/Room/room_init?id="+roomid);

roomid = JSONObject.parseObject(result).getJSONObject("data").getIntValue("room_id");;

//获取弹幕服务信息

result = get("https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?type=0&id="+roomid);

return JSONObject.parseObject(getDanmuInfo(result )).getJSONObject("data");

}

使用:

//获取弹幕服务器信息

JSONObject danmuInfoData = getDanmuInfoData(直播间ID);

//获取完整弹幕信息的Token

String token = danmuInfoData.getString("token");

//服务器节点列表

JSONArray hostList = danmuInfoData.getJSONArray("host_list");

//选一个服务器节点

JSONObject host = hostList.getJSONObject(0);

//弹幕服务器地址

String wsUrl = String.format("ws://%s:%s/sub", host.getString("host"), host.getString("ws_port"));

4、定义websocket监听类,处理监听到的事件

4.1先新建几个常量,后面方便使用。

public interface Opt{

short HEARTBEAT = 2;// 客户端发送的心跳包(30秒发送一次)

short HEARTBEAT_REPLY = 3;// 服务器收到心跳包的回复 人气值,数据不是JSON,是4字节整数

short SEND_SMS_REPLY = 5;// 服务器推送的弹幕消息包

short AUTH = 7;//客户端发送的鉴权包(客户端发送的第一个包)

short AUTH_REPLY = 8;//服务器收到鉴权包后的回复

}

public interface Version{

short NORMAL = 0;//Body实际发送的数据——普通JSON数据

short ZIP = 2; //Body中是经过压缩后的数据,请使用zlib解压,然后按照Proto协议去解析。

}

4.2定义WebsocketListener 监听类

定义变量cookie、roomid、token,需要用他们生成鉴权包。

@ClientEndpoint

public class WebsocketListener {

private String cookie;

private int roomid;

private String token;

public WebsocketListener(String cookie, int roomid, String token) {

this.cookie = cookie;

this.roomid = roomid;

this.token = token;

}

private Session session;

@OnOpen

public void onOpen(Session session) throws IOException {

}

@OnMessage

public void onMessage(ByteBuffer byteBuffer) {

}

@OnClose

public void onClose(Session session, CloseReason closeReason) {

System.out.println("服务器断开: " + closeReason);

}

@OnError

public void onError(Session session, Throwable t) {

t.printStackTrace();

}

定义 封包 / 解包 方法

Websocket发送和接收时使用。

定义WebsocketListener 封包方法

public static byte[] pack(String jsonStr, @NonNull short code) throws IOException {

byte[] contentBytes = new byte[0];

if(Opt.AUTH == code){

contentBytes = jsonStr.getBytes();

}

try(ByteArrayOutputStream data = new ByteArrayOutputStream();

DataOutputStream stream = new DataOutputStream(data)){

stream.writeInt(contentBytes.length + 16);//封包总大小

stream.writeShort(16);//头部长度 header的长度,固定为16

stream.writeShort(Version.NORMAL);

stream.writeInt(code);//操作码(封包类型)

stream.writeInt(1);//保留字段,可以忽略。

if(Opt.AUTH == code){

stream.writeBytes(jsonStr);

}

return data.toByteArray();

}

}

定义 WebsocketListener 创建鉴权包方法

用于鉴权。 这里就需要用到变量cookie、roomid、token。

public byte[] generateAuthPack(String cookie,int roomid, String token) throws IOException {

JSONObject jo = new JSONObject();

Arrays.stream(cookie.split(";")).forEach(c ->{

if(c.trim().startsWith("DedeUserID=")){

jo.put("uid", Long.valueOf(c.split("=")[1]));

}else if(c.trim().startsWith("buvid3=")){

jo.put("buvid", c.split("=")[1]);

}

});

jo.put("roomid", roomid);

jo.put("protover", 1);

jo.put("platform", "web");

jo.put("type", 2);

jo.put("key", token);

return pack(jo.toString(), Opt.AUTH);

}

定义 WebsocketListener 创建心跳包方法

用于维持服务连接。

public static byte[] generateHeartBeatPack() throws IOException {

return pack(null, Opt.HEARTBEAT);

}

定义 WebsocketListener 的解包方法

用于解析服务器返回的消息。 在方法内解析完消息后,可以在 //todo处自定义方法 或 处理器去处理弹幕消息;

public static void unpack(ByteBuffer byteBuffer){

int packageLen = byteBuffer.getInt();

short headLength = byteBuffer.getShort();

short protVer = byteBuffer.getShort();

int optCode = byteBuffer.getInt();

int sequence = byteBuffer.getInt();

if(Opt.HEARTBEAT_REPLY == optCode){

System.out.println("这是服务器心跳回复");

}

byte[] contentBytes = new byte[packageLen - headLength];

byteBuffer.get(contentBytes);

//如果是zip包就进行解包

if(Version.ZIP == protVer){

unpack(ByteBuffer.wrap(ZipUtil.unZlib(contentBytes)));

return;

}

String content = new String(contentBytes, StandardCharsets.UTF_8);

if(Opt.AUTH_REPLY == optCode){

//返回{"code":0}表示成功

System.out.println("这是鉴权回复:"+content);

}

//真正的弹幕消息

if(Opt.SEND_SMS_REPLY == optCode){

System.out.println("真正的弹幕消息:"+content);

//todo 自定义处理

}

//只存在ZIP包解压时才有的情况

//如果byteBuffer游标 小于 byteBuffer大小,那就证明还有数据

if(byteBuffer.position() < byteBuffer.limit()){

unpack(byteBuffer);

}

}

4.3 实现WebsocketListener 的 onOpen方法。

连接成功后,需要做两件事:

发送鉴权包发送维持服务连接的心跳包

@OnOpen

public void onOpen(Session session) throws IOException {

this.session = session;

RemoteEndpoint.Async remote = session.getAsyncRemote();

//鉴权协议包

ByteBuffer authPack = ByteBuffer.wrap(generateAuthPack());

remote.sendBinary(authPack);

//每30秒发送心跳包

ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

executorService.scheduleAtFixedRate(() -> {

try {

ByteBuffer heartBeatPack = ByteBuffer.wrap(generateHeartBeatPack());

remote.sendBinary(heartBeatPack);

} catch (IOException e) {

throw new RuntimeException(e);

}

}, 0, 30, TimeUnit.SECONDS);

}

4.4 实现WebsocketListener 的 onMessage方法,接收服务器消息

需要调用上面的解包方法。

@OnMessage

public void onMessage(ByteBuffer byteBuffer) {

//解包

unpack(byteBuffer);

}

二、我的代码实现

1、创建BiliRequest.java

import com.alibaba.fastjson2.JSONArray;

import com.alibaba.fastjson2.JSONObject;

import jakarta.websocket.ContainerProvider;

import jakarta.websocket.DeploymentException;

import jakarta.websocket.WebSocketContainer;

import java.io.BufferedReader;

import java.io.DataOutputStream;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.HttpURLConnection;

import java.net.URI;

import java.net.URISyntaxException;

import java.net.URL;

import java.nio.charset.StandardCharsets;

import java.security.InvalidKeyException;

import java.security.NoSuchAlgorithmException;

import java.util.Map;

import java.util.stream.Collectors;

public class BiliRequest {

private String cookie;

public BiliRequest(String cookie) {

this.cookie = cookie;

}

/**

* 获取直播间ID ,因为存在短ID

*/

private String getReadRoomId(int roomid) throws IOException, NoSuchAlgorithmException, InvalidKeyException {

return get("https://api.live.bilibili.com/room/v1/Room/room_init?id="+roomid);

}

/**获得弹幕服务地址信息**/

private String getDanmuInfo(int roomid) throws IOException, NoSuchAlgorithmException, InvalidKeyException {

return get("https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?type=0&id="+roomid);

}

public JSONObject getDanmuInfoData(int roomid) throws IOException, NoSuchAlgorithmException, InvalidKeyException {

JSONObject readRoomId = JSONObject.parseObject(getReadRoomId(roomid));

roomid = readRoomId.getJSONObject("data").getIntValue("room_id");

return JSONObject.parseObject(getDanmuInfo(roomid)).getJSONObject("data");

}

enum Method{

GET("GET"),POST("POST");

public String code;

Method(String code) {

this.code = code;

}

}

public String get(String url) throws IOException, NoSuchAlgorithmException, InvalidKeyException {

return request(Method.GET,url,null);

}

private String request(Method method,String url, Map dataMap) throws IOException, NoSuchAlgorithmException, InvalidKeyException {

String bodyStr = JSONObject.toJSONString(dataMap);

HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();

con.setRequestMethod(method.code);

/**----------设置请求头------------------------------------------------------------------**/

con.setRequestProperty("User-Agent", "Mozilla/5.0");

con.setRequestProperty("Accept", "application/json");

con.setRequestProperty("Content-Type", "application/json");

con.setRequestProperty("Cookie", cookie);

// 发送 POST 请求

if(Method.POST == method && null != dataMap && !dataMap.isEmpty()){

con.setDoOutput(true);

try(DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {

wr.writeBytes(bodyStr);

wr.flush();

}

}

// 获取响应结果

try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))){

// 返回响应结果

return bufferedReader.lines().collect(Collectors.joining("\n"));

}

}

}

2、创建WebsocketListener.java 监听器

import cn.hutool.core.util.ZipUtil;

import com.alibaba.fastjson2.JSONObject;

import jakarta.websocket.*;

import java.io.ByteArrayOutputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.nio.ByteBuffer;

import java.nio.charset.StandardCharsets;

import java.util.Arrays;

import java.util.concurrent.Executors;

import java.util.concurrent.ScheduledExecutorService;

import java.util.concurrent.TimeUnit;

@ClientEndpoint

public class WebsocketListener {

private String cookie;

private int roomid;

private String token;

public WebsocketListener(String cookie, int roomid, String token) {

this.cookie = cookie;

this.roomid = roomid;

this.token = token;

}

private Session session;

@OnOpen

public void onOpen(Session session) throws IOException {

this.session = session;

RemoteEndpoint.Async remote = session.getAsyncRemote();

//鉴权协议包

ByteBuffer authPack = ByteBuffer.wrap(generateAuthPack());

remote.sendBinary(authPack);

//每30秒发送心跳包

ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

executorService.scheduleAtFixedRate(() -> {

try {

ByteBuffer heartBeatPack = ByteBuffer.wrap(generateHeartBeatPack());

remote.sendBinary(heartBeatPack);

} catch (IOException e) {

throw new RuntimeException(e);

}

}, 0, 30, TimeUnit.SECONDS);

}

@OnMessage

public void onMessage(ByteBuffer byteBuffer) {

//解包

unpack(byteBuffer);

}

@OnClose

public void onClose(Session session, CloseReason closeReason) {

System.out.println("断开连接: " + closeReason);

}

@OnError

public void onError(Session session, Throwable t) {

t.printStackTrace();

}

public interface Opt{

short HEARTBEAT = 2;// 客户端发送的心跳包(30秒发送一次)

short HEARTBEAT_REPLY = 3;// 服务器收到心跳包的回复 人气值,数据不是JSON,是4字节整数

short SEND_SMS_REPLY = 5;// 服务器推送的弹幕消息包

short AUTH = 7;//客户端发送的鉴权包(客户端发送的第一个包)

short AUTH_REPLY = 8;//服务器收到鉴权包后的回复

}

public interface Version{

short NORMAL = 0;//Body实际发送的数据——普通JSON数据

short ZIP = 2; //Body中是经过压缩后的数据,请使用zlib解压,然后按照Proto协议去解析。

}

/**

* 封包

* @param jsonStr 数据

* @param code 协议包类型

* @return

* @throws IOException

*/

public static byte[] pack(String jsonStr, short code) throws IOException {

byte[] contentBytes = new byte[0];

if(Opt.AUTH == code){

contentBytes = jsonStr.getBytes();

}

try(ByteArrayOutputStream data = new ByteArrayOutputStream();

DataOutputStream stream = new DataOutputStream(data)){

stream.writeInt(contentBytes.length + 16);//封包总大小

stream.writeShort(16);//头部长度 header的长度,固定为16

stream.writeShort(Version.NORMAL);

stream.writeInt(code);//操作码(封包类型)

stream.writeInt(1);//sequence,可以取常数1 .保留字段,可以忽略。

if(Opt.AUTH == code){

stream.writeBytes(jsonStr);

}

return data.toByteArray();

}

}

/**

* 生成认证包

* @return

*/

public byte[] generateAuthPack(String jsonStr) throws IOException {

return pack(jsonStr, Opt.AUTH);

}/**

* 生成认证包-用于非官方开放API

* @return

*/

public byte[] generateAuthPack() throws IOException {

JSONObject jo = new JSONObject();

Arrays.stream(cookie.split(";")).forEach(c ->{

if(c.trim().startsWith("DedeUserID=")){

jo.put("uid", c.split("=")[1]);

}else if(c.trim().startsWith("buvid3=")){

jo.put("buvid", c.split("=")[1]);

}

});

jo.put("roomid", String.valueOf(roomid));

jo.put("protover", Version.NORMAL);

jo.put("platform", "web");

jo.put("type", 2);

jo.put("key", token);

return pack(jo.toString(), Opt.AUTH);

}

/**

* 生成心跳包

* @return

*/

public static byte[] generateHeartBeatPack() throws IOException {

return pack(null, Opt.HEARTBEAT);

}

/**

* 解包

* @param byteBuffer

* @return

*/

public static void unpack(ByteBuffer byteBuffer){

int packageLen = byteBuffer.getInt();

short headLength = byteBuffer.getShort();

short protVer = byteBuffer.getShort();

int optCode = byteBuffer.getInt();

int sequence = byteBuffer.getInt();

if(Opt.HEARTBEAT_REPLY == optCode){

System.out.println("这是服务器心跳回复");

}

byte[] contentBytes = new byte[packageLen - headLength];

byteBuffer.get(contentBytes);

//如果是zip包就进行解包

if(Version.ZIP == protVer){

unpack(ByteBuffer.wrap(ZipUtil.unZlib(contentBytes)));

return;

}

String content = new String(contentBytes, StandardCharsets.UTF_8);

if(Opt.AUTH_REPLY == optCode){

//返回{"code":0}表示成功

System.out.println("这是鉴权回复:"+content);

}

//真正的弹幕消息

if(Opt.SEND_SMS_REPLY == optCode){

System.out.println("真正的弹幕消息:"+content);

// todo 自定义处理

}

//只存在ZIP包解压时才有的情况

//如果byteBuffer游标 小于 byteBuffer大小,那就证明还有数据

if(byteBuffer.position() < byteBuffer.limit()){

unpack(byteBuffer);

}

}

}

3、使用

public class App {

public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeyException, URISyntaxException, DeploymentException {

openLiveRoom(直播间ID, 你的Cookie);

}

public static void openLiveRoom(int roomId,String cookie) throws IOException, NoSuchAlgorithmException, InvalidKeyException, URISyntaxException, DeploymentException {

BiliRequest biliReqest = new BiliRequest(cookie);

JSONObject danmuInfoData = biliReqest.getDanmuInfoData(roomId);

//登录Token

String token = danmuInfoData.getString("token");

//选一个服务器节点

JSONArray hostList = danmuInfoData.getJSONArray("host_list");

JSONObject host = hostList.getJSONObject(0);

String wsUrl = String.format("ws://%s:%s/sub", host.getString("host"), host.getString("ws_port"));

//创建Websocket并连接

WebSocketContainer container = ContainerProvider.getWebSocketContainer();

container.connectToServer(new WebsocketListener(cookie, roomId, token), new URI(wsUrl)); // 连接到WebSocket服务器

}

}

精彩链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。