├── public └── favicon.ico ├── .vscode └── extensions.json ├── src ├── views │ ├── HomeView.vue │ └── AboutView.vue ├── main.js ├── assets │ ├── logo.svg │ └── base.css ├── components │ ├── icons │ │ ├── IconSupport.vue │ │ ├── IconTooling.vue │ │ ├── IconCommunity.vue │ │ ├── IconDocumentation.vue │ │ └── IconEcosystem.vue │ ├── HelloWorld.vue │ ├── WelcomeItem.vue │ └── TheWelcome.vue ├── stores │ └── counter.js ├── router │ └── index.js └── App.vue ├── .eslintrc.cjs ├── index.html ├── .gitignore ├── vite.config.js ├── package.json ├── LICENSE └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/79W/bilibili-bullet/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | 4 | import App from './App.vue' 5 | import router from './router' 6 | 7 | const app = createApp(App) 8 | 9 | app.use(createPinia()) 10 | app.use(router) 11 | 12 | app.mount('#app') 13 | -------------------------------------------------------------------------------- /src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | "root": true, 6 | "extends": [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-prettier" 10 | ], 11 | "env": { 12 | "vue/setup-compiler-macros": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/stores/counter.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useCounterStore = defineStore({ 4 | id: 'counter', 5 | state: () => ({ 6 | counter: 0 7 | }), 8 | getters: { 9 | doubleCount: (state) => state.counter * 2 10 | }, 11 | actions: { 12 | increment() { 13 | this.counter++ 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import HomeView from '../views/HomeView.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'home', 10 | component: HomeView 11 | }, 12 | { 13 | path: '/about', 14 | name: 'about', 15 | // route level code-splitting 16 | // this generates a separate chunk (About.[hash].js) for this route 17 | // which is lazy-loaded when the route is visited. 18 | component: () => import('../views/AboutView.vue') 19 | } 20 | ] 21 | }) 22 | 23 | export default router 24 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "url"; 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | import vueJsx from "@vitejs/plugin-vue-jsx"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [vue(), vueJsx()], 10 | resolve: { 11 | alias: { 12 | "@": fileURLToPath(new URL("./src", import.meta.url)), 13 | }, 14 | }, 15 | server: { 16 | host: "0.0.0.0", 17 | port: 3001, 18 | open: true, 19 | hmr: true, 20 | proxy: { 21 | "/api": { 22 | target: "https://api.live.bilibili.com", 23 | changeOrigin: true, 24 | rewrite: (path) => path.replace(/^\/api/, ""), 25 | }, 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bzsocket", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview --port 5050", 8 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.26.1", 12 | "less": "^4.1.2", 13 | "less-loader": "^10.2.0", 14 | "pako": "^2.0.4", 15 | "pinia": "^2.0.13", 16 | "vue": "^3.2.33", 17 | "vue-router": "^4.0.14" 18 | }, 19 | "devDependencies": { 20 | "@rushstack/eslint-patch": "^1.1.0", 21 | "@vitejs/plugin-vue": "^2.3.1", 22 | "@vitejs/plugin-vue-jsx": "^1.3.10", 23 | "@vue/eslint-config-prettier": "^7.0.0", 24 | "eslint": "^8.5.0", 25 | "eslint-plugin-vue": "^8.2.0", 26 | "prettier": "^2.5.1", 27 | "vite": "^2.9.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 44 | -------------------------------------------------------------------------------- /src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 我是一只虫子 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /src/components/TheWelcome.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 85 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 238 | 239 | 245 | 259 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 需求:实时获取直播间的弹幕 2 | 3 | 我们打开一个B站的直播间 4 | 5 | ```javascript 6 | https://live.bilibili.com/23423267?session_id=6db8b01d511cae96d90825a02161af46_E97F63BD-7769-40B1-8239-2CFC7294000A&launch_id=1000218&hotRank=0&spm_id_from=333.1007.partition_recommend.content.click 7 | ``` 8 | 9 | 然后我们删除 `?`即以后的所有内容 就剩下这个链接了 10 | 11 | ```javascript 12 | https://live.bilibili.com/23423267 13 | ``` 14 | 15 | 这里可以看到一串数字 这并不是真实的 直播间ID 16 | 17 | #### 获取真实直播间ID 18 | 19 | ```javascript 20 | // 注意ID 21 | https://api.live.bilibili.com/room/v1/Room/room_init?id=23423267 22 | ``` 23 | 24 | 这里需要填写个ID 这就是 网页URL 上面的数字 25 | 26 | ```javascript 27 | async function getRoomId(id) { 28 | const res = await Axios.get(`https://api.live.bilibili.com/room/v1/Room/room_init?id=${id}`); 29 | return res.data; 30 | } 31 | ``` 32 | 33 | 返回值 34 | 35 | ```javascript 36 | { 37 | "code": 0, 38 | "msg": "ok", 39 | "message": "ok", 40 | "data": { 41 | "room_id": 23423267, 42 | "short_id": 0, 43 | "uid": 431856380, 44 | "need_p2p": 0, 45 | "is_hidden": false, 46 | "is_locked": false, 47 | "is_portrait": false, 48 | "live_status": 1, 49 | "hidden_till": 0, 50 | "lock_till": 0, 51 | "encrypted": false, 52 | "pwd_verified": false, 53 | "live_time": 1650773392, 54 | "room_shield": 0, 55 | "is_sp": 0, 56 | "special_type": 0 57 | } 58 | } 59 | ``` 60 | 61 | 这里数据会反回真实的直播间ID 也就是:`room_id` 62 | 63 | #### 获取WebSocket地址 64 | 65 | 我们有了真实的房间ID就需要获取 Socket 交互的地址 66 | 67 | ```javascript 68 | // roomid ===》 room_id 69 | https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id=${roomid}&platform=pc&player=web 70 | ``` 71 | 72 | 写个方法然后方便进行调用 73 | 74 | ```javascript 75 | async function getWebSocketHost(roomid) { 76 | const res = await Axios.get( 77 | `https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id=${roomid}&platform=pc&player=web` 78 | ); 79 | return res.data; 80 | } 81 | ``` 82 | 83 | 返回值 84 | 85 | ```javascript 86 | { 87 | "code": 0, 88 | "msg": "ok", 89 | "message": "ok", 90 | "data": { 91 | "refresh_row_factor": 0.125, 92 | "refresh_rate": 100, 93 | "max_delay": 5000, 94 | "port": 2243, 95 | "host": "broadcastlv.chat.bilibili.com", 96 | "host_server_list": [ 97 | { 98 | "host": "hw-sh-live-comet-01.chat.bilibili.com", 99 | "port": 2243, 100 | "wss_port": 443, 101 | "ws_port": 2244 102 | }, 103 | { 104 | "host": "tx-bj-live-comet-04.chat.bilibili.com", 105 | "port": 2243, 106 | "wss_port": 443, 107 | "ws_port": 2244 108 | }, 109 | { 110 | "host": "broadcastlv.chat.bilibili.com", 111 | "port": 2243, 112 | "wss_port": 443, 113 | "ws_port": 2244 114 | } 115 | ], 116 | "server_list": [ 117 | { 118 | "host": "119.3.126.45", 119 | "port": 2243 120 | }, 121 | { 122 | "host": "49.232.64.111", 123 | "port": 2243 124 | }, 125 | { 126 | "host": "broadcastlv.chat.bilibili.com", 127 | "port": 2243 128 | }, 129 | { 130 | "host": "119.3.126.45", 131 | "port": 80 132 | }, 133 | { 134 | "host": "49.232.64.111", 135 | "port": 80 136 | }, 137 | { 138 | "host": "broadcastlv.chat.bilibili.com", 139 | "port": 80 140 | } 141 | ], 142 | "token": "_xRKFgXU73NHsSrvh4jAKiQ8CRFZMqao1r6xarlMgqNppUZRbmjVK-VcYxU1AgoPKkJ7ruT27X9AwpVsM8F92xux8jKu4Nlm6tfOYyUCsuGSvxLRD1damLw5GpeRewYjXCVfK7ylDwCHhyisaaII0IRIdw==" 143 | } 144 | } 145 | ``` 146 | 147 | 我们直接使用 `host`字段就可以 148 | 149 | 这里需要注意 链接webSocket需要进行拼接 在B站直播间进行`F12`查看 150 | 151 | ![image-20220424125640854](https://cdn.jsdelivr.net/gh/duogongneng/OneMyBlogImg@master/image-20220424125640854.png) 152 | 153 | 最后我们的WebSocket链接拼接成 154 | 155 | ``` 156 | wss://broadcastlv.chat.bilibili.com/sub 157 | ``` 158 | 159 | 然后我们就可以进行连接了 我们还是写个方法 然后在这个方法里面进行处理 160 | 161 | ```js 162 | function openSocket(url, room_id) { 163 | let ws = new WebSocket(`wss://${url}/sub`); 164 | let json = { 165 | uid: 0, // 0表示未登录 166 | roomid: room_id, //上面获取到的room_id 167 | protover: 1, 168 | platform: "web", 169 | clientver: "1.4.0", 170 | }; 171 | // WebSocket连接成功回调 172 | ws.onopen = function () { 173 | console.log("WebSocket 已连接上"); 174 | ws.send(getCertification(JSON.stringify(json)).buffer); 175 | //组合认证数据包 并发送 176 | }; 177 | 178 | 179 | // WebSocket连接关闭回调 180 | ws.onclose = function () { 181 | console.log("连接已关闭"); 182 | }; 183 | } 184 | ``` 185 | 186 | **注意点:** 187 | 188 | - json 数据 这是我们需要提交进行认证使用的否则是连接不上的 189 | - 这里用到一个方法下面会进行讲解 因为b站接受的数据需要进行封包处理才能连接 190 | - 还需要一个奖字符串转 bytes 191 | 192 | #### 封包格式 193 | 194 | 封包由头部和数据组成,**字节序均为大端模式** 195 | 196 | 头部格式: 197 | 198 | | 偏移量 | 长度 | 含义 | 199 | | ------ | ---- | --------------------- | 200 | | 0 | 4 | 封包总大小 | 201 | | 4 | 2 | 头部长度 | 202 | | 6 | 2 | 协议版本,目前是1 | 203 | | 8 | 4 | 操作码(封包类型) | 204 | | 12 | 4 | sequence,可以取常数1 | 205 | 206 | 已知的操作码: 207 | 208 | | 操作码 | 含义 | 209 | | ------ | --------------------------------- | 210 | | 2 | 客户端发送的心跳包 | 211 | | 3 | 人气值,数据不是JSON,是4字节整数 | 212 | | 5 | 命令,数据中`['cmd']`表示具体命令 | 213 | | 7 | 认证并加入房间 | 214 | | 8 | 服务器发送的心跳包 | 215 | 216 | 数据格式:一般为JSON字符串UTF-8编码 217 | 218 | ```js 219 | //组合认证数据包 220 | function getCertification(json) { 221 | var bytes = str2bytes(json); //字符串转bytes 222 | var n1 = new ArrayBuffer(bytes.length + 16); 223 | var i = new DataView(n1); 224 | i.setUint32(0, bytes.length + 16), //封包总大小 225 | i.setUint16(4, 16), //头部长度 226 | i.setUint16(6, 1), //协议版本 227 | i.setUint32(8, 7), //操作码 7表示认证并加入房间 228 | i.setUint32(12, 1); //就1 229 | for (var r = 0; r < bytes.length; r++) { 230 | i.setUint8(16 + r, bytes[r]); //把要认证的数据添加进去 231 | } 232 | return i; //返回 233 | } 234 | ``` 235 | 236 | **JavaScript DataView** 237 | 238 | DataView提供了用于在ArrayBuffer中读取和写入多种数字类型的低级接口。 239 | 240 | 让我们看看JavaScript的列表< strong> DataView 方法及其说明。 241 | 242 | | 方法 | 说明 | 243 | | --------------------- | ------------------------------------------------------------ | 244 | | DataView.getFloat32() | DataView.getFloat32()方法用于在指定位置获取32位浮点数。 | 245 | | DataView.getFloat64() | DataView.getFloat64()方法用于在指定位置获取64位float(double)数字。 | 246 | | DataView.getInt16() | DataView.getInt16()方法用于在指定位置获取带符号的16位整数(短)数字。 | 247 | | Dataview.getInt32() | DataView.getInt32()方法用于在指定位置获取带符号的32位整数(长整数)。 | 248 | | DataView.getInt8() | DataView.getInt8()方法用于在指定位置获取带符号的8位整数(字节)数字。 | 249 | | DataView.getUint16() | DataView.getUint16()方法用于在指定位置获取无符号的16位整数(无符号的短整数)。 | 250 | | DataView.getUint32() | DataView.getUint32()方法用于在指定位置获取无符号的32位整数(无符号长整数)。 | 251 | | DataView.getUint8() | DataView.getUint8()方法用于在指定位置获取无符号的8位整数(无符号字节)数字。 | 252 | 253 | ```js 254 | //字符串转bytes //这个方法是从网上找的QAQ 255 | function str2bytes(str) { 256 | const bytes = []; 257 | let c; 258 | const len = str.length; 259 | for (let i = 0; i < len; i++) { 260 | c = str.charCodeAt(i); 261 | if (c >= 0x010000 && c <= 0x10ffff) { 262 | bytes.push(((c >> 18) & 0x07) | 0xf0); 263 | bytes.push(((c >> 12) & 0x3f) | 0x80); 264 | bytes.push(((c >> 6) & 0x3f) | 0x80); 265 | bytes.push((c & 0x3f) | 0x80); 266 | } else if (c >= 0x000800 && c <= 0x00ffff) { 267 | bytes.push(((c >> 12) & 0x0f) | 0xe0); 268 | bytes.push(((c >> 6) & 0x3f) | 0x80); 269 | bytes.push((c & 0x3f) | 0x80); 270 | } else if (c >= 0x000080 && c <= 0x0007ff) { 271 | bytes.push(((c >> 6) & 0x1f) | 0xc0); 272 | bytes.push((c & 0x3f) | 0x80); 273 | } else { 274 | bytes.push(c & 0xff); 275 | } 276 | } 277 | return bytes; 278 | } 279 | ``` 280 | 281 | 上面这两个方法是进行认证使用也就是进行连接这一步 就算完成了 282 | 283 | 链接上不算完成 B站的心跳是30秒一次也就是 284 | 285 | 我们可以在 连接的时候写个定时器 每30秒进行连接一次 然后在连接关闭的时候进行 清除定时器 286 | 287 | 我们来改造 `openSocket` 方法 288 | 289 | ```js 290 | function openSocket(url, room_id) { 291 | let timer = null 292 | let ws = new WebSocket(`wss://${url}/sub`); 293 | let json = { 294 | uid: 0, // 0表示未登录 295 | roomid: room_id, //上面获取到的room_id 296 | protover: 1, 297 | platform: "web", 298 | clientver: "1.4.0", 299 | }; 300 | // WebSocket连接成功回调 301 | ws.onopen = function () { 302 | console.log("WebSocket 已连接上"); 303 | ws.send(getCertification(JSON.stringify(json)).buffer); 304 | //组合认证数据包 并发送 305 | 306 | //心跳包的定时器 307 | timer = setInterval(function () { 308 | //定时器 注意声明timer变量 309 | var n1 = new ArrayBuffer(16); 310 | var i = new DataView(n1); 311 | i.setUint32(0, 0), //封包总大小 312 | i.setUint16(4, 16), //头部长度 313 | i.setUint16(6, 1), //协议版本 314 | i.setUint32(8, 2), // 操作码 2 心跳包 315 | i.setUint32(12, 1); //就1 316 | ws.send(i.buffer); //发送 317 | }, 30000); 318 | }; 319 | 320 | 321 | // WebSocket连接关闭回调 322 | ws.onclose = function () { 323 | console.log("连接已关闭"); 324 | if (timer != null) clearInterval(timer); 325 | }; 326 | } 327 | ``` 328 | 329 | 这连接就算真正的完事了 330 | 331 | 那么接下来就是获取消息了 332 | 333 | 这个应该是放在 `openSocket` 方法里面的 334 | 335 | ```js 336 | //WebSocket接收数据回调 337 | ws.onmessage = function (evt) { 338 | var blob = evt.data; 339 | //对数据进行解码 decode方法 340 | decode(blob, function (packet) { 341 | 342 | } 343 | ); 344 | }; 345 | ``` 346 | 347 | 我们可以看下控制台数据 348 | 349 | ![image-20220424132518466](https://cdn.jsdelivr.net/gh/duogongneng/OneMyBlogImg@master/image-20220424132518466.png) 350 | 351 | 这里可以看到又一些数据是没有进行压缩的 可以直接进行使用的 352 | 353 | 但是还有一些数据是进行压缩了的 354 | 355 | ![image-20220424132603712](https://cdn.jsdelivr.net/gh/duogongneng/OneMyBlogImg@master/image-20220424132603712.png) 356 | 357 | 我们就可以对这两种情况进行分析 358 | 359 | 我们创建`decode`函数 360 | 361 | ```js 362 | /** 363 | * blob blob数据 364 | * call 回调 解析数据会通过回调返回数据 365 | */ 366 | function decode(blob, call) { 367 | // 文本解码器 368 | var textDecoder = new TextDecoder("utf-8"); 369 | let reader = new FileReader(); 370 | reader.onload = function (e) { 371 | let buffer = new Uint8Array(e.target.result); 372 | let result = {}; 373 | result.packetLen = readInt(buffer, 0, 4); 374 | result.headerLen = readInt(buffer, 4, 2); 375 | result.ver = readInt(buffer, 6, 2); 376 | result.op = readInt(buffer, 8, 4); 377 | result.seq = readInt(buffer, 12, 4); 378 | if (result.op == 5) { 379 | result.body = []; 380 | let offset = 0; 381 | while (offset < buffer.length) { 382 | let packetLen = readInt(buffer, offset + 0, 4); 383 | let headerLen = 16; // readInt(buffer,offset + 4,4) 384 | let data = buffer.slice(offset + headerLen, offset + packetLen); 385 | 386 | let body = "{}"; 387 | if (result.ver == 2) { 388 | //协议版本为 2 时 数据有进行压缩 通过pako.js 进行解压 389 | body = textDecoder.decode(pako.inflate(data)); 390 | } else { 391 | //协议版本为 0 时 数据没有进行压缩 392 | body = textDecoder.decode(data); 393 | } 394 | if (body) { 395 | // 同一条消息中可能存在多条信息,用正则筛出来 396 | // eslint-disable-next-line no-control-regex 397 | const group = body.split(/[\x00-\x1f]+/); 398 | group.forEach((item) => { 399 | try { 400 | result.body.push(JSON.parse(item)); 401 | } catch (e) { 402 | // 忽略非JSON字符串,通常情况下为分隔符 403 | } 404 | }); 405 | } 406 | offset += packetLen; 407 | } 408 | } 409 | //回调 410 | call(result); 411 | }; 412 | reader.readAsArrayBuffer(blob); 413 | } 414 | ``` 415 | 416 | 这里使用了一个方法 417 | 418 | ```js 419 | // 从buffer中读取int 420 | const readInt = function (buffer, start, len) { 421 | let result = 0; 422 | for (let i = len - 1; i >= 0; i--) { 423 | result += Math.pow(256, len - i - 1) * buffer[start + i]; 424 | } 425 | return result; 426 | }; 427 | ``` 428 | 429 | 当返回值协议是2的时候我们需要进行解压然后在返回数据 430 | 431 | 这里我们解压用到一个包是`pako.js` https://www.npmjs.com/package/pako 432 | 433 | 然后我们继续完善 `ws.onmessage` 434 | 435 | ```js 436 | //WebSocket接收数据回调 437 | ws.onmessage = function (evt) { 438 | var blob = evt.data; 439 | //对数据进行解码 decode方法 440 | decode(blob, function (packet) { 441 | //解码成功回调 442 | if (packet.op == 5) { 443 | //会同时有多个 数发过来 所以要循环 444 | for (let i = 0; i < packet.body.length; i++) { 445 | var element = packet.body[i]; 446 | //做一下简单的打印 447 | console.log(element); //数据格式从打印中就可以分析出来啦 448 | //cmd = DANMU_MSG 是弹幕 449 | if (element.cmd == "DANMU_MSG") { 450 | console.log( 451 | "uid: " + 452 | element.info[2][0] + 453 | " 用户: " + 454 | element.info[2][1] + 455 | " \n内容: " + 456 | element.info[1] 457 | ); 458 | } 459 | //cmd = INTERACT_WORD 有人进入直播了 460 | else if (element.cmd == "INTERACT_WORD") { 461 | console.log("进入直播: " + element.data.uname); 462 | } 463 | //还有其他的 464 | } 465 | } 466 | } 467 | ); 468 | }; 469 | ``` 470 | 471 | #### 命令包 472 | 473 | 根据前端代码,数据也可能是多条命令的数组,不过我只收到过单条命令。每条命令中`['cmd']`表示具体命令 474 | 475 | 已知的命令: 476 | 477 | | 命令 | 含义 | 478 | | ------------- | ---------------- | 479 | | DANMU_MSG | 收到弹幕 | 480 | | SEND_GIFT | 有人送礼 | 481 | | WELCOME | 欢迎加入房间 | 482 | | WELCOME_GUARD | 欢迎房管加入房间 | 483 | | SYS_MSG | 系统消息 | 484 | | PREPARING | 主播准备中 | 485 | | LIVE | 直播开始 | 486 | | WISH_BOTTLE | 许愿瓶? | 487 | 488 | --------------------------------------------------------------------------------