一、教程
如果只想要代码实现,直接看第二部分。
1、相关依赖
fastjson2用于解析JSON字符串,可自行替换成别的框架。
hutool-core用于解压zip数据,可自行替换成别的框架。
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.40</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-core -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.21</version>
</dependency>
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方法。
连接成功后,需要做两件事:文章来源:https://www.toymoban.com/news/detail-826222.html
- 发送鉴权包
- 发送维持服务连接的心跳包
@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方法,接收服务器消息
需要调用上面的解包方法。文章来源地址https://www.toymoban.com/news/detail-826222.html
@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<String,Object> 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服务器
}
}
到了这里,关于[JAVA版本] 最新websocket获取B站直播弹幕——非官方API的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!