Spring boot + LayIM + t-io 单聊群聊的实现


声明:本文转载自https://my.oschina.net/panzi1/blog/1577007,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

前言        

    新人上任三把火,除了新人报道篇,本篇可以算是正式的第一篇了,本文是来自博客园《从零一起学Spring Boot之LayIM项目长成记》系列的第六篇,原标题为:《从零一起学Spring Boot之LayIM项目长成记(六)用户登录验证和单聊群聊的实现》。看前几篇的话,大家可以去 http://www.cnblogs.com/panzi/ 围观一下。不过由于用户登录没有去实现,所以就暂时介绍一下单聊和群聊的开发过程吧。

功能介绍

    先简单介绍一下项目背景,突然发现, 项目背景就是标题。。。直接进入正题吧。当然除了LayIM我已经实现过N个版本以外,Springboot和t-io都是初次使用,难免有使用不当之处,欢迎批评指正。

    其实从标题就可以看出来,单聊和群聊功能无非就是借助通讯框架来实现时时通讯,单聊,1对1,群聊1对多,或者1对1也能理解成为两个人的群聊。不过t-io相对于springboot中已经封装好的websocket来说,具有更强的开发灵活性,并且自带丰富功能的API。不过不得不说,这个框架用起来还真是挺爽的,尤其当你更加明确消息的走向的时候,你会发现,原来通讯这么好玩。另外,我的开发过程中和LayIM的业务结合比较强,所以通用性较差。学习更高级的部分可以去看 tio-im,它的实现就比较牛啦。

    废话少说,直接上代码。

单聊

    在文章 从零一起学Spring Boot之LayIM项目长成记(五)websocket 中已经实现了消息的发送,所以这里不在赘述,下文中将会详细的讲到消息发送以及处理上的细节。

    前半段是js介绍部分,无兴趣的可以直接跳到server端

    首先,我自己封装了一个简单的适用于Layui开发模式的socket模块。Layui模块化开发官网示例如下:

/**   项目JS主入口   以依赖layui的layer和form模块为例 **/     layui.define(['layer', 'form'], function(exports){   var layer = layui.layer   ,form = layui.form;      layer.msg('Hello World');      exports('index', {}); //注意,这里是模块输出的核心,模块名必须和use时的模块名一致 });  

    (题外话,osc的代码段还是比较好看的)同样的道理,照葫芦画瓢,先搭建一个模型再说:

//依赖jquery和layer layui.define(['jquery','layer'],function (exports) {     var $ = layui.jquery;     var layer = layui.layer;     //定义     var socket = function () {      }     //导出     exports('socket',new socket()); })  /**  使用方式    layui.use('socket',function(socket){                  //do something               })  */

    基架搭建完了,剩下的就是填充功能。那么socket.js要做什么事情呢?

  • 连接服务器
  • 监听各种事件
  • 要能灵活配置
  • 消息发送和接收(监听)

    从以上几点呢,就可以分模块写代码了。首先连接服务器,很简单,就是判断支不支持websocket,做个提示,然后监听成功失败等事件。

  //内部默认配置 var defaultOptions = {         log:true,//是否打印日志         server:'ws://127.0.0.1:8888',//ws地址         reconn:false //是否断线重连 };   //连接代码片段   this.ws = new WebSocket(this.options.server); //监听事件   this.regWsEvent();   

    事件监听机制是我参考的 layim.js 源码中的写法。

socket.prototype.on=function(event,callback) {         //回调是function,进行事件注册,已经注册过的事件不在注册         if(typeof callback === 'function'){             (!call[event]) && (call[event] = callback);              tool.log('注册事件:【'+event+'】');         }         return this;     }

    上文中 regWsEvent 源码,其实就是ws的几个默认事件,然后让我转化成了socket的监听事件方式。

 regWsEvent:function () {       if(this.ws){          //收到消息时          this.ws.onmessage = function (event) {             call.msg&&call.msg(event);           };            //关闭时            this.ws.onclose = function (event) {            call.close&&call.close(event);           };            //连接成功时            this.ws.onopen = function (event) {            call.open&&call.open(event);            };            //出错时            this.ws.onerror = function (event) {                call.error&&call.error(event);             };           }        }

    所以在外部调用是酱紫的:

 socket.config({             log:true,             server:'ws://127.0.0.1:8888/2'         });                  socket.on('open',function (e) {             console.log("监听到事件:open");         });         socket.on('close',function (e) {             console.log("监听到事件:close");         });         socket.on('error',function (e) {             console.log("监听到事件:error");         });         socket.on('msg',function (e) {             console.log("监听到事件:msg");             //和layim对接的地方会在这里          });

    运行一下,看看打印,基本没啥问题

    

    接触过的websocket的,或者做过demo的,上边的代码都不难理解,只不过我做了稍许封装。在进入服务端开发部分之前,先简单介绍一个layim中的聊天api,首先他发送消息的格式是酱紫的:

    

    从以上截图可以看出来,消息体中包含了发送人的ID,头像,昵称等信息,接收人(群)的ID,头像,昵称等信息,并且消息类型是由 type 来区分的,一个 friend,一个group。但是我的消息体设计是不需要这么多字段的。

//base 中包含了 timestamp 和 mtype(消息类型) public class ChatRequestBody extends LayimBaseBody {     /**      * 接收者用户ID 或者群ID      * */     private String toId;     /**      * 消息内容      * */     private String content;     //getter setter  }

    为什么只要接收方ID和内容呢?因为,发送人就是当前登录用户,所以头像昵称可以从服务端获取,当然你想直接从客户端传给服务端也没有问题。另外由于群聊和单聊的结构差不多,只是type的区分。(当然群聊还有一个小细节处理,下文中会讲到)所以我们只要这么简单一个消息结构就可以满足客户端发送了。所以在LayIM发送事件中,编写如下代码:

//监听发送消息 layim.on('sendMessage', function(data){    var t = data.to.type=='friend';    socket.send({       mtype:(t?1:2),//用来解析服务器消息类型       content:data.mine.content,//消息内容       toid:data.to.id//具体接收人或者群ID    });    return; });

    哦了,客户端发送完了,服务端得解析吧。(下文中的内容稍微和第五篇中有些重复,不过没关系,着重细节处理)

    在LayimServerAioHandler中,代码执行到handleDetail时候,就需要对消息进行处理了。

//接收过来的json数据 String text = ByteUtil.toText(bytes); //解析数据消息 LayimMsgProperty property = Json.toBean(text,LayimMsgProperty.class); //获取到消息类型 byte type = property.getMtype(); //获取消息处理器 LayimAbsMsgProcessor processor = LayimMsgProcessorManager.getProcessor(type); boolean unknown = processor == null; if(!unknown) {              processor.process(websocketPacket, channelContext); } //这里应该增加未知消息处理

    获取消息处理器部分呢,其实就是初始化的时候将各种消息处理类实例化加入到一个hashmap中

  private static Map<String,LayimAbsMsgProcessor<?>> processorMap = new HashMap<String, LayimAbsMsgProcessor<?>>();    public static void init(){       //单聊消息处理       processorMap.put("CLIENT_TO_CLIENT",new ClientToClientMsgProcessor());       //群聊消息处理       processorMap.put("CLIENT_TO_GROUP",new ClientToGroupMsgProcessor());   }

    消息处理器接收到消息,进行处理,这里需要强调一下的是,消息转换厚的结果只是略有不同,比如,区分type是friend还是group,id的设置。其他都是一样的,最主要的区别在于,一个是给单个对象发送,说白了,就是调用send方法,另外一个是调用sendToGroup方法。下面看详细代码。

    @Override     public WsResponse process(WsRequest layimPacket, ChatRequestBody body, ChannelContext channelContext) throws Exception {          logger.info("ClientToClientMsgProcessor.process");          LayimToClientMsgBody msgBody = BodyConvert.getInstance().convertToClientMsgBody(body,channelContext);         WsResponse toClientBody = BodyConvert.getInstance().convertToTextResponse(msgBody);         //发送消息         send(channelContext,toClientBody,body.getToId());         return null;     }      /**      * 这个方法提出来的目的,是让 ClientToGroupMsgProcessor 进行重写      *(当然这么设计只是符合Layim,讲究通用性的话应该是分开设计比较好)      * */     public void send(ChannelContext channelContext,WsResponse toClientBody,String toId){         //调用t-io的接口获取对方的channel,如果没有的话,肯定是对方收不到消息的         ChannelContext toChannelContext = Aio.getChannelContextByUserid(channelContext.getGroupContext(),toId);         //发射~~~~         Aio.send(toChannelContext,toClientBody);     }

    中间消息体的转化部分,其中的注释已经很详细了。上文中提到群组发送的细节就是,需要带上from参数,否则自己也会接收到群组消息,然后会导致重复加载消息的情况。客户端会根据from进行筛消息处理。

public WsResponse convertToTextResponse(Object body) throws IOException{         WsResponse response = new WsResponse();         if(body != null) {             String json = Json.toJson(body);             response.setBody(ByteUtil.toBytes(json));             response.setWsBodyText(json);             response.setWsBodyLength(response.getWsBodyText().length());             //返回text类型消息(如果这里设置成 BINARY,那么客户端就需要进行解析了)             response.setWsOpcode(Opcode.TEXT);         }         return response;     }     public LayimToClientMsgBody convertToClientMsgBody(ChatRequestBody requestBody, ChannelContext channelContext){         LayimToClientMsgBody msgBody = new LayimToClientMsgBody();         //先获取用户信息         ContextUser contextUser =(ContextUser)channelContext.getAttribute(channelContext.getUserid());         //设置当前用户名         msgBody.setUsername(contextUser.getUsername());         //用户头像         msgBody.setAvatar(contextUser.getAvatar());         /**          * 这里要注意,如果是单聊,那么id为发送人id,否则为群组id          * 根据requestBody的msgType判断          * 当msgType==1 的时候,toId为接收人的ID          * 当msgType==2 的时候,toId为接收群的ID          * 这里有了if else 判断,当时也考虑用抽象类实现。          * */         if(requestBody.getMtype()==LayimMsgType.CLIENT_TO_CLIENT){             msgBody.setId(contextUser.getUserid());             //消息类型:好友             msgBody.setType(LayimConst.CHAT_TYPE_FRIEND);         }else if(requestBody.getMtype() == LayimMsgType.CLIENT_TO_GROUP){             msgBody.setId(requestBody.getToId());             //消息类型:群组             msgBody.setType(LayimConst.CHAT_TYPE_GROUP);             //群聊消息会让用户都收到信息,所以,自己的就不给自己显示了,需要客户端根据from字段进行处理,另外,单聊消息就不给赋值了,没必要。             msgBody.setFrom(channelContext.getUserid());         }         //消息内容         msgBody.setContent(requestBody.getContent());         //发送时间         msgBody.setTimestamp(requestBody.getTimestamp());         return msgBody;     }

    上文中讲了这么多,可能还会有很多同学云里雾里的,下面做一下演示,给大家看看流程打印。

    客户端打印:

    我们已经看到客户端打印出了消息。(为了测试,我让用户发送的消息在给自己发回来),然后直接调用layim的接口即可。 layim.getMessage(msg)

 var msg = JSON.parse(e.data);    layim.getMessage(msg);

    效果如下(临时会话的原因是因为我自己和自己发消息,如果是和好友的话,不会出现这种情况):

群聊

    下面在将一下群消息,群消息的处理和单聊差不多,只要,继承单聊消息处理器,重写 send方法即可

/**  * 这里由于群聊消息用的接收消息体都是ChatRequestBody,  * 所以,直接继承 ClientToClientMsgProcessor,并且重写process方法即可  * */ public class ClientToGroupMsgProcessor extends ClientToClientMsgProcessor {     private static final Logger logger = LoggerFactory.getLogger(ClientToGroupMsgProcessor.class);     public void send(ChannelContext channelContext,WsResponse toClientBody,String toId){         logger.info("execute ClientToGroupMsgProcessor.send");         Aio.sendToGroup(channelContext.getGroupContext(),toId,toClientBody);     } }

    群消息演示:

    问题1:我的消息重复了

    问题2:为什么左边的框也是我,但是消息在左边。

问题1解决:

    通过from参数进行判断,

问题2解决:

    写一个临时变量,判断是否该窗口发送的消息

 socket.on('msg',function (e) {      console.log("监听到事件:msg");                  var msg = JSON.parse(e.data);      //判断from是否为当前用户      if(msg.from==current_uid) {        //发送消息flag,发送的时候赋值为true,这样其他多开的窗口这个值就是false了。        if(selfFlag) {          selfFlag = false;          return;         }else{          //通过layim接口文档可以知道,加上mine参数为true,就是自己发的消息          msg['mine']=true;         }       }       layim.getMessage(msg);              });

    最后演示效果:

总结

    本文基本把群聊和单聊的各种细节以及消息处理流程介绍完了。OSChina 处女篇宣告完结。编辑起来还挺爽的,哈哈哈哈

参考资料:

    https://gitee.com/tywo45/t-io

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

    本文代码地址:https://github.com/fanpan26/SpringBootLayIM 欢迎star,博客会继续更新,最后再次感谢 t-io 原作者的优秀框架。

本文发表于2017年11月22日 08:33
(c)注:本文转载自https://my.oschina.net/panzi1/blog/1577007,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 2049 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1