• 【JAVA版本】websocket获取B站直播弹幕——基于直播开放平台


    教程

    B站直播间弹幕Websocket获取 — 哔哩哔哩直播开放平台
    基于B站直播开放平台开放且未上架时,只能个人使用。

    代码实现

    1、相关依赖

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

    <dependency>
        <groupId>com.alibaba.fastjson2groupId>
        <artifactId>fastjson2artifactId>
        <version>2.0.40version>
    dependency>
    
    <dependency>
        <groupId>cn.hutoolgroupId>
        <artifactId>hutool-coreartifactId>
        <version>5.8.21version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    1、新建ProjectRequest.java

    用于发送项目start、end、heartbeat请求。
    注意:
    没有上架的项目,start返回结果没有场次ID,导致end、heartbeat请求不能正常执行。
    但是没有关系,start能获得弹幕服务信息就行。

    import com.alibaba.fastjson2.JSONArray;
    import com.alibaba.fastjson2.JSONObject;
    import jakarta.annotation.Nonnull;
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.io.BufferedReader;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.nio.charset.StandardCharsets;
    import java.security.InvalidKeyException;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.UUID;
    import java.util.stream.Collectors;
    
    public class ProjectRequest {
        /**
         * 项目ID
         */
        private long appId;
        /**
         * 身份验证Key
         */
        private String accessKey;
        /**
         * 身份验证密钥
         */
        private String accessSecret;
    
        public ProjectRequest(long appId, String accessKey, String accessSecret) {
            this.appId = appId;
            this.accessKey = accessKey;
            this.accessSecret = accessSecret;
        }
    
        public final static String START_URL = "https://live-open.biliapi.com/v2/app/start";
        public final static String END_URL = "https://live-open.biliapi.com/v2/app/end";
        public final static String HEART_BEAT_URL = "https://live-open.biliapi.com/v2/app/heartbeat";
        public final static String BATCH_HEART_BEAT_URL = "https://live-open.biliapi.com/v2/app/batchHeartbeat";
    
        /**
         * 接口描述:开启项目第一步,平台会根据入参进行鉴权校验。鉴权通过后,返回长连信息、场次信息和主播信息。开发者拿到长连和心跳信息后,需要参照[长连说明]和[项目心跳],与平台保持健康的
         * @param code 必填	string	[主播身份码]
         * param appId 必填	integer(13位长度的数值,注意不要用普通int,会溢出的)	项目ID
         */
        public String start(String code) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
            Map<String,Object> params = new HashMap<>();
            params.put("code", code);
            params.put("app_id", appId);
            return post(START_URL, params);
        }
        /**
         * 接口描述:项目关闭时需要主动调用此接口,使用对应项目Id及项目开启时返回的game_id作为唯一标识,调用后会同步下线互动道具等内容,项目关闭后才能进行下一场次互动。
         * param appId 必填	integer(13位长度的数值,注意不要用普通int,会溢出的)	项目ID
         * param gameId 必填	场次id
         */
        public String end(String gameId) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
            Map<String,Object> params = new HashMap<>();
            params.put("game_id", gameId);
            params.put("app_id", appId);
            return post(END_URL, params);
        }
        /**
         * 接口描述:项目开启后,需要持续间隔20秒调用一次该接口。平台超过60s未收到项目心跳,会自动关闭当前场次(game_id),同时将道具相关功能下线,以确保下一场次项目正常运行。
         * 接口地址:/v2/app/heartbeat
         * 方法:POST
         * param gameId 必填	场次id
         */
        public String heartbeat(String gameId) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
            Map<String,Object> params = new HashMap<>();
            params.put("game_id", gameId);
            return post(HEART_BEAT_URL, params);
        }
        /**
         * 项目批量心跳
         * 接口地址:/v2/app/batchHeartbeat
         * 方法:POST
         * @param gameIds    必填	[]string	场次id
         * */
        public String batchHeartbeat(@Nonnull List<String> gameIds) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
            Map<String,Object> params = new HashMap<>();
            params.put("game_ids", JSONArray.toJSONString(gameIds));
            return post(HEART_BEAT_URL, params);
        }
    
    
        /**
         * 自定义post请求
         * @param url
         * @param dataMap
         * @throws IOException
         * @throws NoSuchAlgorithmException
         * @throws InvalidKeyException
         */
        private String post(String url, Map<String,Object> dataMap) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
            String bodyStr = JSONObject.toJSONString(dataMap);
            HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
            con.setRequestMethod("POST");
            // 设置请求头
            setHeader(con, bodyStr);
            // 发送 POST 请求
            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"));
            }
        }
    
        public static String KEY_CONTENT_MD5 = "x-bili-content-md5";
        public static String KEY_TIMESTAMP = "x-bili-timestamp";
        public static String KEY_SIGNATURE_NONCE = "x-bili-signature-nonce";
    
        /**
         * 设置请求头
         * @param con
         * @param bodyStr 请求体
         * @throws NoSuchAlgorithmException
         * @throws InvalidKeyException
         */
        private void setHeader(HttpURLConnection con,String bodyStr) throws NoSuchAlgorithmException, InvalidKeyException {
            con.setRequestProperty("User-Agent", "Mozilla/5.0");
            /**----------------------------------------------------------------------------**/
            //必填:接受的返回结果的类型。目前只支持JSON类型,取值:application/json。
            con.setRequestProperty("Accept", "application/json");
            //必填:当前请求体(Request Body)的数据类型。目前只支持JSON类型,取值:application/json。
            con.setRequestProperty("Content-Type", "application/json");
            //必填:请求体的编码值,根据请求体计算所得。算法说明:将请求体内容当作字符串进行MD5编码。
            con.setRequestProperty(KEY_CONTENT_MD5, getContentMd5(bodyStr));
            //必填:unix时间戳,单位是秒。请求时间戳不能超过当前时间10分钟,否则请求会被丢弃。
            con.setRequestProperty(KEY_TIMESTAMP, String.valueOf(System.currentTimeMillis()/1000));
            //必填: 版本1.0
            con.setRequestProperty("x-bili-signature-version", "1.0");
            //必填:签名唯一随机数。用于防止网络重放攻击,建议您每一次请求都使用不同的随机数
            con.setRequestProperty(KEY_SIGNATURE_NONCE, UUID.randomUUID().toString());
            //必填:加密算法
            con.setRequestProperty("x-bili-signature-method", "HMAC-SHA256");
            //必填: accesskey id
            con.setRequestProperty("x-bili-accesskeyid", accessKey);
            //必填:请求签名(注意生成的签名是小写的)。关于请求签名的计算方法,请参见签名机制
            con.setRequestProperty("Authorization", generateSignature(con));
        }
    
        /**
         * MD5计算
         */
        private String getContentMd5(String content) throws NoSuchAlgorithmException {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            return byte2Hex( md5.digest(content.getBytes(StandardCharsets.UTF_8)) );
        }
    
        /**
         * 签名 HmacSHA256计算
         */
        public String generateSignature(HttpURLConnection con) throws NoSuchAlgorithmException, InvalidKeyException {
            StringBuilder s = new StringBuilder();
            s.append("x-bili-accesskeyid:").append(accessKey).append("\n");
            s.append("x-bili-content-md5:").append(con.getRequestProperty(KEY_CONTENT_MD5)).append("\n");
            s.append("x-bili-signature-method:").append("HMAC-SHA256").append("\n");
            s.append("x-bili-signature-nonce:").append(con.getRequestProperty(KEY_SIGNATURE_NONCE)).append("\n");
            s.append("x-bili-signature-version:").append("1.0").append("\n");
            s.append("x-bili-timestamp:").append(con.getRequestProperty(KEY_TIMESTAMP));
            byte[] headerByte = s.toString().getBytes(StandardCharsets.UTF_8);
            byte[] secretByte = accessSecret.getBytes(StandardCharsets.UTF_8);
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secretByte, "HmacSHA256"));
            byte[] bytes = mac.doFinal(headerByte);
            return byte2Hex(bytes);
        }
    
        /**
         * 字节数组转16进制字符串
         * @param bytes
         * @return
         */
        private static String byte2Hex(byte[] bytes){
            StringBuffer stringBuffer = new StringBuffer();
            String temp = null;
            for (int i=0;i<bytes.length;i++){
                temp = Integer.toHexString(bytes[i] & 0xFF);
                if (temp.length()==1){
                    //1得到一位的进行补0操作
                    stringBuffer.append("0");
                }
                stringBuffer.append(temp);
            }
            return stringBuffer.toString();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200

    3、新建 WebsocketListener.java

    用于监听接收到的数据。

    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.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    import cn.hutool.core.util.ZipUtil;
    
    @ClientEndpoint
    public class WebsocketListener {
    	private Session session;
        private String authBody;
    
        public WebsocketListener(String authBody) {
            this.authBody = authBody;
        }
        @OnOpen
        public void onOpen(Session session) throws IOException {
            System.out.println("已连接服务...");
            this.session = session;
            RemoteEndpoint.Async remote = session.getAsyncRemote();
            //鉴权协议包
            ByteBuffer authPack = ByteBuffer.wrap(generateAuthPack(authBody));
            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("关闭Websocket服务: " + closeReason);
        }
    
        @OnError
        public void onError(Session session, Throwable t) {
            System.out.println("Websocket服务异常: " + t.getMessage());
        }
    
        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);//保留字段,可以忽略。
                if(Opt.AUTH == code){
                    stream.writeBytes(jsonStr);
                }
                return data.toByteArray();
            }
        }
    
    
        /**
         * 生成认证包
         * @return
         */
        public static byte[] generateAuthPack(String jsonStr) throws IOException {
            return pack(jsonStr, 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);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152

    4、使用

    public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeyException, URISyntaxException, DeploymentException {
    	ProjectRequest p = new ProjectRequest(你的应用ID, 你的Access_key, 你的 Access_Secret);
    	//获取弹幕服务信息
    	String result = p.start(你的身份码);
    	JSONObject data = JSONObject.parseObject(result).getJSONObject("data");
    	//个人信息
    	JSONObject anchorInfo = data.getJSONObject("anchor_info");
    	//弹幕服务器信息
    	JSONObject websocketInfo = data.getJSONObject("websocket_info");
    	//弹幕服务器地址
    	JSONArray wssLinks = websocketInfo.getJSONArray("wss_link");
    	//websocket鉴权信息
    	String authBody = websocketInfo.getString("auth_body");
    	//选一个服务器节点
    	String uri = wssLinks.getString(0);
    	WebSocketContainer container = ContainerProvider.getWebSocketContainer();
    	// 连接到WebSocket服务器
    	container.connectToServer(new WebsocketListener(authBody), new URI(uri)); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    参数获取
    Access_key 和 Access_SecretB站直播开放平台注册申请个人开发者后就能获得
    应用ID成为个人开发者后,在直播开放平台创建应用后,就能获得应用ID
    身份码登录B站直播间找到幻星-互动玩法,在里面就能找到身份码

    其他版本

    【flutter / dart 版本】Websocket获取B站直播间弹幕教程——基于B站直播开发平台

  • 相关阅读:
    苹果Ios系统app应用程序开发者如何获取IPA文件签名证书时需要注意什么?
    Kotlin协程:Flow基础原理
    Linux 必知必会
    hmcl_HMCL安装与使用
    10 【异步组件 组合式函数(hooks)】
    java-php-python--损失赔偿保险的客户情况登记及管理-计算机毕业设计
    容器编排工具之Kubernetes -- k8s
    SQL 与 Pandas 数据查询和操作对比
    Unity3D将比较简单的字典结构封装为json
    [CSCCTF 2019 Qual]FlaskLight 过滤 url_for globals 绕过globals过滤
  • 原文地址:https://blog.csdn.net/malu_record/article/details/133667361