前言

  距离上一篇已经比较久的时间了,项目也是开了个头。并且,由于网上的关于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 方法之后。可能,大伙看到这里有些晕,下面我画一张图来从大局上介绍一个消息的发送处理流程。这里我以单聊发送消息举例。

  

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

      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。

  

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

  

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

  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,到这里我们配置一下。

  

  然后启动程序。

  

    启动成功!

项目演示

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

  用户 1,2 链接服务器。

  

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

  

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

  

总结

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

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

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

从零一起学Spring Boot之LayIM项目长成记(五)websocket的更多相关文章

  1. 从零一起学Spring Boot之LayIM项目长成记(四) Spring Boot JPA 深入了解

    前言 本篇内容主要是一些关于JPA的常用的一些用法等.内容也是很多是看其他博客学来的,顺道在本系列博客里抽出一篇作为总结.下面让我们来看看吧. 不过我更推荐大家读本篇:https://lufficc. ...

  2. 从零一起学Spring Boot之LayIM项目长成记(三) 数据库的简单设计和JPA的简单使用。

    前言 今天是第三篇了,上一篇简单模拟了数据,实现了LayIM页面的数据加载.那么今天呢就要用数据库的数据了.闲言少叙,书归正传,让我们开始吧. 数据库 之前有好多小伙伴问我数据库是怎么设计的.我个人用 ...

  3. 从零一起学Spring Boot之LayIM项目长成记(二) LayIM初体验

    前言 接上篇,已经完成了一个SpringBoot项目的基本搭建.那么现在就要考虑要做什么,怎么做的问题.所以本篇内容不多,带大家一起来简单了解一下要做的东西,之前有很多人不知道从哪里下手,那么今天我带 ...

  4. 从零一起学Spring Boot之LayIM项目长成记(一) 初见 Spring Boot

    项目背景 之前写过LayIM的.NET版后端实现,后来又写过一版Java的.当时用的是servlet,websocket和jdbc.虽然时间过去很久了,但是仍有些同学在关注.偶然间我听说了Spring ...

  5. 从零一起学Spring Boot之LayIM项目长成记(六)单聊群聊的实现

    文章传送门: https://my.oschina.net/panzi1/blog/1577007 并没有放弃博客园,只是 t-io 在 oschina发展.用了人家的框架,也得帮人家做做宣传是吧~~

  6. (31)Spring Boot导入XML配置【从零开始学Spring Boot】

    [来也匆匆,去也匆匆,在此留下您的脚印吧,转发点赞评论: 您的认可是我最大的动力,感谢您的支持] Spring Boot理念就是零配置编程,但是如果绝对需要使用XML的配置,我们建议您仍旧从一个@Co ...

  7. 57. Spring 自定义properties升级篇【从零开始学Spring Boot】

    之前在两篇文章中都有简单介绍或者提到过 自定义属性的用法: 25.Spring Boot使用自定义的properties[从零开始学Spring Boot] 51. spring boot属性文件之多 ...

  8. 4. 使用别的json解析框架【从零开始学Spring Boot】

    转载:http://blog.csdn.net/linxingliang/article/details/51585921 此文章已经废弃,请看新版的博客的完美解决方案: 78. Spring Boo ...

  9. 17、Spring Boot普通类调用bean【从零开始学Spring Boot】

    转载:http://blog.csdn.net/linxingliang/article/details/52013017 我们知道如果我们要在一个类使用spring提供的bean对象,我们需要把这个 ...

随机推荐

  1. HDU 5934 强联通分量

    Bomb Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submis ...

  2. 百度地图JS调用示例

    <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content ...

  3. IsKeyboardFocused -- 键盘焦点

    <Trigger Property="IsKeyboardFocused" Value="true"> <!--<Setter Prop ...

  4. Win10下python3和python2同时安装并解决pip共存问题

    特别说明,本文是在Windows64位系统下进行的,32位系统请下载相应版本的安装包,安装方法类似. 使用python开发,环境有Python2和 python3 两种,有时候需要两种环境切换使用,下 ...

  5. PhoneWindow,ViewRoot,Activity之间的大致关系

    http://www.nowamagic.net/academy/detail/50160216 在android里,我们都知道activity.但是一个activity跟一个Window是一个什么关 ...

  6. sql server 2008 18456错误

    来自:http://blog.csdn.net/qishuangquan/article/details/6024767 百度搜18456错误几乎只能搜到一篇文章,并不是说结果条数,而是所有的文章都是 ...

  7. oracle建表权限问题和JSP连接oracle数据库基本操作

    JSP连接oracle数据库相关操作 1.创建表 打开Enterprise Manager Console,为用户添加权限CREATE ANY TABLE和分配一定的表空间USERS限额1024k. ...

  8. C#的常见算法(面试)

    一.求以下表达式的值,写出您想到的一种或几种实现方法: 1-2+3-4+--+m //方法一,通过顺序规律写程序,同时也知道flag标志位的重要性. static int F1(int m) { ; ...

  9. ASP.NET Core中间件实现分布式 Session

    1. ASP.NET Core中间件详解 1.1. 中间件原理 1.1.1. 什么是中间件 1.1.2. 中间件执行过程 1.1.3. 中间件的配置 1.2. 依赖注入中间件 1.3. Cookies ...

  10. 使用element ui 日期选择器获取值后的格式问题

    一般情况下,我们需要给后台的时间格式是: "yyyy-MM-dd" 但是使用Element ui日期选择器获取的值是这样的: Fri Sep :: GMT+ (中国标准时间) 在官 ...