netty-websocket扩展协议及token鉴权补充

文章源码:gitee
源码部分可以看上一篇文章中的源码分析netty-websocket 鉴权token及统一请求和响应头(鉴权控制器)

最近刚好没事,看到有朋友说自定义协议好搞,我就想了想,发现上面那种方式实现确实麻烦,而且兼容性还不行,后来我对照着WebSocketServerProtocolHandler试了试扩展一下,将WebSocketServerProtocolHandler中handlerAdded添加的握手逻辑换成自己的,终于测通了,我用postman测试时,请求头也可以自定义,下面上代码

1.(userEventTriggered): 鉴权成功后可以抛出自定义事件,业务channel中实现 事件监听器userEventTriggered,这样就可以在鉴权成功后,握手成功前执行某个方法,比如验证权限啥的,具体可看SecurityHandler中的例子
2. (exceptionCaught): 异常捕获
3. channel设置attr实现channel上下文的数据属性
4. …等等

扩展WebSocketProtocolHandler

这个协议有很多私有方法外部引用不了,所以只能copy一份出来,主要是把handlerAdded这个方法重写了,将原有的‘WebSocketServerProtocolHandshakeHandler’替换为‘自己的(SecurityHandler)’

package com.chat.nettywebsocket.handler.test;import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.Utf8FrameValidator;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.util.AttributeKey;import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;/***** @author qb* @date 2024/2/5 8:53* @version 1.0*/
public class CustomWebSocketServerProtocolHandler extends WebSocketServerProtocolHandler {public enum ServerHandshakeStateEvent {/*** The Handshake was completed successfully and the channel was upgraded to websockets.** @deprecated in favor of {@link WebSocketServerProtocolHandler.HandshakeComplete} class,* it provides extra information about the handshake*/@DeprecatedHANDSHAKE_COMPLETE}/*** The Handshake was completed successfully and the channel was upgraded to websockets.*/public static final class HandshakeComplete {private final String requestUri;private final HttpHeaders requestHeaders;private final String selectedSubprotocol;HandshakeComplete(String requestUri, HttpHeaders requestHeaders, String selectedSubprotocol) {this.requestUri = requestUri;this.requestHeaders = requestHeaders;this.selectedSubprotocol = selectedSubprotocol;}public String requestUri() {return requestUri;}public HttpHeaders requestHeaders() {return requestHeaders;}public String selectedSubprotocol() {return selectedSubprotocol;}}public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) {super(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith);this.websocketPath = websocketPath;this.subprotocols = subprotocols;this.allowExtensions = allowExtensions;maxFramePayloadLength = maxFrameSize;this.allowMaskMismatch = allowMaskMismatch;this.checkStartsWith = checkStartsWith;}private final String websocketPath;private final String subprotocols;private final boolean allowExtensions;private final int maxFramePayloadLength;private final boolean allowMaskMismatch;private final boolean checkStartsWith;@Overridepublic void handlerAdded(ChannelHandlerContext ctx) {System.err.println("handlerAdded");ChannelPipeline cp = ctx.pipeline();if (cp.get(SecurityHandler.class) == null) {// Add the WebSocketHandshakeHandler before this one.// 增加协议实现handlerctx.pipeline().addBefore(ctx.name(), SecurityHandler.class.getName(),new SecurityHandler(websocketPath, subprotocols,allowExtensions, maxFramePayloadLength, allowMaskMismatch, checkStartsWith));}if (cp.get(Utf8FrameValidator.class) == null) {// Add the UFT8 checking before this one.ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(),new Utf8FrameValidator());}}private static final AttributeKey<WebSocketServerHandshaker> HANDSHAKER_ATTR_KEY =AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER");static WebSocketServerHandshaker getHandshaker(Channel channel) {return channel.attr(HANDSHAKER_ATTR_KEY).get();}static void setHandshaker(Channel channel, WebSocketServerHandshaker handshaker) {channel.attr(HANDSHAKER_ATTR_KEY).set(handshaker);}static ChannelHandler forbiddenHttpRequestResponder() {return new ChannelInboundHandlerAdapter() {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {if (msg instanceof FullHttpRequest) {((FullHttpRequest) msg).release();FullHttpResponse response =new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.FORBIDDEN);ctx.channel().writeAndFlush(response);} else {ctx.fireChannelRead(msg);}}};}}

SecurityHandler

复制的WebSocketServerProtocolHandshakeHandler的方法,就是改了请求头逻辑和发布事件的相关类调整

package com.chat.nettywebsocket.handler.test;import com.chat.nettywebsocket.handler.test.CustomWebSocketServerProtocolHandler;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.ssl.SslHandler;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;import static com.chat.nettywebsocket.handler.AttributeKeyUtils.SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;/***** @author qb* @date 2024/2/5 8:37* @version 1.0*/
@Slf4j
@ChannelHandler.Sharable
public class SecurityHandler extends ChannelInboundHandlerAdapter {private final String websocketPath;private final String subprotocols;private final boolean allowExtensions;private final int maxFramePayloadSize;private final boolean allowMaskMismatch;private final boolean checkStartsWith;SecurityHandler(String websocketPath, String subprotocols,boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) {this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false);}SecurityHandler(String websocketPath, String subprotocols,boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) {this.websocketPath = websocketPath;this.subprotocols = subprotocols;this.allowExtensions = allowExtensions;maxFramePayloadSize = maxFrameSize;this.allowMaskMismatch = allowMaskMismatch;this.checkStartsWith = checkStartsWith;}@Overridepublic void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {final FullHttpRequest req = (FullHttpRequest) msg;if (isNotWebSocketPath(req)) {ctx.fireChannelRead(msg);return;}try {if (req.method() != GET) {sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));return;}// 比如 此处极权成功就抛出成功事件SecurityCheckComplete complete = new SecurityHandler.SecurityCheckComplete(true);// 设置 channel属性,相当于channel固定的上下文属性ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);ctx.fireUserEventTriggered(complete);final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols,allowExtensions, maxFramePayloadSize, allowMaskMismatch);final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);if (handshaker == null) {WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());} else {String s = req.headers().get("Sec-token");HttpHeaders httpHeaders = null;if(StringUtils.hasText(s)){httpHeaders = new DefaultHttpHeaders().add("Sec-token",s);}else {httpHeaders = new DefaultHttpHeaders();}// 设置请求头final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(),req, httpHeaders,ctx.channel().newPromise());System.err.println("handshakeFuture: "+handshakeFuture.isSuccess());handshakeFuture.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) throws Exception {if (!future.isSuccess()) {ctx.fireExceptionCaught(future.cause());} else {// Kept for compatibilityctx.fireUserEventTriggered(CustomWebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);ctx.fireUserEventTriggered(new CustomWebSocketServerProtocolHandler.HandshakeComplete(req.uri(), req.headers(), handshaker.selectedSubprotocol()));}}});CustomWebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker);ctx.pipeline().replace(this, "WS403Responder",CustomWebSocketServerProtocolHandler.forbiddenHttpRequestResponder());}} finally {req.release();}}private boolean isNotWebSocketPath(FullHttpRequest req) {return checkStartsWith ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath);}private static void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {ChannelFuture f = ctx.channel().writeAndFlush(res);if (!isKeepAlive(req) || res.status().code() != 200) {f.addListener(ChannelFutureListener.CLOSE);}}private static String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) {String protocol = "ws";if (cp.get(SslHandler.class) != null) {// SSL in use so use Secure WebSocketsprotocol = "wss";}String host = req.headers().get(HttpHeaderNames.HOST);return protocol + "://" + host + path;}// 自定义事件实体@Getter@AllArgsConstructorpublic static final class SecurityCheckComplete {private Boolean isLogin;}
}

ChatHandler

package com.chat.nettywebsocket.handler;import com.alibaba.fastjson.JSONObject;
import com.chat.nettywebsocket.domain.Message;
import com.chat.nettywebsocket.handler.test.CustomWebSocketServerProtocolHandler;
import com.chat.nettywebsocket.handler.test.SecurityHandler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;import java.nio.charset.StandardCharsets;/*** 自定义控制器* @author qubing* @date 2021/8/16 9:26*/
@Slf4j
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {/*** 为channel添加属性  将userid设置为属性,避免客户端特殊情况退出时获取不到userid*/AttributeKey<Integer> userid = AttributeKey.valueOf("userid");/*** 连接时* @param ctx 上下文* @throws Exception /*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {log.info("与客户端建立连接,通道开启!");// 添加到channelGroup通道组MyChannelHandlerPool.channelGroup.add(ctx.channel());}/*** 断开连接时* @param ctx /* @throws Exception /*/@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {log.info("与客户端断开连接,通道关闭!");// 从channelGroup通道组移除
//        MyChannelHandlerPool.channelGroup.remove(ctx.channel());
//        Integer useridQuit = ctx.channel().attr(userid).get();
//        MyChannelHandlerPool.channelIdMap.remove(useridQuit);log.info("断开的用户id为");}// 监听事件@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {// 自定义鉴权成功事件if (evt instanceof SecurityHandler.SecurityCheckComplete){// 鉴权成功后的逻辑log.info("鉴权成功  SecurityHandler.SecurityCheckComplete");}// 握手成功else if (evt instanceof CustomWebSocketServerProtocolHandler.HandshakeComplete) {log.info("Handshake has completed");// 握手成功后的逻辑  鉴权和不鉴权模式都绑定channellog.info("Handshake has completed after binding channel");}super.userEventTriggered(ctx, evt);}/*** 获取消息时* @param ctx /* @param msg 消息* @throws Exception /*/@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {String mssage = msg.content().toString(StandardCharsets.UTF_8);ctx.channel().writeAndFlush(mssage);System.err.println(mssage);}/*** 群发所有人*/private void sendAllMessage(String message){//收到信息后,群发给所有channelMyChannelHandlerPool.channelGroup.writeAndFlush( new TextWebSocketFrame(message));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.info("exceptionCaught 异常:{}",cause.getMessage());cause.printStackTrace();Channel channel = ctx.channel();//……if(channel.isActive()){log.info("手动关闭通道");ctx.close();};}
}

AttributeKeyUtils

public class AttributeKeyUtils {/*** 为channel添加属性  将userid设置为属性,避免客户端特殊情况退出时获取不到userid*/public static final AttributeKey<String> USER_ID = AttributeKey.valueOf("userid");public static final AttributeKey<SecurityHandler.SecurityCheckComplete> SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY =AttributeKey.valueOf("SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY");}

WsServerInitializer

@Slf4j
@ChannelHandler.Sharable
public class WsServerInitializer extends ChannelInitializer<SocketChannel> {//    @Override
//    protected void initChannel(SocketChannel socketChannel) throws Exception {
//        log.info("有新的连接");
//        ChannelPipeline pipeline = socketChannel.pipeline();
//        //netty 自带的http解码器
//        pipeline.addLast(new HttpServerCodec());
//        //http聚合器
//        pipeline.addLast(new HttpObjectAggregator(8192));
//        pipeline.addLast(new ChunkedWriteHandler());
//        //压缩协议
//        pipeline.addLast(new WebSocketServerCompressionHandler());
//        //http处理器 用来握手和执行进一步操作
        pipeline.addLast(new NettyWebsocketHttpHandler(config, listener));
//
//    }@Overrideprotected void initChannel(SocketChannel ch) throws Exception {log.info("有新的连接");//获取工人所要做的工程(管道器==管道器对应的便是管道channel)ChannelPipeline pipeline = ch.pipeline();//为工人的工程按顺序添加工序/材料 (为管道器设置对应的handler也就是控制器)//1.设置心跳机制pipeline.addLast(new IdleStateHandler(5,0,0, TimeUnit.SECONDS));//2.出入站时的控制器,大部分用于针对心跳机制pipeline.addLast(new WsChannelDupleHandler());//3.加解码pipeline.addLast(new HttpServerCodec());//3.打印控制器,为工人提供明显可见的操作结果的样式pipeline.addLast("logging", new LoggingHandler(LogLevel.INFO));pipeline.addLast(new ChunkedWriteHandler());pipeline.addLast(new HttpObjectAggregator(8192));// 扩展的websocket协议pipeline.addLast(new CustomWebSocketServerProtocolHandler("/ws","websocket",true,65536 * 10,false,true));//7.自定义的handler针对业务pipeline.addLast(new ChatHandler());}
}

上截图

postman测试怎增加自定义请求头

在这里插入图片描述

点击链接查看控制台

postman链接成功
在这里插入图片描述
根据日志可以看出,链接成功并且相应和请求的头是一致的
在这里插入图片描述

发送消息

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/670012.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

远程桌面使用Pr剪视频

要远程访问高性能计算机并使用 Pr&#xff08;Adobe Premiere Pro&#xff09;进行视频编辑&#xff0c;您可以考虑使用流畅且响应迅速的远程桌面软件。您可以考虑以下选项。 Splashtop Business Access Performance Splashtop 以其高性能远程桌面解决方案而闻名&#xff0c;…

HarmonyOS远程真机调试方法

生成密钥库文件 打开DevEco Studio&#xff0c;点击菜单栏上的build&#xff0c; 填一些信息点击&#xff0c;没有key的话点击new一个新的key。 生成profile文件 AppGallery Connect (huawei.com) 进入该链接网站&#xff0c;点击用户与访问将刚生成的csr证书提交上去其中需…

地下停车场智慧监查系统:科技让停车更智能

随着城市化进程的加速&#xff0c;停车难成为了许多城市居民的痛点。而地下停车场作为解决停车难问题的重要手段&#xff0c;其安全性和便捷性也成为了人们关注的焦点。为了解决这一问题&#xff0c;山海鲸可视化搭建的地下停车场智慧监查系统应运而生&#xff0c;为车主们提供…

【极数系列】Flink集成KafkaSink 实时输出数据(11)

文章目录 01 引言02 连接器依赖2.1 kafka连接器依赖2.2 base基础依赖 03 使用方法04 序列化器05 指标监控06 项目源码实战6.1 包结构6.2 pom.xml依赖6.3 配置文件6.4 创建sink作业 01 引言 KafkaSink 可将数据流写入一个或多个 Kafka topic 实战源码地址,一键下载可用&#xf…

查大数据检测到风险等级太高是怎么回事?

随着金融风控越来越多元化&#xff0c;大数据作为新兴的技术被运用到贷前风控中去了&#xff0c;不少人也了解过自己的大数据&#xff0c;但是由于相关知识不足&#xff0c;看不懂报告&#xff0c;在常见的问题中&#xff0c;大数据检测到风险等级太高是怎么回事呢?小易大数据…

Oracle systemstate、gdb、dbx介绍

当数据库出现严重的性能问题或者hang了的时候&#xff0c; 可能最常用的办法就是重启数据库&#xff0c;简单有效解决问题&#xff1b;但是重启后如何追踪问题的根本原因成了难题&#xff0c;很多信息随着重启也消失不见了&#xff0c;让追查问题变的十分棘手&#xff0c;这时就…

别具一格,质感拉满 | PITAKA苹果Apple Watch彩碳表带开箱

别具一格&#xff0c;质感拉满 | PITAKA苹果Apple Watch彩碳表带开箱 我是在前年的时候购买的目前手头这款Apple Watch Series7&#xff0c;因为是购买的Nike版&#xff0c;所以可以看到它的表带标配为透气孔的运动型表带。 &#x1f53a;耐克版的透气孔表带虽说在一定程度上解…

OpenStack安全策略提升

1.规划节点 IP主机名节点192.168.100.10controllerOpenStack控制节点192.168.100.20computeOpenStack计算节点 2.环境准备​ 使用OpenStack平台的两台节点&#xff0c;节点规划表中的IP地址为作者的IP地址&#xff0c;在进行实操案例的时候&#xff0c;按照自己的环境规划网络…

Three.js学习6:透视相机和正交相机

一、相机 相机 camera&#xff0c;可以理解为摄像机。在拍影视剧的时候&#xff0c;最终用户看到的画面都是相机拍出来的内容。 Three.js 里&#xff0c;相机 camera 里的内容就是用户能看到的内容。从这个角度来看&#xff0c;相机其实就是用户的视野&#xff0c;就像用户的眼…

【cmu15445c++入门】(6)c++的迭代器

一、迭代器 C 迭代器是指向容器内元素的对象。它们可用于循环访问该容器的对象。我们知道迭代器的一个示例是指针。指针可用于循环访问 C 样式数组. 二、代码 自己实现一个迭代器 // C iterators are objects that point to an element inside a container. // They can be…

学习与学习理论 - 2024教招

一 学习的概述 1 学习的概念及其内涵 &#xff08;1&#xff09;学习的概念 学习是个体在特定情境下由于练习和反复经验而产生的行为或行为潜能的比较持久的变化。 人类的学习和动物学习的本质区别 人类学习是一个积极、主动的建构过程;人类的学习是掌握社会历史经验和个体经…

University Program VWF仿真步骤__全加器

本教程将以全加器为例&#xff0c;选择DE2-115开发板的Cyclone IV EP4CE115F29C7 FPGA&#xff0c;使用Quartus Lite v18.1&#xff0c;循序渐进的介绍如何创建Quartus工程&#xff0c;并使用Quartus Prime软件的University Program VWF工具创建波形文件&#xff0c;对全加器的…

生物素-PEG4-酪胺,Biotin-PEG4-TSA,应用于酶联免疫吸附实验

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;生物素-PEG4-酪胺&#xff0c;Biotin-PEG4-Tyramide&#xff0c;Biotin-PEG4-TSA 一、基本信息 产品简介&#xff1a;Biotin PEG4 Tyramine is a reagent used for tyramine signal amplification (TSA) through ca…

5-2、S曲线计算【51单片机+L298N步进电机系列教程】

↑↑↑点击上方【目录】&#xff0c;查看本系列全部文章 摘要&#xff1a;本节介绍S曲线的基本变换&#xff0c;将基本形式的S曲线变换成为任意过两点的S曲线&#xff0c;为后续步进电机S曲线运动提供理论支撑 一.计算目标 ①计算经过任意不同两点的S曲线方程 ②可调节曲线平…

C++之字符串

C风格字符串 字符串处理在程序中应用广泛&#xff0c;C风格字符串是以\0&#xff08;空字符&#xff09;来结尾的字符数组。对字符串进行操作的C函数定义在头文件<string.h>或中。常用的库函数如下&#xff1a; //字符检查函数(非修改式操作) size_t strlen( const char …

linux麒麟系统安装mongodb7.0

1.mogedb下载 下载的是他tar包 https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel80-7.0.5.tgz wget -o https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel80-7.0.5.tgz 也可以下载rpm包 2.将包上传至服务器并解压 #进入目录 并解压 cd /opt/ tar …

【 buuctf-另外一个世界】

flag 就隐藏在这一串二进制数中&#xff0c;可以利用在线工具转换得到 flag&#xff0c;本次讲一下用代码怎么转换。将二进制数转换成 ascii 字母&#xff0c;手写的话两种思路&#xff1a; 1.将二进制数四位一组先转成十六进制数&#xff0c;再将十六进制数两位一组&#xff…

JavaScript的表单、控件

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;《爱蹦跶的大A阿》 &#x1f525;当前正在更新专栏&#xff1a;《VUE》 、《JavaScript保姆级教程》、《krpano》、《krpano中文文档》 ​ ​ ✨ 前言 表单是 web 开发中不可或缺的一部分&#xff0c;用于收集用户输入。本…

vscode 突然连接不上服务器了(2024年版本 自动更新从1.85-1.86)

vscode日志 ll192.168.103.5s password:]0;C:\WINDOWS\System32\cmd.exe [17:09:16.886] Got some output, clearing connection timeout [17:09:16.887] Showing password prompt [17:09:19.688] Got password response [17:09:19.688] "install" wrote data to te…

AE2023 After Effects 2023

After Effects 2023是一款非常强大的视频编辑软件&#xff0c;提供了许多新功能和改进&#xff0c;使得视频编辑和合成更加高效和灵活。以下是一些After Effects 2023的特色功能&#xff1a; 新合成预设列表&#xff1a;After Effects 2023彻底修改了预设列表&#xff0c;使其…