从零一起学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对象,我们需要把这个 ...
随机推荐
- 通信技术:SSE设计方案(一)--- 前端Server-Sent Events概念讲解和基础类库完善发布
好了,开篇还是要扯扯的,否则感觉这个技术讲的么有那么冻人,嗯,这个晚上是有点冷了,秋衣秋裤大家都该加起来了,反正我不帮你买,妹子除外,嘻嘻. 之前几篇博客,研究前端通信技术的第一层ajax技术,从最基 ...
- Django实现用户密码重置
使用Django内置的认证视图实现简单的通过邮箱重置密码的功能版本:django 1.11 在django.contrib.auth.views中提供了四个类视图用于密码重置 class Passwo ...
- 通过nginx的fastcgi_param来设置环境变量
在nginx配置文件中,可以在nginx总体的配置文件nginx.conf中,也可以在单独的网站配置环境中进行设置,如:www.tomener.com.conf 在配置环境server段locatio ...
- C# 使用itextsharp 读取pdf中文字坐标
程序调用: using iTextSharp.text.pdf; using System; using System.Collections.Generic; using System.Linq ...
- Hyper-v 虚拟机安装win7
Hyper-v 是微软自带的虚拟机 一般安装win10都有 对小娜说:Hyper就会出现 进入Hyper-v 如果虚拟机不可用 启动服务 新建虚拟机 一般设置可以自己随意 虚拟机第一代才可以使用win ...
- 博客志第一天——判断一个整数N是否是完全平方数?
关注博客园很久,今天是第一次写博客.先附上一个C题目:写一个函数判断一个整数是否为完全平方数,同时是否该数的各位数至少两个相同的数字 #include <stdio.h> #include ...
- 爬取朋友圈,Get年度关键词
人生苦短,我用Python && C#. 1.引言 最近初学Python,写爬虫上瘾.爬了豆瓣练手,又爬了公司的论坛生成词云分析年度关键词.最近琢磨着2017又仅剩两月了,我的年度关键 ...
- Snail’s trouble
Snail’s trouble Time Limit: 20000/10000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) T ...
- Is It A Tree?
Is It A Tree? Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total ...
- centos 6.9安装zabbix 3.0
Linux下常用的系统监控软件有Nagios.Cacti.Zabbix.Monit等,这些开源的软件,可以帮助我们更好的管理机器,在第一时间内发现,并警告系统维护人员. 今天开始研究下Zabbix,使 ...