前言

  距离上一篇已经比较久的时间了,项目也是开了个头。并且,由于网上的关于Spring Boot的websocket讲解也比较多。于是我采用了另外的一个通讯框架 t-io 来实现LayIM中的通讯功能。本篇会着重介绍我在研究与开发过程中踩过的坑和比较花费的时间的部分。

WebSocket

  在研究 t-io 的时候,我已经写过关于t-io框架的一些简单例子分析以及框架中关于 websocket 中的编解码代码分析等,有兴趣的同学可以先看一下。因为 在LayIM项目中我会是用到 Showcase Demo 中的设计思路。

  通讯框架 t-io 学习——给初学者的Demo:ShowCase设计分析

  通讯框架 t-io 学习——websocket 部分源码解析

  如果你潜心想学到这些东西的话,本人还是建议静下心来看看。为什么不用Spring Boot 封装好的websocket呢?因为它封装的太完备,许多业务不能定制。而通过t-io框架自己开发websocket端,就比较灵活了。甚至可以打造专门为LayIM定制的websocket服务,在讲解我的开发之路之前,也向大家推荐更完备的解决方案 tio-im,当然,我也是借鉴该源代码的设计思路。不过它的实现更加强大,由于我的水平有限,我只能照猫画虎,胡乱写了一通。不过也还是能用的。

  tio-im 地址:https://gitee.com/xchao/tio-im

项目实战

  前几篇已经实现了LayIM主要界面的数据加载功能。接下来就是最核心的部分,通讯。实现思路很多,这里呢我使用了 基于 t-io 通讯框架的 websocket。在进入详细代码之前,我们先分析LayIM中用到的一些功能点。

  • 登录功能  
  • 单聊功能
  • 群聊功能
  • 其他自定义消息提醒功能
  • 等等。。。。

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

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

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

  t-io 中的对外发送消息接口在 Aio.java 中实现。(下文中只列取部分接口,以及在LayIM项目中用到的)

//绑定用户
public static void bindUser(ChannelContext channelContext, String userid)
//发送给用户
public static Boolean sendToUser(GroupContext groupContext, String userid, Packet packet)
//发送到群组
public static void sendToGroup(GroupContext groupContext, String group, Packet packet)
//发送给所有人
public static void sendToAll(GroupContext groupContext, Packet packet)
//发送到指定channel
public static Boolean send(ChannelContext channelContext, Packet packet)

  开工之前呢,我们还要开发消息的编解码类(框架中已经实现),消息监听事件的处理,由于对于LayIM我们有基于业务的定制开发,所以会改一部分源代码。那我这里呢就把框架中部分源码粘贴到项目中,然后进行代码修改。不过像比如:握手流程,升级Websocket连接,解析byte[] 这些功能我们就没必要自己去做了,想要学习的话,可以看着源代码自己去研究。好,我们进入代码部分。

代码剖析

  首先实现  IWsMsgHandler接口。这个接口定义在 org.tio.websocket.server.handler  包中,代码如下。

public interface IWsMsgHandler {
/** * 对httpResponse参数进行补充并返回,如果返回null表示不想和对方建立连接,框架会断开连接,如果返回非null,框架会把这个对象发送给对方
*/
public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception; /** * @return 可以是WsResponse、byte[]、ByteBuffer、String或null,如果是null,框架不会回消息
*/
Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception; /** * @return 可以是WsResponse、byte[]、ByteBuffer、String或null,如果是null,框架不会回消息
*/
Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception; /** * @return 可以是WsResponse、byte[]、ByteBuffer、String或null,如果是null,框架不会回消息
*/
Object onText(WsRequest wsRequest, String text, ChannelContext channelContext) throws Exception;
}

  一般我们会在公开的这些接口实现中做些事情,比如

   @Override
public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception {
logger.info("接收到text消息");
//消息业务处理逻辑
return "消息发送成功";
}

  不过既然这次我们可以自己写websocket内部的业务逻辑,所以,这些接口我们就不在处理主要业务逻辑。那么主要业务逻辑在哪里处理呢? 我把他放在了 decode 方法之后。可能,大伙看到这里有些晕,下面我画一张图来从大局上介绍一个消息的发送处理流程。这里我以单聊发送消息举例。

  从零一起学Spring Boot之LayIM项目长成记(五)websocket-LMLPHP

  首先是,客户端连接服务器。先走握手流程。

      if (!wsSessionContext.isHandshaked()) {
HttpRequest request = HttpRequestDecoder.decode(buffer, channelContext);
if (request == null) {
return null;
}
       //升级到websokcet协议
HttpResponse httpResponse = Protocol.updateToWebSocket(request, channelContext);
if (httpResponse == null) {
throw new AioDecodeException("http协议升级到websocket协议失败");
} wsSessionContext.setHandshakeRequestPacket(request);
wsSessionContext.setHandshakeResponsePacket(httpResponse); WsRequest wsRequestPacket = new WsRequest();
wsRequestPacket.setHandShake(true); return wsRequestPacket;
}
       WsSessionContext wsSessionContext = (WsSessionContext) channelContext.getAttribute();
HttpRequest request = wsSessionContext.getHandshakeRequestPacket();
HttpResponse httpResponse = wsSessionContext.getHandshakeResponsePacket();
       //这里通过handshake接口实现的返回值,判断是否同意握手
HttpResponse r = wsMsgHandler.handshake(request, httpResponse, channelContext);
if (r == null) {
Aio.remove(channelContext, "业务层不同意握手");
return;
}

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

  这里有一个小细节需要注意,无论是握手还是业务登录请求,成功之后,都需要将用户绑定到当前的上下文(channelContext)中。调用 Aio.bindUser 即可。

  下图为简版的聊天发送消息流程:客户端A 发送消息到客户端B。

  从零一起学Spring Boot之LayIM项目长成记(五)websocket-LMLPHP

  正如上文中所说,编解码我们不用过多的关心,那么我们需要关注的部分就是业务处理了。设计思路呢也很容易想到,首先,我们有不同的消息类型。这个消息类型由客户端决定。如果传入了错误的消息类型,就抛出异常或者返回未知消息处理即可。消息处理类结构设计如下:

  从零一起学Spring Boot之LayIM项目长成记(五)websocket-LMLPHP

  是不是很简单,一个通用业务处理入口,将消息转化为友好的类实体,然后在具体的消息处理器中处理业务逻辑即可。

  LayimAbsMsgProcessor 核心代码如下:

   /**
* 这里采用showcase中的设计思路(反序列化消息之后,由具体的消息处理器处理)
* */
@Override
public WsResponse process(WsRequest layimPacket, ChannelContext channelContext) throws Exception {
Class<T> clazz = getBodyClass();
T body = null;
if (layimPacket.getBody() != null) {
       //获取json格式的数据 
String json = ByteUtil.toText(layimPacket.getBody());
       //将字符串转化为具体类型的对象
body = Json.toBean(json, clazz);
}
     //通过具体处理类处理消息对象
return process(layimPacket, body, channelContext);
} public abstract WsResponse process(WsRequest layimPacket,T body,ChannelContext channelContext) throws Exception;

  ClientToClientMsgProcessor 核心代码如下:

 @Override
public WsResponse process(WsRequest layimPacket, ChatRequestBody body, ChannelContext channelContext) throws Exception {
     //requestBody 转化为接收端的消息类型
ClientToClientMsgBody msgBody = BodyConvert.getInstance().convertToMsgBody(body,channelContext);
     //消息包装,返回WsResponse
WsResponse response = BodyConvert.getInstance().convertToTextResponse(msgBody);
     //得到对方的channelContext
ChannelContext toChannelContext = Aio.getChannelContextByUserid(channelContext.getGroupContext(),body.getToId());
//发送给对方
Aio.send(toChannelContext,response);
return null;
}

对接spring boot

  那么如何启动websocket服务呢,一般框架中都是绑定好的。这里呢,我们特殊处理一下,刚开始我是手动调用start方法,后来研究了一下spring boot starter。下面简单介绍一下starter的用法。

  首先建立一个配置类。

@ConfigurationProperties("layim.websocket")
public class LayimServerProperties { public LayimServerProperties(){
port = ;
heartBeatTimeout = ;
ip = null;
} // getter setter
private int port;
private int heartBeatTimeout;
private String ip;
}

  第二部,新建一个 AutoConfig类

@Configuration
@EnableConfigurationProperties(LayimServerProperties.class)
public class LayimWebsocketServerAutoConfig { @Autowired
LayimServerProperties properties; @Bean
LayimWebsocketStarter layimWebsocketStarter() throws Exception{
//初始化配置信息
LayimServerConfig config = new LayimServerConfig(properties.getPort());
config.setBindIp(properties.getIp());
config.setHeartBeatTimeout(properties.getHeartBeatTimeout()); LayimWebsocketStarter layimWebsocketStarter = new LayimWebsocketStarter(config);
//启动服务
layimWebsocketStarter.start();
//返回
return layimWebsocketStarter;
}
}

  第三步,在resources文件夹下,新建META-INF文件夹,在新建一个spring.factories文件,文件内容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.fyp.layim.im.server.LayimWebsocketServerAutoConfig

  OK,到这里我们配置一下。

  从零一起学Spring Boot之LayIM项目长成记(五)websocket-LMLPHP

  然后启动程序。

  从零一起学Spring Boot之LayIM项目长成记(五)websocket-LMLPHP

    启动成功!

项目演示

  啰啰嗦嗦的讲了这么多,还是给大家看一下演示。

  用户 1,2 链接服务器。

  从零一起学Spring Boot之LayIM项目长成记(五)websocket-LMLPHP

  用户2给用户1发送消息:

  从零一起学Spring Boot之LayIM项目长成记(五)websocket-LMLPHP

  看上面的只是演示消息能够顺利发送,下面的日志打印图可以看出来服务器的处理流程。

  从零一起学Spring Boot之LayIM项目长成记(五)websocket-LMLPHP

总结

  到此为止我们已经可以实现通讯了,但是这些还不够还有更多的业务去处理。不过没关系,通讯实现了,后边的就不难了。其实更多的是细节的把握,比如用户退群,用户下线,统计用户在线个数等。

  下期预告:从零一起学Spring Boot之LayIM项目长成记(六)用户登录验证和单聊群聊的实现

  GitHub:https://github.com/fanpan26/SpringBootLayIM

05-07 10:39