从零一起学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 = 8081;
        heartBeatTimeout = 0;
        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

时间: 2024-10-02 04:24:38

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

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

项目背景 之前写过LayIM的.NET版后端实现,后来又写过一版Java的.当时用的是servlet,websocket和jdbc.虽然时间过去很久了,但是仍有些同学在关注.偶然间我听说了SpringBoot这么个东东,据说是省去了很多繁杂的配置.可以傻瓜式的创建项目,轻轻松松做出一个网站来,那么出于我对LayIM的情有独钟,于是乎想借用它来帮助我学习SpringBoot,并且全程记录,省的以后再走弯路和掌握解决问题的方法.(当然,我也是新手,我的解决方法就是百度,stackovreflow等网

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

转载:http://blog.csdn.net/linxingliang/article/details/51585921 此文章已经废弃,请看新版的博客的完美解决方案: 78. Spring Boot完美使用FastJson解析JSON数据[从零开始学Spring Boot] http://412887952-qq-com.iteye.com/blog/2315202

21. Spring Boot过滤器、监听器【从零开始学Spring Boot】

转载:http://blog.csdn.net/linxingliang/article/details/52069490 上一篇文章已经对定义Servlet 的方法进行了说明,过滤器(Filter)和 监听器(Listener)的注册方法和 Servlet 一样,不清楚的可以查看下上一篇文章(20): 本文将直接使用@WebFilter和@WebListener的方式,完成一个Filter 和一个 Listener:使用注解 @ServletComponentScan//这个就是扫描相应的Se

Spring Boot使用模板freemarker【从零开始学Spring Boot(转)

视频&交流平台: à SpringBoot网易云课堂视频 http://study.163.com/course/introduction.htm?courseId=1004329008 à Spring Boot交流平台 http://412887952-qq-com.iteye.com/blog/2321532 [原创文章,转载请注明出处] 103. Spring Boot Freemarker特别篇之contextPath[从零开始学Spring Boot] 最近有好久没有更新博客了,感谢

小代学Spring Boot之数据源

想要获取更多文章可以访问我的博客?-?代码无止境. 经过一天对Spring Boot的研究,小代同学已经对Spring Boot框架有了一个大概的认识.并且还创建了一个简单的Spring Boot的Web应用程序,如果你还不知道如何创建的话,可以访问<小代学Spring Boot之开篇>这篇文章.下一步陈Boss让小代做的是使用Spring Boot项目连接数据库. 相信我们都知道,在连接数据库的时候我们一般都会使用数据库连接池,这样做的好处在于可以重用数据库资源,还可以统一管理数据库连接,避

小代学Spring Boot之自定义Starter

想要获取更多文章可以访问我的博客?-?代码无止境. 使用Spring Boot框架一段时间之后的小代同学,发现在Spring Boot项目中经常会引入各种各样的Starter,例如Web项目的spring-boot-starter-web以及集成MyBatis时的mybatis-spring-boot-starter.那么这个Starter到底是些什么呢? 什么是Starter 经过一番研究,小代同学了解到Starter主要是Spring Boot用来简化项目依赖的一种形式,比如spring-b

Spring Boot 多模块项目创建与配置 (一) (转)

最近在负责的是一个比较复杂项目,模块很多,代码中的二级模块就有9个,部分二级模块下面还分了多个模块.代码中的多模块是用maven管理的,每个模块都使用spring boot框架.之前有零零散散学过一些maven多模块配置的知识,但没自己从头到尾创建和配置过,也快忘得差不多了.这次正好对照着这个项目,动手实践一下,下面我们就开始吧. maven多模块项目通常由一个父模块和若干个子模块构成,每个模块都对应着一个pom.xml.它们之间通过继承和聚合(也称作多模块)相互关联.多模块适用于一些比较大的项

Maven 搭建spring boot多模块项目

Maven 搭建spring boot多模块项目 备注:所有项目都在idea中创建 1.idea创建maven项目 1-1: 删除src,target目录,只保留pom.xml 1-2: 根目录pom.xml可被子模块继承,因此项目只是demo,未考虑太多性能问题,所以将诸多依赖 都写在根级`pom.xml`,子模块只需继承就可以使用. 1-3: 根级pom.xml文件在附录1 1-4: 依赖模块 mybatis spring-boot相关模块 2.创建子模块(module) 2-1: file

Spring Boot 入门之消息中间件篇(五)

原文地址:Spring Boot 入门之消息中间件篇(五) 博客地址:http://www.extlight.com 一.前言 在消息中间件中有 2 个重要的概念:消息代理和目的地.当消息发送者发送消息后,消息就被消息代理接管,消息代理保证消息传递到指定目的地. 我们常用的消息代理有 JMS 和 AMQP 规范.对应地,它们常见的实现分别是 ActiveMQ 和 RabbitMQ. 上篇文章<Spring Boot 入门之缓存和 NoSQL 篇(四)>. 二.整合 ActiveMQ 2.1 添