【JAVA版本】websocket获取B站直播弹幕——基于直播开放平台
教程
B站直播间弹幕Websocket获取 — 哔哩哔哩直播开放平台
基于B站直播开放平台开放且未上架时,只能个人使用。
代码实现
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>
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.0con.setRequestProperty("x-bili-signature-version", "1.0");//必填:签名唯一随机数。用于防止网络重放攻击,建议您每一次请求都使用不同的随机数con.setRequestProperty(KEY_SIGNATURE_NONCE, UUID.randomUUID().toString());//必填:加密算法con.setRequestProperty("x-bili-signature-method", "HMAC-SHA256");//必填: accesskey idcon.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();}
}
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;}@OnOpenpublic 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);}@OnMessagepublic void onMessage(ByteBuffer byteBuffer) {//解包unpack(byteBuffer);}@OnClosepublic void onClose(Session session, CloseReason closeReason) {System.out.println("关闭Websocket服务: " + closeReason);}@OnErrorpublic 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的长度,固定为16stream.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);}}
}
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));
}
参数获取 | |
---|---|
Access_key 和 Access_Secret | 去B站直播开放平台注册申请个人开发者后就能获得 |
应用ID | 成为个人开发者后,在直播开放平台创建应用后,就能获得应用ID |
身份码 | 登录B站直播间找到幻星-互动玩法,在里面就能找到身份码 |
其他版本
【flutter / dart 版本】Websocket获取B站直播间弹幕教程——基于B站直播开发平台
相关文章:
【JAVA版本】websocket获取B站直播弹幕——基于直播开放平台
教程 B站直播间弹幕Websocket获取 — 哔哩哔哩直播开放平台 基于B站直播开放平台开放且未上架时,只能个人使用。 代码实现 1、相关依赖 fastjson2用于解析JSON字符串,可自行替换成别的框架。 hutool-core用于解压zip数据,可自行替换成别的…...

Vue中的监视属性
一、监视属性的使用 (一)配置watch进行监视 当我们想要监视一个属性改变的时候就可以使用监视属性监视其变化并进行操作。 语法格式如下: watch:{ 监视属性名称 : { // 监视属性的配置项 } } 1. handler函数 当监视的属性发生变化时就调…...

汽车一键启动点火开关按键一键启动按钮型号规格
汽车点火开关/移动管家一键启动按键/汽车改装引擎启动按钮型号:YD828溥款开关 一键启动按钮(适用于配套启动主机使用或原车一键启动开关更换) 1.适合配套专用板板安装 2.开孔器开孔安装 3.原车钥匙位安装 外观:黑色 按钮上有3种不…...

快速学习微服务保护框架--Sentinel
学习一个框架最好的方式就是查看官方地址,sentinel是国内阿里巴巴公司的,官网更方便官网 官网 微服务保护框架 Sentinel 1.初识Sentinel 1.1.雪崩问题及解决方案 1.1.1.雪崩问题 微服务中,服务间调用关系错综复杂,一个微服务往往依赖于多个其它微…...

bootz启动 Linux内核过程总结
一. bootz启动Linux uboot 启动 Linux内核使用bootz命令。当然还有其它的启动命令,例如,bootm命令等等。 前面几篇文章分析 bootz命令启动 Linux内核的过程中涉及的几个重要函数。 bootz启动 Linux内核过程中涉及的全局变量images_凌肖战的博客-CSDN博…...

前端项目--尚医通学习分享
这段时间跟着线上课程完成了一个项目:商医通(基于Vue3TypeScript的医院挂号平台)。具体我就不过多地介绍其具体功能以及详细的实现步骤了,感兴趣的小伙伴直接:传送门 。该文章我就分享一下在该项目中学习到的一些知识点…...

【Python】QTreeWidget树形结构添加
源码: # 参考网址: https://blog.csdn.net/weixin_42286052/article/details/129532631 import os.path import sys from PySide6.QtWidgets import QApplication,QMainWindow,QHBoxLayout,QVBoxLayout,QPushButton,QTreeWidget,QTreeWidgetItem,QTreeW…...
day 2 2.3.2 类和对象
具有相同或相似性质的一组对象的抽象就是类 null只能被转换成引用类型,不能转换成基本类型,因此不要把一个null值赋给基本数据类型的变量 Java语言支持的类型分为两类:基本类型和引用类型 基本类型包括boolean类型和数值类型。数值类型有整…...

vscode虚拟环境使用jupyter
在某虚拟环境内安装torch,但是ipyn文件保存后无法正常导入torch 1.conda环境下安装Jupyter等一切配置,进入虚拟环境 2.conda install nb_conda_kernels 3.安装完成后重新打开VSCode,在运行Jupyter notebook中的代码之前,在右上…...

Maven 依赖管理
Maven 一个核心的特性就是依赖管理。当我们处理多模块的项目(包含成百上千个模块或者子项目),模块间的依赖关系就变得非常复杂,管理也变得很困难。针对此种情形,Maven 提供了一种高度控制的方法。 可传递性依赖发现 …...

【踩坑】hive脚本笛卡尔积严重降低查询效率问题
前一阵子查看我们公司的大数据平台的离线脚本运行情况, 结果发现有一个任务居然跑了一天多, 要知道这还只是几千万量级的表, 且这个任务是每天需要执行的 于是我把hive脚本捞出来看了下, 发现无非多join了几个复杂的子查询, 应该不至于这么久, 包括我又检查了是不是没有加上每…...

【C进阶】内存函数
strcpy拷贝的仅仅是字符串,但是内存中的数据不仅仅是字符,所以就有了memcpy函数 1. memcpy void *memcpy (void * destination ,const void * source , size_t num) 函数memcpy从source的位置开始向后拷贝num个字节的数据到desti…...
h2database BTree 设计实现与查询优化思考
h2database 是使用Java 编写的开源数据库,兼容ANSI-SQL89。 即实现了常规基于 BTree 的存储引擎,又支持日志结构存储引擎。功能非常丰富(死锁检测机制、事务特性、MVCC、运维工具等),数据库学习非常好的案例。 本文理论…...
Linux命令(100)之sz
linux命令之sz 1.sz介绍 linux命令sz是用来把文件从Linux平台下载到Windows上 2.sz用法 sz [参数] file sz参数 参数说明-b使用binary的方式下载,不解释字符为ascii-y相同文件名,覆盖-E相同文件名,不会将其覆盖,而是会在所上传…...
Insight h2database SQL like 查询
我们认为的 SQL like 查询和优化技巧,设计的初衷和真正的实现原理是什么。 在 h2database SQL like 查询实现类中(CompareLike),可以看到 SQL 语言到具体执行的实现、也可以看到数据库尝试优化语句的过程,以及查询优化…...
wpf中listview内容居中显示
在WPF中使用ListView经常会用到GridView作为视图,但是却碰到GridViewColumn不能居中对齐的问题, 实现方法 给ListViewItem设置Style,让ListViewItem在水平方向拉伸填充: <Setter Property"HorizontalContentAlignment" Value&…...

第二章 C++的输出
系列文章目录 第一章 C的输入 文章目录 系列文章目录前言一、个人名片二、cout三、printf总结 前言 今天来学C的输出吧! 一、个人名片 二、cout cout 三、printf printf 总结 最近懒得写博客怎么办?...

Qt中常用容器组控件介绍和实操
目录 常用容器组控件(Containers): 1.Group Box 2.Scroll Area 3.Tab Widget 4.Frame 5.Dock Widget 常用容器组控件(Containers): 控件名称依次解释如下(常用的用红色标出): Group Box: 组合框: 提供带有标题的组合框框架Scroll Area…...

kafka、rabbitmq 、rocketmq的区别
一、语言不同 RabbitMQ是由内在高并发的erlanng语言开发,用在实时的对可靠性要求比较高的消息传递上。 kafka是采用Scala语言开发,它主要用于处理活跃的流式数据,大数据量的数据处理上 RocketMQ是采用java语言开发的 二、吞吐量 kafka吞吐量更高&…...
java的amazonaws接口出现无法执行http请求:管道中断
java使用amazonaws的接口上传文件到minio出现以下异常: com.amazonaws.SdkClientException: Unable to execute HTTP request: Broken pipe (Write failed) at com.amazonaws.http.AmazonHttpClient R e q u e s t E x e c u t o r . h a n d l e R e t r y a b l e…...

XCTF-web-easyupload
试了试php,php7,pht,phtml等,都没有用 尝试.user.ini 抓包修改将.user.ini修改为jpg图片 在上传一个123.jpg 用蚁剑连接,得到flag...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...

《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...

AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...

技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...