t-io 入门篇(三)即时消息发送demo学习
t-io 入门篇(三)即时消息发送demo学习
卡尔码农 发表于4个月前
t-io 入门篇(三)即时消息发送demo学习
  1. 发表于 4个月前
  2. 阅读 5603
  3. 收藏 114
  4. 点赞 8
  5. 评论 14
摘要: T-io Im聊天demo分析

前言

     t-io作者在开源其框架的同时还附带了几个demo,如:简单的hello world、im等。接下来这篇博客将会围绕tio-examples-im-simple-client、tio-examples-im-simple-server展开分析和学习。

demo项目结构

    im-simple分成三个maven子项目:
  1. tio-examples-im-simple-client就是客户端聊天工具项目
  2. tio-examples-im-simple-common保存了一些公用的类型定义和utils,
  3. tio-examples-im-simple-server包含了聊天系统的服务端和一套附带了nginx的网页聊天工具。
   整片博客是围绕着网页版聊天工具而写的。

聊天系统

  • 概述
  •       首先介绍一下这个聊天系统demo大概交互的逻辑是怎么样子的,如下图:
    1. demo中web聊天系统使用了websocket协议来和服务端进行通信。
    2. demo中数据交互无论是浏览器端、还是服务端数据传输方式都使用了proto-buf(这个让我眼前一亮,之前只听说过客户端和服务器端采用proto-buf,demo里面竟然使用了js的proto-buf,因为之前接触js这一块不多,第一次看到就给跪了)。
        2.代码分析 
    1. 服务端初始化demo:
    .. // 定义handler,所有的请求数据全部都由这个handler来处理,decode/encode/handler等等 // 如果您做web开发一定知道dispatcher的概念,这个handler会将数据解码,然后将数据分发给对应的handler处理业务 aioHandler = new ImServerAioHandler(); // listenr 可以在连接上、接收到消息、发送消息后等等回调其内部方法 aioListener = new ImServerAioListener(); // 服务端上下文初始化 serverGroupContext = new ServerGroupContext<>(aioHandler, aioListener); serverGroupContext.setEncodeCareWithChannelContext(true); aioServer = new AioServer<>(serverGroupContext); aioServer.start(ip, port); .. 从初始化的几句代码中可以看出ImServerAioHandler 是聊天系统中的重中之重,我们来看一下里面的代码 /.. // 握手请求 handlerMap.put(Command.COMMAND_HANDSHAKE_REQ, new HandshakeReqHandler()); // 授权请求 handlerMap.put(Command.COMMAND_AUTH_REQ, new AuthReqHandler()); // 聊天请求 handlerMap.put(Command.COMMAND_CHAT_REQ, new ChatReqHandler()); // 加入群组请求 handlerMap.put(Command.COMMAND_JOIN_GROUP_REQ, new JoinReqHandler()); // 心跳请求 handlerMap.put(Command.COMMAND_HEARTBEAT_REQ, new HeartbeatReqHandler()); // 关闭连接请求 handlerMap.put(Command.COMMAND_CLOSE_REQ, new CloseReqHandler()); /.. /.. 在这里通过解析消息头之后获取下一步调用哪个handler来处理业务 public Object handler(ImPacket packet, ChannelContext<ImSessionContext, ImPacket, Object> channelContext) throws Exception { Command command = packet.getCommand(); ImBsHandlerIntf handler = handlerMap.get(command); if (handler != null) { Object obj = handler.handler(packet, channelContext); CommandStat.getCount(command).handled.incrementAndGet(); return obj; } else { CommandStat.getCount(command).handled.incrementAndGet(); log.warn("找不到对应的命令码[{}]处理类", command); return null; } } /.. 当然,上面的其实都是业务代码,还没有很明显的看到框架代码,接下来我们看看怎样发送一条聊天的消息给一个群组。
    1. ChatReqHandler.java是处理发送聊天消息的类:
    // protobuf反序列化消息体 ChatReqBody chatReqBody = ChatReqBody.parseFrom(packet.getBody()); // demo这里写死了一些信息,消息发送者 Integer fromId = 111; String fromNick = "test"; // 把消息发送给谁 Integer toId = chatReqBody.getToId(); String toNick = chatReqBody.getToNick(); String toGroup = chatReqBody.getGroup(); // 其实demo的代码这里写的不太好,如果chatReqBody==null,前面就报错了 if (chatReqBody != null) { // 构建消息体响应类 ChatRespBody.Builder builder = ChatRespBody.newBuilder(); builder.setType(chatReqBody.getType()); builder.setText(chatReqBody.getText()); builder.setFromId(fromId); builder.setFromNick(fromNick); builder.setToId(toId); builder.setToNick(toNick); builder.setGroup(toGroup); builder.setTime(SystemTimer.currentTimeMillis()); //同样protobuf序列化 ChatRespBody chatRespBody = builder.build(); byte[] bodybyte = chatRespBody.toByteArray(); //组建即时聊天响应包 ImPacket respPacket = new ImPacket(); respPacket.setCommand(Command.COMMAND_CHAT_RESP); respPacket.setBody(bodybyte); // 如果是对群组发送,直接调用Aio.sendToGroup即可(框架代码) if (Objects.equals(ChatType.CHAT_TYPE_PUBLIC, chatReqBody.getType())) { Aio.sendToGroup(channelContext.getGroupContext(), toGroup, respPacket); } else if (Objects.equals(ChatType.CHAT_TYPE_PRIVATE, chatReqBody.getType())) { // 如果是对单个人发送,也有Aio.sendToUser方法(框架代码) if (toId != null) { Aio.sendToUser(channelContext.getGroupContext(), toId + "", respPacket); } } }      看看,真正使用到框架的代码实际上就一行:Aio.sendToXXX静态方法。
    1. 那也许会有人问,框架是怎么知道这个group里面有哪些连接啊?框架怎么知道会有哪些连接的user呢?
         注意看众多handler中,其中有一个JoinReqHandler是在客户端调用加入群组命令的时候执行的: public Object handler(ImPacket packet, ChannelContext<ImSessionContext, ImPacket, Object> channelContext) throws Exception { // .. // 连接上下文绑定群组 Aio.bindGroup(channelContext, group); // .. }      一个简单的bindGroup即可把这个连接绑定到一个组里面,从而通过Aio.sendToGroup即可轻松的给一个组发送消息。而在AuthReqHandler中同样调用了Aio.bindUser方法来绑定有用户连接(这个不贴代码了,贴多了界面太长,让人不想看)。      保存user和group,作者使用的是一个带了读写锁的自己封装的DualHashBidiMap (apache工具包里面的,双向map,可以通过key获取value,可以通过value获取key)。

         总结t-io在聊天系统中的使用方法

    1. 创建了一个server端
    2. 客户端在鉴权的时候Aio.bindUser
    3. 客户端在加入群组的时候Aio.bindGroup
    4. 客户端在发送消息时Aio.sendToUser/Aio.sendToGroup即可
    5. 当然,只要你实现一下ClientAioHandler的heartbeatPacket方法,框架本身给实现了自动心跳检测,重连也只需要在初始化连接上下文时传入ReconnConf即可。
    6. 后面我自己设计config-server的时候,给客户端推送一组服务IP过去也是相当容易的事情啦。
    7. 棋牌类游戏交互也类似了,一桌四个人,一个的操作,发送到服务端做算法逻辑校验后,同时推送给其他三个人,好像这样实现起来也不复杂了。而需要关心的仅仅只是server本身的集群问题了。

        demo中我最感兴趣的和我学到的

    1. 框架提供的user和group概念很方便
    2. ByteBuffer消息头的定义方法和解析,demo里面定义消息头定义得很节省空间 ByteBuffer buffer = ByteBuffer.allocate(allLen); buffer.order(groupContext.getByteOrder()); //这里比较有迷惑性,这个version并非随便乱定,而是需要根据后面是否使用压缩、序号同步等等标记出来的二进制 buffer.put(ImPacket.VERSION); buffer.put((byte) packet.getCommand().getNumber()); buffer.put(isCompress ? (byte)1 : (byte)0); buffer.putInt(packet.getSynSeq()); buffer.putShort((short)bodyLen); 这里注意,我差点被作者绕进去了,ImPacket.VERSION比较有迷惑性 //这是客户端解析第一个字节的定义,不是浏览器解析规则 //其实作者定义得version自己规定的只占用4个比特位,如:0B00001111,后四位都是用来真正标示版本号 //实际上前面的几位是另外分出来标记是否压缩,是否同步序号等等 byte version = ImPacket.decodeVersion(firstbyte); boolean isCompress = ImPacket.decodeCompress(firstbyte); boolean hasSynSeq = ImPacket.decodeHasSynSeq(firstbyte); boolean is4ByteLength = ImPacket.decode4ByteLength(firstbyte);  
    3. 消息点对点发送和消息的群组发送性能和稳定性还有待我做做测试,后续在全面了解完t-io框架后准备弄一份关于性能测试的报告出来试试。
         同时感谢t-io作者对我的指导 !
    标签: java SocketServer t-io
    1. 打赏
    2. 点赞
    3. 收藏
    4. 分享
    共有 人打赏支持
    粉丝 73
    博文 14
    码字总数 18165
    评论 (14)
    小徐同学
    赞一个
    talent-tan
    写得超级棒,那个协议的第一个字节是略复杂,主要是为了节约带宽,所以我后面出了个im-simple版,就是简化了这个协议,因为很多人不会bit操作,所以也就看不懂这个协议的第一个字节。。。。博主这个都看明白了,玩t-io肯定会非常顺手!!!@天蓬小猪 @小徐同学 你们一定要关注这位博主。
    小徐同学

    引用来自“talent-tan”的评论

    写得超级棒,那个协议的第一个字节是略复杂,主要是为了节约带宽,所以我后面出了个im-simple版,就是简化了这个协议,因为很多人不会bit操作,所以也就看不懂这个协议的第一个字节。。。。博主这个都看明白了,玩t-io肯定会非常顺手!!!@天蓬小猪 @小徐同学 你们一定要关注这位博主。
    已经关注了。
    K不是你的帝
    之前用过netty搞了一个已经用于生产环境的客服系统,基于websocket,最近看到t-io超级火也跟着demo走了一遍,有种想重构客服的冲动了。。。
    K不是你的帝

    引用来自“talent-tan”的评论

    写得超级棒,那个协议的第一个字节是略复杂,主要是为了节约带宽,所以我后面出了个im-simple版,就是简化了这个协议,因为很多人不会bit操作,所以也就看不懂这个协议的第一个字节。。。。博主这个都看明白了,玩t-io肯定会非常顺手!!!@天蓬小猪 @小徐同学 你们一定要关注这位博主。
    大神有关于t-io后端集群,不同t-io服务之间通信的例子么,比如用户a绑定咋t-io-1上,用户b绑定在t-io-2上,那么a和b通信肯定要跨域2个t-io服务,我之前做类似功能块用了第三方工具来通信(比如redis),不知道有没有更好的实践方式?先谢谢您的tio项目。
    talent-tan

    引用来自“K不是你的帝”的评论

    之前用过netty搞了一个已经用于生产环境的客服系统,基于websocket,最近看到t-io超级火也跟着demo走了一遍,有种想重构客服的冲动了。。。
    如果你觉得netty顺手,似乎也没有必要转战t-io,当然如果觉得不顺手或者不爽,那欢迎尝试一下t-io
    talent-tan

    引用来自“talent-tan”的评论

    写得超级棒,那个协议的第一个字节是略复杂,主要是为了节约带宽,所以我后面出了个im-simple版,就是简化了这个协议,因为很多人不会bit操作,所以也就看不懂这个协议的第一个字节。。。。博主这个都看明白了,玩t-io肯定会非常顺手!!!@天蓬小猪 @小徐同学 你们一定要关注这位博主。

    引用来自“K不是你的帝”的评论

    大神有关于t-io后端集群,不同t-io服务之间通信的例子么,比如用户a绑定咋t-io-1上,用户b绑定在t-io-2上,那么a和b通信肯定要跨域2个t-io服务,我之前做类似功能块用了第三方工具来通信(比如redis),不知道有没有更好的实践方式?先谢谢您的tio项目。
    这个方案很多,譬如走jms,再或者用zk自己来实现类似的功能,根据自己掌握的技术情况进行灵活选择
    Dzqt

    引用来自“talent-tan”的评论

    写得超级棒,那个协议的第一个字节是略复杂,主要是为了节约带宽,所以我后面出了个im-simple版,就是简化了这个协议,因为很多人不会bit操作,所以也就看不懂这个协议的第一个字节。。。。博主这个都看明白了,玩t-io肯定会非常顺手!!!@天蓬小猪 @小徐同学 你们一定要关注这位博主。

    引用来自“K不是你的帝”的评论

    大神有关于t-io后端集群,不同t-io服务之间通信的例子么,比如用户a绑定咋t-io-1上,用户b绑定在t-io-2上,那么a和b通信肯定要跨域2个t-io服务,我之前做类似功能块用了第三方工具来通信(比如redis),不知道有没有更好的实践方式?先谢谢您的tio项目。

    引用来自“talent-tan”的评论

    这个方案很多,譬如走jms,再或者用zk自己来实现类似的功能,根据自己掌握的技术情况进行灵活选择
    受教了
    芝麻谷
    能不能解释下为什么说version具有迷惑性,看着一头雾水啊
    talent-tan
    博文细致之处
    1、读懂了im协议设计的精妙之处----为了节省带宽而用到了bit级别的编码解码粒度
    2、看懂了js中也是有protobuf的----对性能的极致追求让作者使用了protobuf协议作为websocket侧的格式
    3、写完后还小结一下,让新手很受益
    4、几乎没有咨询过t-io作者,就能掌握这么多核心的东西,博主水平:+1:
    talent-tan

    引用来自“芝麻谷”的评论

    能不能解释下为什么说version具有迷惑性,看着一头雾水啊
    version只占了首字节的后4bit,其它bit用作其它用途了,主要是为了节省带宽
    卡尔码农

    引用来自“芝麻谷”的评论

    能不能解释下为什么说version具有迷惑性,看着一头雾水啊
    version我们平常自己定义版本号可能就随意定义一个数字对不,他这个demo version使用了一个字节,8个二进制位,后四位是真正的版本号,前面四位是做了其他标示。相当于一个version在标示版本号的同时,也决定了数据解析方式,比如是否用了gzip
    talent-tan
    升级版的im web : http://www.t-io.org:9292/im/app/im/index.html
    守护天使

    引用来自“talent-tan”的评论

    写得超级棒,那个协议的第一个字节是略复杂,主要是为了节约带宽,所以我后面出了个im-simple版,就是简化了这个协议,因为很多人不会bit操作,所以也就看不懂这个协议的第一个字节。。。。博主这个都看明白了,玩t-io肯定会非常顺手!!!@天蓬小猪 @小徐同学 你们一定要关注这位博主。
    确实不错,学习了
    ×
    卡尔码农
    如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
    * 金额(元)
    ¥1 ¥5 ¥10 ¥20 其他金额
    打赏人
    留言
    * 支付类型
    微信扫码支付
    打赏金额:
    已支付成功
    打赏金额:
    利发国际官方网