(转)开源项目t-io

石墨文档:https://shimo.im/docs/tHwJJcvKl2AIiCZD/

(二期)18、开源t-io项目解读

【课程18】BIO、...AIO.xmind0.4MB

【课程18】t-io简介.xmind0.2MB

【课程18】两个官方例子.xmind0.3MB

【课程18】同步异...阻塞.xmind0.3MB

【课程18预习】百万...t-io.xmind0.3MB

t-io是什么

官网相关

官网:https://t-io.org/

宣传:不仅仅是百万级网络通信框架,让天下没有难开发的网络通信

git地址:https://gitee.com/tywo45/t-io

t-io手册:https://t-io.org/doc/index.html

t-io.pdf3MB

t-io与websocket

t-io:是一个网络框架,从这一点来说是有点像 netty 的,但 t-io 为常见和网络相关的业务(如 IM、消息推送、RPC、监控)提供了近乎于现成的解决方案,即丰富的编程 API,极大减少业务层的编程难度。

websocket: WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

预备知识AIO、NIO、Socket
同步、异步、阻塞、非阻塞

先来个例子理解一下概念,以银行取款为例: 

  • 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
  • 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);

  • 阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
  • 非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)

IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。

同步阻塞BIO
  • 一个连接一个线程

在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。

优化:弄一个线程池来管理线程。即伪异步阻塞IO

同步非阻塞NIO
  • 一个请求一个线程

NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题:每个客户端请求必须使用一个线程单独来处理。问题在于系统本身对线程总数有一定限制,容易瘫痪。

NIO,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。  也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。

** NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。

异步非阻塞AIO

对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。  即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。

总结

Java对BIO、NIO、AIO的支持:

  • Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
  • Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理

BIO、NIO、AIO适用场景分析:

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
t-io框架
使用场景

常用关键类
  • ChannelContext(通道上下文)
  • 每一个 tcp 连接的建立都会产生一个 ChannelContext 对象
  • (1)ServerChannelContext
  • ChannelContext 的子类,当用 tio 作 tcp 服务器时,业务层接触的是这个类的实例。
  • (2)ClientChannelContext
  • ChannelContext 的子类,当用 tio 作 tcp 客户端时,业务层接触的是这个类的实例

  • GroupContext(服务配置与维护)
  • GroupContext 就是用来配置线程池、确定监听端口,维护客户端各种数据等的
  • ClientGroupContext
  • ServerGroupContext

我们在写 TCP Server 时,都会先选好一个端口以监听客户端连接,再创建 N 组线程池来执行相关的任 务,譬如发送消息、解码数据包、处理数据包等任务,还要维护客户端连接的各种数据,为了和业务互动, 还要把这些客户端连接和各种业务数据绑定起来,譬如把某个客户端绑定到一个群组,绑定到一个 userid, 绑定到一个 token 等。GroupContext 就是用来配置线程池、确定监听端口,维护客户端各种数据等的。

GroupContext 是个抽象类,如果你是用 tio 作 tcp 客户端,那么你需要创建 ClientGroupContext,如 果你是用 tio 作 tcp 服务器,那么你需要创建 ServerGroupContext

  • AioHandler(消息处理接口)
  • 处理消息的核心接口,它有两个子接口
  • ClientAioHandler
  • ServerAioHandler
  • AioListener(通道监听者)
  • 处理事件监听的核心接口,它有两个子接口,
  • ClientAioListener
  • ServerAioListener
  • Packet(应用层数据包)
  • TCP 层过来的数据,都会被 tio 要求解码成 Packet 对象,应用都需要继承这个类,从而实现自己的业务 数据包。

用于应用层与传输层的数据传递

传输层在往应用层传递数据时,并不保证每次传递的数据是一个完整的应用层数据包(以 http 协议为 例,就是并不保证应用层收到的数据刚好可以组成一个 http 包),这就是我们经常提到的半包粘包。传输层只负责传递 byte[]数据,应用层需要自己对 byte[]数据进行解码,以 http 协议为例,就是把 byte[] 解码成 http 协议格式的字符串。

  • AioServer(tio 服务端入口类)

  • AioClient(tio 客户端入口类)

  • ObjWithLock(自带读写锁的对象)
  • 是一个自带了一把(读写)锁的普通对象(一般是集合对象),每当要对 这个对象进行同步安全操作(并发下对集合进行遍历或对集合对象进行元素修改删除增加)时,就得用这个 锁。

t-io是基于tcp层协议的一个网络框架,所以在应用层与tcp传输层之间设计到一个数据的编码与解码问题,t-io让我们能自定义数据协议,所以需要我们自己手动去编码解码过程。

入门例子HelloWord
  • git项目地址

https://gitee.com/tywo45/tio-showcase

git里面有个几个例子,首先我们看helloword的例子:

业务逻辑

本例子演示的是一个典型的TCP长连接应用,大体业务简介如下。

  • 分为server和client工程,server和client共用common工程
  • 服务端和客户端的消息协议比较简单,消息头为4个字节,用以表示消息体的长度,消息体为一个字符串的byte[]
  • 服务端先启动,监听6789端口
  • 客户端连接到服务端后,会主动向服务器发送一条消息
  • 服务器收到消息后会回应一条消息
  • 之后,框架层会自动从客户端发心跳到服务器,服务器也会检测心跳有没有超时(这些事都是框架做的,业务层只需要配一个心跳超时参数即可)
  • 框架层会在断链后自动重连(这些事都是框架做的,业务层只需要配一个重连配置对象即可)

具体类说明:

server端
  • 导入核心包
<dependency>
   <groupId>org.t-io</groupId>
   <artifactId>tio-core</artifactId>
</dependency>
  • HelloServerStarter

构造ServerGroupContext,main方法启动服务

  • HelloServerAioHandler

实现ServerAioHandler接口,重写decodeencode,handler方法。

common
  • HelloPacket

继承Packet类。自定义数据包的内容

  • Const

常量类,配置ip,端口等信息

client端
  • HelloClientStarter

构造clientGroupContext,连接节点,使用信息发送消息

  • HelloClientAioHandler

实现ClientAioHandler接口,重写decode,encode,handler方法。

流程图
  • idea请安装PlantUML intergration插件

客户端与服务端沟通时序图.puml0.8KB

Server端初始化时序图.puml0.6KB

(初始化服务器)

 

 

(客户端与服务端通讯流程)

入门例子showcase
  • git项目地址

https://gitee.com/tywo45/tio-showcase

上面讲的是helloword的例子,比较简单,接下来的是showcase的例子,结合实际场景的一个例子。

业务逻辑

tio的框架初始化使用过程是一样的。不过因为showcase中涉及到的交互越来越多,因此不能像helloword例子中的HelloPacket只有body这么简单了,我们至少要加上一个type参数(消息类型),这样的话服务器获取到数据包之后再更加type来选择消息处理类,从而拓展系统可用性。

(客户端与服务器沟通流程)

客户端与服务端沟通时序图.puml1.4KB

 

客户端发起登录操作

  • org.tio.examples.showcase.client.ShowcaseClientStarter#processCommand
LoginReqBody loginReqBody = new LoginReqBody();
loginReqBody.setLoginname(loginname);
loginReqBody.setPassword(password);
 
  
ShowcasePacket reqPacket = new ShowcasePacket();
#这里指定消息类型
reqPacket.setType(Type.LOGIN_REQ);
reqPacket.setBody(Json.toJson(loginReqBody).getBytes(ShowcasePacket.CHARSET));
 
  
Tio.send(clientChannelContext, reqPacket);
  • LoginReqBody:登录请求参数封装类,继承BaseBody
  • ShowcasePacket:数据包,继承Packet,和ByteBuffer相互转换
  • clientChannelContext:连接上下文通道

服务端接受请求操作

  • org.tio.examples.showcase.server.ShowcaseServerAioHandler#handler
ShowcasePacket showcasePacket = (ShowcasePacket) packet;
#获取消息类型
Byte type = showcasePacket.getType();
#根据消息类型找到对应的消息处理类
AbsShowcaseBsHandler<?> showcaseBsHandler = handlerMap.get(type);
if (showcaseBsHandler == null) {
   log.error("{}, 找不到处理类,type:{}", channelContext, type);
   return;
}
#执行消息处理。消息处理类必须继承AbsShowcaseBsHandler
showcaseBsHandler.handler(showcasePacket, channelContext);
  • handlerMap:存有当前所有消息处理类的map。数据包中包含消息类型,会根据消息类型获取对应的消息处理类,而这个消息处理类会调用handler()方法处理数据。
  • AbsShowcaseBsHandler:消息处理抽象类,继承这个类的处理类会对一种消息类型进行处理,并且一般专门处理一种消息封装类(继承BaseBody的封装类)。

消息类型对应消息处理类的初始化

private static Map<Byte, AbsShowcaseBsHandler<?>> handlerMap = new HashMap<>();
static {
   #把消息类型与消息处理类映射起来 
   handlerMap.put(Type.GROUP_MSG_REQ, new GroupMsgReqHandler());
   handlerMap.put(Type.HEART_BEAT_REQ, new HeartbeatReqHandler());
   handlerMap.put(Type.JOIN_GROUP_REQ, new JoinGroupReqHandler());
   handlerMap.put(Type.LOGIN_REQ, new LoginReqHandler());
   handlerMap.put(Type.P2P_REQ, new P2PReqHandler());
}

如果接收到的消息类型是P2P_REQ,那么处理类就是P2PReqHandler:

  • org.tio.examples.showcase.server.handler.P2PReqHandler#handler
log.info("收到点对点请求消息:{}", Json.toJson(bsBody));
 
  
ShowcaseSessionContext showcaseSessionContext = (ShowcaseSessionContext) channelContext.getAttribute();
 
  
P2PRespBody p2pRespBody = new P2PRespBody();
p2pRespBody.setFromUserid(showcaseSessionContext.getUserid());
p2pRespBody.setText(bsBody.getText());
 
  
ShowcasePacket respPacket = new ShowcasePacket();
respPacket.setType(Type.P2P_RESP);
respPacket.setBody(Json.toJson(p2pRespBody).getBytes(ShowcasePacket.CHARSET));
Tio.sendToUser(channelContext.groupContext, bsBody.getToUserid(), respPacket);
springboot集成t-io

项目运行:https://github.com/fanpan26/SpringBootLayIM.git

由于layim是付费产品,所以在网络上找了一个。(仅供学习哈)

需要放到项目目录下面

static/js/layui/lay/modules/layim.js

layim.js33.3KB

项目结构

项目集成:

因为这里需要和浏览器之间进行通讯,所以需要用到websocket机制。tio有集成websocket的框架,所以直接导入即可。

<dependency>
    <groupId>org.t-io</groupId>
    <artifactId>tio-websocket-server</artifactId>
    <version>0.0.5-tio-websocket</version>
</dependency>

代码结构

Guava - EventBus(事件总线)

项目使用Guava的EventBus替代了Spring的ApplicationEvent事件机制。

Guava的EventBus使用介绍如下:

事件定义

EventBus为我们提供了register方法来订阅事件,不需要实现任何的额外接口或者base类,只需要在订阅方法上标注上@Subscribe和保证只有一个输入参数的方法就可以搞定。

new Object() {
 
  
    @Subscribe
    public void lister(Integer integer) {
        System.out.printf("%d from int%n", integer);
    }
}

事件发布

对于事件源,则可以通过post方法发布事件。 正在这里对于Guava对于事件的发布,是依据上例中订阅方法的方法参数类型决定的,换而言之就是post传入的类型和其基类类型可以收到此事件。

//定义事件
final EventBus eventBus = new EventBus();
//注册事件
eventBus.register(new Object() {
 
  
    //使用@Subscribe说明订阅事件处理方法
    @Subscribe
    public void lister(Integer integer) {
        System.out.printf("%s from int%n", integer);
    }
 
  
    @Subscribe
    public void lister(Number integer) {
        System.out.printf("%s from Number%n", integer);
    }
 
  
    @Subscribe
    public void lister(Long integer) {
        System.out.printf("%s from long%n", integer);
    }
});
 
  
//发布事件
eventBus.post(1);
eventBus.post(1L);

项目的而运用

主要处理事件:包含了申请通知,添加好友成功通知

关键类:

  • com.fyp.layim.common.event.bus.EventUtil:封装了事件的监听注册,以及发布动作
  • com.fyp.layim.common.event.bus.body.EventBody:发布的内容封装类,包含消息类型和消息内容字段
  • com.fyp.layim.common.event.bus.handler.AbsEventHandler:事件处理抽象类,具体处理器需要继承这个重写handler()方法
  • com.fyp.layim.common.event.bus.handler.AddFriendEventHandler:添加好友成功通知处理类。
  • com.fyp.layim.common.event.bus.LayimEventType:消息类型常量

调用:

  • com.fyp.layim.web.biz.UserController#handleFriendApply:好友同意好友请求之后发布事件

逻辑:

事件的处理其实是给申请人发起好友同意通知

  • com.fyp.layim.im.common.util.PushUtil#pushAddFriendMessage
/**
 * 添加好友成功之后向对方推送消息
 * */
public static void pushAddFriendMessage(long applyid){
    if(applyid==0){
        return;
    }
    Apply apply = applyService.getApply(applyid);
    ChannelContext channelContext = getChannelContext(""+apply.getUid());
    //先判断是否在线,再去查询数据库,减少查询次数
    if (channelContext != null && !channelContext.isClosed()) {
        LayimToClientAddFriendMsgBody body = new LayimToClientAddFriendMsgBody();
        User user = getUserService().getUser(apply.getToid());
        if (user==null){return;}
        //对方分组ID
        body.setGroupid(apply.getGroup());
 
  
        //当前用户的基本信息,用于调用layim.addList
        body.setAvatar(user.getAvatar());
        body.setId(user.getId());
        body.setSign(user.getSign());
        body.setType("friend");
        body.setUsername(user.getUserName());
 
  
        push(channelContext, body);
    }
}
 
  

最后是通过Aio.send发送消息。

  • com.fyp.layim.im.common.util.PushUtil#push
/**
 * 服务端主动推送消息
 * */
private static void push(ChannelContext channelContext,Object msg) {
    try {
        WsResponse response = BodyConvert.getInstance().convertToTextResponse(msg);
        Aio.send(channelContext, response);
    }catch (IOException ex){
 
  
    }
}
功能结构
  • 登录功能  
  • 单聊功能
  • 群聊功能
  • 其他自定义消息提醒功能
  • 等等。。。。

登录的目的是过滤非法请求,如果有一个非法用户请求websocket服务,直接返回403或者401即可。

单聊,群聊这个就不用解释了

其他自定义消息提醒,比如:时时加好友消息,广播消息,审核消息等。

项目设计-前端

前端的框架用到是layim,layim已经帮我们做好了界面和封装了接口,所以我们的工作只需要按照layim来写好接口提供结果就可以了。

首先看初始化:

  • templates/index.html

layim.config({
    //初始化接口
    init: {
        url: '/layim/base'
    }
    //查看群员接口
    ,members: {
        url: '/layim/members'
    }
    //上传图片接口
    ,uploadImage: {url: '/upload/file'}
    //上传文件接口
    ,uploadFile: {url: '/upload/file'}
 
  
    ,isAudio: true //开启聊天工具栏音频
    ,isVideo: true //开启聊天工具栏视频
    ,initSkin: '5.jpg' //1-5 设置初始背景
    ,notice: true //是否开启桌面消息提醒,默认false
    ,msgbox: '/layim/msgbox'
    ,find: layui.cache.dir + 'css/modules/layim/html/find.html' //发现页面地址,若不开启,剔除该项即可
    ,chatLog: layui.cache.dir + 'css/modules/layim/html/chatLog.html' //聊天记录页面地址,若不开启,剔除该项即可
 
  
});
 
  

因为需要进行通讯,所以websocket的信息也需要初始化。都是在layim里面一起初始化的。

socket.config({
    log:true,
    token:'/layim/token',
    server:'ws://127.0.0.1:8888'
});
  • templates/layim/msgbox.html
项目设计-后端
基本相关类

控制器:

com.fyp.layim.web.api.GroupController:群组相关

com.fyp.layim.web.biz.AccountController:账户相关,登录注册

com.fyp.layim.web.biz.UploadController:上传文件、图片

com.fyp.layim.web.biz.UserController:用户基础数据

拦截器:

com.fyp.layim.web.filter.TokenVerifyInterceptor:校验token,与userid转换

com.fyp.layim.web.filter.LayimAspect:给所有请求设置当前用户的值,放session当中

全局异常处理:

com.fyp.layim.web.filter.GlobalExceptionHandler:异常处理

网络编程主要代码

com.fyp.layim.im.common.*:定义消息类型,消息处理类的公共包。

com.fyp.layim.im.packet.*:这是数据包,因为协议是websocket,所以返回的不是packet,最后要返回的其实是符合websocket定义的org.tio.websocket.common.WsResponse。而这些数据是放在body中。

com.fyp.layim.im.server.*:t-io的服务配置相关,包括t-io服务的启动,启动之前需要ServerAioHandler、ServerAioListener、ServerGroupContext等参数。

所以总体逻辑是这样的:

步骤一、配置了springboot的模块自动加载机制。

  • META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.fyp.layim.im.server.LayimServerAutoConfig

LayimServerAutoConfig配置将会被自动装载。

  • com.fyp.layim.im.server.LayimServerAutoConfig:自动装载的配置类
  • com.fyp.layim.im.server.config.LayimServerConfig:服务的ip、端口、心跳时间等基本配置
  • com.fyp.layim.im.server.LayimWebsocketStarter:初始化配置,启动t-io服务,其中配置初始化和启动都是委托给com.fyp.layim.im.server.LayimServerStarter完成。
  • com.fyp.layim.im.server.LayimServerStarter:根据配置初始化serverGroupContext、初始化消息处理器
//初始化t-io的serverGroupContext
//还有消息处理器与消息类型的映射关系
public LayimServerStarter(LayimServerConfig wsServerConfig, IWsMsgHandler wsMsgHandler, TioUuid tioUuid, SynThreadPoolExecutor tioExecutor, ThreadPoolExecutor groupExecutor) throws Exception {
    this.layimServerConfig = wsServerConfig;
    this.wsMsgHandler = wsMsgHandler;
 
  
    layimServerAioHandler = new LayimServerAioHandler(wsServerConfig, wsMsgHandler);
    layimServerAioListener = new LayimServerAioListener();
 
  
    serverGroupContext = new ServerGroupContext(layimServerAioHandler, layimServerAioListener, tioExecutor, groupExecutor);
    //心跳时间,暂时设置为0
    serverGroupContext.setHeartbeatTimeout(wsServerConfig.getHeartBeatTimeout());
    serverGroupContext.setName("Tio Websocket Server for LayIM");
 
  
    aioServer = new AioServer(serverGroupContext);
    serverGroupContext.setTioUuid(tioUuid);
    //initSsl(serverGroupContext);
    
    
    //初始化消息处理器
    LayimMsgProcessorManager.init();
}

接下来看下与serverGroupContext相关的消息处理咧、事件监听类

  • com.fyp.layim.im.server.handler.LayimServerAioHandler:继承ServerAioHandler,完成消息的解码、编码、消息处理过程
  • com.fyp.layim.im.server.listener.LayimServerAioListener:继承ServerAioListener,完成事件监听

绑定用户信息

  • com.fyp.layim.im.common.processor.ClientCheckOnlineMsgProcessor#process
SetWithLock<ChannelContext> checkChannelContexts =
      Aio.getChannelContextsByUserid(channelContext.getGroupContext(),body.getId());

绑定用户上线信息。

  • com.fyp.layim.im.server.handler.LayimMsgHandler#handleHandshakeUserInfo
private HttpResponse handleHandshakeUserInfo(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws  Exception {
    UserService userService = getUserService();
    //增加token验证方法
 
  
    String path = httpRequest.getRequestLine().getPath();
    String token = URLDecoder.decode(path.substring(1),"utf-8");
 
  
    String userId = TokenVerify.IsValid(token);
 
  
    if (userId == null) {
        //没有token 未授权
        httpResponse.setStatus(HttpResponseStatus.C401);
    } else {
        long uid = Long.parseLong(userId);
        //解析token
        LayimContextUserInfo userInfo = userService.getContextUserInfo(uid);
        if (userInfo == null) {
            //没有找到用户
            httpResponse.setStatus(HttpResponseStatus.C404);
        } else {
            channelContext.setAttribute(userId, userInfo.getContextUser());
            //绑定用户ID
            Aio.bindUser(channelContext, userId);
            //绑定用户群组
            List<String> groupIds = userInfo.getGroupIds();
            //绑定用户群信息
            if (groupIds != null) {
                groupIds.forEach(groupId -> Aio.bindGroup(channelContext, groupId));
            }
            //通知所有好友本人上线了
            notify(channelContext,true);
        }
    }
    return httpResponse;
}

Aio.java的api:

实现  IWsMsgHandler接口:用于websocket握手,响应,关闭通道等过程的业务处理。

握手逻辑首先是在:com.fyp.layim.im.server.handler.LayimServerAioHandler中。

wsMsgHandler.handshake 方法,这里一般直接返回默认的 httpReponse即可,代表(框架层)握手成功。但是我们可以在接口中自定义一些业务逻辑,比如用户判断之类的逻辑,然后决定是否同意握手流程。

猜你喜欢

转载自www.cnblogs.com/free-wings/p/9920771.html