从零一起学Spring Boot之LayIM项目长成记(五)websocket
前言
距离上一篇已经比较久的时间了,项目也是开了个头。并且,由于网上的关于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的更多相关文章
- 从零一起学Spring Boot之LayIM项目长成记(四) Spring Boot JPA 深入了解
前言 本篇内容主要是一些关于JPA的常用的一些用法等.内容也是很多是看其他博客学来的,顺道在本系列博客里抽出一篇作为总结.下面让我们来看看吧. 不过我更推荐大家读本篇:https://lufficc. ...
- 从零一起学Spring Boot之LayIM项目长成记(三) 数据库的简单设计和JPA的简单使用。
前言 今天是第三篇了,上一篇简单模拟了数据,实现了LayIM页面的数据加载.那么今天呢就要用数据库的数据了.闲言少叙,书归正传,让我们开始吧. 数据库 之前有好多小伙伴问我数据库是怎么设计的.我个人用 ...
- 从零一起学Spring Boot之LayIM项目长成记(二) LayIM初体验
前言 接上篇,已经完成了一个SpringBoot项目的基本搭建.那么现在就要考虑要做什么,怎么做的问题.所以本篇内容不多,带大家一起来简单了解一下要做的东西,之前有很多人不知道从哪里下手,那么今天我带 ...
- 从零一起学Spring Boot之LayIM项目长成记(一) 初见 Spring Boot
项目背景 之前写过LayIM的.NET版后端实现,后来又写过一版Java的.当时用的是servlet,websocket和jdbc.虽然时间过去很久了,但是仍有些同学在关注.偶然间我听说了Spring ...
- 从零一起学Spring Boot之LayIM项目长成记(六)单聊群聊的实现
文章传送门: https://my.oschina.net/panzi1/blog/1577007 并没有放弃博客园,只是 t-io 在 oschina发展.用了人家的框架,也得帮人家做做宣传是吧~~
- (31)Spring Boot导入XML配置【从零开始学Spring Boot】
[来也匆匆,去也匆匆,在此留下您的脚印吧,转发点赞评论: 您的认可是我最大的动力,感谢您的支持] Spring Boot理念就是零配置编程,但是如果绝对需要使用XML的配置,我们建议您仍旧从一个@Co ...
- 57. Spring 自定义properties升级篇【从零开始学Spring Boot】
之前在两篇文章中都有简单介绍或者提到过 自定义属性的用法: 25.Spring Boot使用自定义的properties[从零开始学Spring Boot] 51. spring boot属性文件之多 ...
- 4. 使用别的json解析框架【从零开始学Spring Boot】
转载:http://blog.csdn.net/linxingliang/article/details/51585921 此文章已经废弃,请看新版的博客的完美解决方案: 78. Spring Boo ...
- 17、Spring Boot普通类调用bean【从零开始学Spring Boot】
转载:http://blog.csdn.net/linxingliang/article/details/52013017 我们知道如果我们要在一个类使用spring提供的bean对象,我们需要把这个 ...
随机推荐
- HDU 5934 强联通分量
Bomb Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submis ...
- 江西省移动物联网发展战略新闻发布会举行-2017年10月江西IDC排行榜与发展报告
编者按:当人们在做技术创新时,我们在做“外包产业“:当人们在做制造产业,我们在做”服务产业“:江人们在做AI智能时,我们在做”物联网“崛起,即使有一个落差,但红色热土从不缺少成长激情. 本期摘自上月初 ...
- 51nod 1593 公园晨跑 | ST表(线段树?)思维题
51nod 1593 公园晨跑 有一只猴子,他生活在一个环形的公园里.有n棵树围绕着公园.第i棵树和第i+1棵树之间的距离是 di ,而第n棵树和第一棵树之间的距离是 dn .第i棵树的高度是 hi ...
- 使用测试思路快速学习Python-适合测试工程师的学习方法
本文采用Python doctest单元测试的方法,直接用代码学习代码,滚雪球式的迭代学习. doctest是一个python标准库自带的轻量单元测试工具,适合实现一些简单的单元测试.它可以在docs ...
- Scala 运算符和集合转换操作示例
Scala是数据挖掘算法领域最有力的编程语言之一,语言本身是面向函数,这也符合了数据挖掘算法的常用场景:在原始数据集上应用一系列的变换,语言本身也对集合操作提供了众多强大的函数,本文将以List类型为 ...
- linux 远程操作(expect)
Expect是在Tcl基础上创建起来的,它还提供了一些Tcl所没有的命令,它可以用来做一些linux下无法做到交互的一些命令操作,在远程管 理方面发挥很大的作用. spawn命令激活一个Unix程序来 ...
- OpenSCAD 建模:矿泉水瓶花洒
下载地址:https://github.com/ZhangGaoxing/openscad-models/tree/master/Sprinkle 代码: module screw(r=){ ::]) ...
- 张高兴的 Xamarin.Android 学习笔记:(一)环境配置
最近在自学 Xamarin 和 Android ,同时发现国内在做 Xamarin 的不多.我在自学中间遇到了很多问题,而且百度到的很多教程也有些过时,现在打算写点东西稍微总结下,顺便帮后人指指路了. ...
- Noip2016愤怒的小鸟(状压DP)
题目描述 题意大概就是坐标系上第一象限上有N只猪,每次可以构造一条经过原点且开口向下的抛物线,抛物线可能会经过某一或某些猪,求使所有猪被至少经过一次的抛物线最少数量. 原题中还有一个特殊指令M,对于正 ...
- Visual Studio插件Resharper 2016.1 及以上版本激活方法【亲测有效】
1.破解补丁下载:https://flydoos.ctfile.com/fs/y80153828783.下载下来解压之后的文件如下: 2.打开文件夹“IntelliJIDEALicenseServer ...