十次方社交项目-消息通知系统改进6

一、获取新消息通知

1. 获取新消息通知的两种模式

 用户获取新的消息通知有两种模式

  • 上线登录后向系统主动索取
  • 在线时系统向接收者主动推送新消息

 新消息提醒功能需要定时轮询接口的方式太低效,改进点如下

  1. 将新消息提醒数据由tb_notice_fresh表转移到rabbitmq中,减轻数据库访问压力

  2. 将轮询接口的伪推送改进为真正的使用全双工长连接进行的推送

    2.1 消息通知微服务加入netty框架,为页面中的websocket连接提供接入服务

    2.2 netty框架与rabbitmq对接,接收并下发新消息提醒数据

    2.2 将页面中的定时轮询接口代码替换为websocket连接和事件处理

2. 上线登录后向系统索取

 此模式是接受者请求系统,系统将新的消息通知返回给接收者的模式,流程如下:

  1)接收者向服务端netty请求WebSocket连接

  2)Netty服务把连接放到自己的连接池中

  3)Netty根据接受者信息向RabbitMQ查询消息

  4)如果有新消息,返回新消息通知

  5)使用WebSocket连接向接收者返回新消息的数量

  image

3. 在线时系统向接收者主动推送

 此模式是系统将新的消息通知返回给接收者的模式,流程如下:

  1)RabbitMQ将新消息数据推送给Netty

  2)Netty从连接池中取出接收者的WebSocket连接

  3)Netty通过接收者的WebSocket连接返回新消息的数量

二、文章订阅群发消息改进

文章订阅群发消息的改进步骤:

  1)准备RabbitMQ消息中间件

  2)改进文章订阅功能,创建RabbitMQ队列存放新消息通知

  3)改进发布文章后群发消息通知功能

  4)整合Netty和WebSocket实现双向通信

 在虚拟机中启动RabbitMQ

docker run -id --name=tensquare_rabbit -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 15672:15672 -p 25672:25672 rabbitmq:management

 访问地址:http://192.168.240.134:15672   登录账号: guest  登录密码: guest

1. 文章订阅功能改进

 1.1 修改文章微服务配置文件

  因为文章订阅功能需要增加Rabbitmq的交换机和队列的绑定、解绑等相关操作,所以需要让tensquare_article微服务具备操作Rabbitmq的能力。

  修改tensquare_article微服务的application.yml配置文件,在该文件中添加Rabbitmq相关的配置

  然后修改tensquare_article微服务的pom.xml项目配置文件,添加spring-boot-starter-amqp依赖

 1.2 修改文章订阅功能代码

  在ArticleService中原有的subscribe方法中,增加了几个业务逻辑

    • 定义Rabbitmq的direct类型的交换机
    • 定义用户的Rabbitmq队列
    • 将队列通过路由键绑定或解绑direct交换机

  改进后完整的subscribe方法如下

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public Boolean subscribe(String userId, String articleId) {
        //根据文章id查询文章作者id
        String authorId = articleDao.selectById(articleId).getUserid();

        //创建Rabbit管理器
        RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate.getConnectionFactory());
        //声明exchange,处理新增文章消息
        DirectExchange exchange = new DirectExchange("article_subscribe");
        rabbitAdmin.declareExchange(exchange);
        //创建queue,每个用户都有自己的队列,通过用户id进行区分
        Queue queue = new Queue("article_subscribe_"+userId, true);
        //声明exchange和queue的绑定关系,设置路由键为作者id,队列只收到对应作者的新增文章消息
        Binding binding = BindingBuilder.bind(queue).to(exchange).with(authorId);

        //存放用户订阅信息的集合key,里面存放作者id
        String userKey = "article_subscribe_"+userId;
        //存放作者订阅者信息的集合key,里面存放订阅者id
        String authorKey = "article_author_"+authorId;

        //查询该用户是否已经订阅作者
        //使用userKey查询redis查询不到是因为userkey存储的是编码后的\xac\xed\x00\x05t\x00\x10article_author_2
        Boolean flag = redisTemplate.boundSetOps(userKey).isMember(authorId);
        if(flag){
            //已经订阅,则取消订阅
            redisTemplate.boundSetOps(userKey).remove(authorId);
            redisTemplate.boundSetOps(authorKey).remove(userId);

            //删除队列绑定关系
            rabbitAdmin.removeBinding(binding);
            return false;
        }else {
            //没有订阅,则进行订阅
            redisTemplate.boundSetOps(userKey).add(authorId);
            redisTemplate.boundSetOps(authorKey).add(userId);

            //声明队列和绑定队列
            rabbitAdmin.declareQueue(queue);
            rabbitAdmin.declareBinding(binding);
            return true;
        }
    }

2. 发布文章触发群发消息

 在原有的处理逻辑中,增加向交换机发送Rabbitmq消息的业务逻辑。

 修改ArticleService中的 save方法,在新增方法的最后面添加下面的代码:

        //入库成功后后,发送mq消息,内容是消息通知id
        //第一个参数是交换机名,使用之前完成的订阅功能的交换机
        //第二个参数是路由键,使用文章作者的id作为路由键
        //第三个参数是消息内容,这里只完成新消息提醒,内容是文章id
        rabbitTemplate.convertAndSend("article_subscribe",authorId,id);

 删除消息通知微服务中的 新的通知提醒消息入库 逻辑,因为现在新通知由RabbitMQ发送。修改tensquare_notice微服务的NoticeService方法:

    public void save(Notice notice) {
        //初始化数据
        notice.setState("0");
        notice.setCreatetime(new Date());
        String id = idWorker.nextId()+"";
        notice.setId(id);
        //通知消息入库
        noticeDao.insert(notice);

        //删除消息通知微服务中的新的通知提醒消息入库逻辑,现在新通知由RabbitMQ发送
        //待推送消息入库,新的通知提醒消息
//        NoticeFresh noticeFresh = new NoticeFresh();
//        noticeFresh.setNoticeId(id);
//        noticeFresh.setUserId(notice.getReceiverId());
//        noticeFreshDao.insert(noticeFresh);
    }

三、IO编程

在开始了解Netty之前,先来实现一个客户端与服务端通信的程序,使用传统的IO编程和使用NIO编程有什么不一样。

1. 传统IO编程

 每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。阻塞I/O的通信模型示意图如下:

  0b7604a25817491817f93b68e441a197_io_model

 业务场景:客户端每隔两秒发送字符串给服务端,服务端收到之后打印到控制台。

 1.1 服务端实现:

package com.itheima.io;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

//传统IO编程,服务端
public class IOServer {

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8000);//端口不建议使用1024以下的端口
        while (true){
            //1. 使用阻塞的方式获取新的连接
            Socket socket = serverSocket.accept();

            //每个客户端连接的时候,创建新线程
            new Thread(){
                @Override
                public void run(){
                    String name = Thread.currentThread().getName();
                    try{
                        //2. 每一个新的连接都创建一个线程,负责读取数据
                        byte[] data = new byte[1024];
                        InputStream inputStream = socket.getInputStream();
                        while(true){
                            int len;
                            //3. 按字节流方式读取数据
                            while ((len=inputStream.read(data)) != -1){
                                System.out.println("线程"+name+":"+new String(data,0,len));
                            }
                        }
                    }catch (Exception e){
                    }
                }
            }.start();
        }
    }
}

 1.2 客户端实现:

package com.itheima.io;

import java.net.Socket;

//IO编程的客户端
public class MyClient {
    public static void main(String[] args) {
        //测试使用不同的线程数进行访问,5个线程
        for (int i = 0; i < 5; i++) {
            new ClientDemo().start();
        }
    }

    static class ClientDemo extends Thread{
        @Override
        public void run() {
            try{
                Socket socket = new Socket("127.0.0.1",8000);
                while (true){
                    socket.getOutputStream().write("测试数据".getBytes());
                    socket.getOutputStream().flush();
                    Thread.sleep(2000);
                }
            }catch (Exception e){
            }
        }
    }
}

  先执行服务端,再执行客户端,可以发现每隔2秒打印一次,每次打印5个线程信息

   image

 1.3 这样做存在的问题

  从服务端代码中我们可以看到,在传统的IO模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个while死循环。

  如果在用户数量较少的情况下运行是没有问题的,但是对于用户数量比较多的业务来说,服务端可能需要支撑成千上万的连接,IO模型可能就不太合适了。

  如果有1万个连接就对应1万个线程,继而1万个while死循环,这种模型存在以下问题:

    • 当客户端越多,就会创建越多的处理线程。线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费。并且如果务器遭遇洪峰流量冲击,例如双十一活动,线程池会瞬间被耗尽,导致服务器瘫痪。
    • 因为是阻塞式通信,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
    • IO编程中数据读写是以字节流为单位,效率不高。

2. NIO编程

 NIO,也叫做new-IO或者non-blocking-IO,可理解为非阻塞IO。

 2.1 NIO编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责,我们用一幅图来对比一下IO与NIO:

   1effdc7230abb6264b111d44193f78f3_io_nio

  如上图所示,IO模型中,一个连接都会创建一个线程,对应一个while死循环,死循环的目的就是不断监测这条连接上是否有数据可以读。但是在大多数情况下,1万个连接里面同一时刻只有少量的连接有数据可读,因此,很多个while死循环都白白浪费掉了,因为没有数据。

  而在NIO模型中,可以把这么多的while死循环变成一个死循环,这个死循环由一个线程控制。这就是NIO模型中选择器(Selector)的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到选择器上,通过检查这个选择器,就可以批量监测出有数据可读的连接,进而读取数据。

  举个栗子,在一家餐厅里,客人有点菜的需求,一共有100桌客人,有两种方案可以解决客人点菜的问题:

    • 方案一:每桌客人配一个服务生,每个服务生就在餐桌旁给客人提供服务。如果客人要点菜,服务生就可以立刻提供点菜的服务。那么100桌客人就需要100个服务生提供服务,这就是IO模型,一个连接对应一个线程。

    • 方案二:一个餐厅只有一个服务生(假设服务生可以忙的过来)。这个服务生隔段时间就询问所有的客人是否需要点菜,然后每一时刻处理所有客人的点菜要求。这就是NIO模型,所有客人都注册到同一个服务生,对应的就是所有的连接都注册到一个线程,然后批量轮询。

  这就是NIO模型解决线程资源受限的方案,实际开发过程中,我们会开多个线程,每个线程都管理着一批连接,相对于IO模型中一个线程管理一条连接,消耗的线程资源大幅减少。

 2.2 NIO的三大核心组件:通道(Channel)、缓冲(Buffer)、选择器(Selector)

  • 通道(Channel)

    是传统IO中的Stream(流)的升级版。Stream是单向的、读写分离(inputstream和outputstream),Channel是双向的,既可以进行读操作,又可以进行写操作。

  • 缓冲(Buffer)

    Buffer可以理解为一块内存区域,可以写入数据,并且在之后读取它。

  • 选择器(Selector)

    选择器(Selector)可以实现一个单独的线程来监控多个注册在她上面的信道(Channel),通过一定的选择机制,实现多路复用的效果。

 2.3 NIO相对于IO的优势:

  1. IO是面向流的,每次都是从操作系统底层一个字节一个字节地读取数据,并且数据只能从一端读取到另一端,不能前后移动流中的数据。NIO则是面向缓冲区的,每次可以从这个缓冲区里面读取一块的数据,并且可以在需要时在缓冲区中前后移动。
  2. IO是阻塞的,这意味着,当一个线程读取数据或写数据时,该线程被阻塞,直到有一些数据被读取,或数据完全写入,在此期间该线程不能干其他任何事情。而NIO是非阻塞的,不需要一直等待操作完成才能干其他事情,而是在等待的过程中可以同时去做别的事情,所以能最大限度地使用服务器的资源。
  3. NIO引入了IO多路复用器selector。selector是一个提供channel注册服务的线程,可以同时对接多个channel,并在线程池中为channel适配、选择合适的线程来处理channel。由于NIO模型中线程数量大大降低,线程切换效率因此也大幅度提高。

  2.4 和前面一样的场景,使用NIO实现(复制代码演示效果即可):

package com.itheima.io;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws Exception {
        //负责轮询是否有新的连接
        Selector serverSelector = Selector.open();
        //负责轮询处理连接中的数据
        Selector clientSelector = Selector.open();

        new Thread(){
            @Override
            public void run() {
                //对应IO编程中服务端启动
                ServerSocketChannel listenerChannel = null;
                try {
                    listenerChannel = ServerSocketChannel.open();
                    listenerChannel.socket().bind(new InetSocketAddress(8000));
                    listenerChannel.configureBlocking(false);
                    //OP_ACCEPT表示服务器监听到了客户端连接,服务器可以接收这个连接了。channel注册到selector
                    listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
                    while (true){
                        //监测是否有新的连接,这里的1指的是阻塞的时间为1ms
                        if(serverSelector.select(1)>0){
                            Set<SelectionKey> set = serverSelector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.iterator();
                            while (keyIterator.hasNext()){
                                SelectionKey key = keyIterator.next();
                                if(key.isAcceptable()){
                                    try{
                                        //1. 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
                                        SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                        clientChannel.configureBlocking(false);
                                        //OP_READ表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
                                        clientChannel.register(clientSelector,SelectionKey.OP_READ);
                                    }finally {
                                        keyIterator.remove();
                                    }
                                }
                            }
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                try{
                    while (true){
                        //2. 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为1ms
                        if(clientSelector.select(1)>0){
                            Set<SelectionKey> set = clientSelector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.iterator();
                            while (keyIterator.hasNext()) {
                                SelectionKey key = keyIterator.next();
                                if(key.isReadable()){
                                    try{
                                        SocketChannel clientChannel = (SocketChannel) key.channel();
                                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                        //3. 读取数据以块为单位批量读取
                                        clientChannel.read(byteBuffer);
                                        byteBuffer.flip();
                                        //每隔2秒输出5条数据:线程Thread-1:测试数据
                                        System.out.println("线程"+name+":"+ Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
                                    }finally {
                                        keyIterator.remove();
                                        key.interestOps(SelectionKey.OP_READ);
                                    }
                                }
                            }
                        }
                    }
                }catch (IOException ignored){
                }
            }
        }.start();
    }
}

  先执行NIOServer,再执行IO中的MyClient,查看结果:每隔2秒打印5条数据,且只有一个线程

   image

五、Netty

1. 为什么使用Netty

 我们已经有了NIO能够提高程序效率了,为什么还要使用Netty?

 简单的说:Netty封装了JDK的NIO,让你用得更爽,你不用再写一大堆复杂的代码了。

 官方术语:Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

 下面是使用Netty不使用JDK原生NIO的一些原因:

  • 使用JDK自带的NIO需要了解太多的概念,编程复杂
  • Netty底层IO模型随意切换,而这一切只需要做微小的改动,就可以直接从NIO模型变身为IO模型
  • Netty自带的拆包解包,异常检测等机制,可以从NIO的繁重细节中脱离出来,只需要关心业务逻辑
  • Netty解决了JDK的很多包括空轮询在内的bug
  • Netty底层对线程,selector做了很多细小的优化,精心设计的线程模型做到非常高效的并发处理
  • 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
  • Netty社区活跃,遇到问题随时邮件列表或者issue
  • Netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大

 和IO编程一样的案例:

 添加Netty依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.5.Final</version>
</dependency>

 服务端:

package com.itheima.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;

public class NettyServer {
    public static void main(String[] args) {
        //用于接收客户端的连接以及为已接收的连接创建子通道,一般用于服务端。
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        // EventLoopGroup包含有多个EventLoop的实例,用来管理event Loop的组件,可以理解为一个线程池,内部维护了一组线程。
        // 接收新连接线程,处理新的连接
        NioEventLoopGroup boos = new NioEventLoopGroup();
        // 读取数据的线程,处理数据
        NioEventLoopGroup worker = new NioEventLoopGroup();

     //服务端执行 serverBootstrap .group(boos, worker) // Channel对网络套接字的I/O操作,例如读、写、连接、绑定等操作进行适配和封装的组件。 .channel(NioServerSocketChannel.class) // ChannelInitializer用于对刚创建的channel进行初始化,将ChannelHandler添加到channel的ChannelPipeline处理链路中。 .childHandler(new ChannelInitializer
<NioSocketChannel>() { //初始化channel的方法 protected void initChannel(NioSocketChannel ch) { // 组件从流水线头部进入,流水线上的工人按顺序对组件进行加工,到达流水线尾部时商品组装完成。 // 流水线相当于ChannelPipeline // 流水线工人相当于ChannelHandler ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new StringDecoder()); pipeline.addLast(new SimpleChannelInboundHandler<String>() { //自己指定流水线工人可以干什么事 @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println(msg); } }); } }) .bind(8000); } }

 客户端:

package com.itheima.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        // 不接受新的连接,并且是在父通道类完成一些操作,一般用于客户端的。
        Bootstrap bootstrap = new Bootstrap();
        // EventLoopGroup包含有多个EventLoop的实例,用来管理event Loop的组件
        NioEventLoopGroup group = new NioEventLoopGroup();
        //客户端执行
        bootstrap.group(group)
                // Channel对网络套接字的I/O操作,
                // 例如读、写、连接、绑定等操作进行适配和封装的组件。
                .channel(NioSocketChannel.class)
                // 用于对刚创建的channel进行初始化,
                // 将ChannelHandler添加到channel的ChannelPipeline处理链路中。
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        // 组件从流水线头部进入,流水线上的工人按顺序对组件进行加工
                        // 流水线相当于ChannelPipeline
                        // 流水线工人相当于ChannelHandler
                        ch.pipeline().addLast(new StringEncoder());
                    }
                });
        //客户端连接服务端
        Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();

        while (true) {
            // 客户端使用writeAndFlush方法向服务端发送数据,返回的是ChannelFuture
            // 与jdk中线程的Future接口类似,即实现并行处理的效果
            // 可以在操作执行成功或失败时自动触发监听器中的事件处理方法。
            ChannelFuture channelFuture = channel.writeAndFlush("测试数据");
            Thread.sleep(2000);
        }
    }
}

  先执行NettyServer再执行NettyClient,查看结果:NettyServer控制台每隔2秒打印一条数据

   image

2. Netty的事件驱动

 例如很多系统都会提供 onClick() 事件,这个事件就代表鼠标按下事件。事件驱动模型的大体思路如下:

  1. 有一个事件队列;
  2. 鼠标按下时,往事件队列中增加一个点击事件;
  3. 有个事件泵,不断循环从队列取出事件,根据不同的事件,调用不同的函数;
  4. 事件一般都各自保存各自的处理方法的引用。这样,每个事件都能找到对应的处理方法;

 22f2a8b20c678ea6fa307d46fd855a24_1563674114568

 为什么使用事件驱动?

  • 程序中的任务可以并行执行

  • 任务之间高度独立,彼此之间不需要互相等待

  • 在等待的事件到来之前,任务不会阻塞

 Netty使用事件驱动的方式作为底层架构,包括:

  • 事件队列(event queue):接收事件的入口。
  • 分发器(event mediator):将不同的事件分发到不同的业务逻辑单元。
  • 事件通道(event channel):分发器与处理器之间的联系渠道。
  • 事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作。

 a2689af8e9c79a29bb56e2760a2dd589_netty1234dfgs

3. 核心组件

 Netty 的功能特性图:

  66cf82fadbb7ff7a85752c1b4be23950_Netty%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7%E5%9B%BE

 Netty 功能特性:

  • 传输服务,支持 BIO 和 NIO。
  • 容器集成:支持 OSGI、JBossMC、Spring、Guice 容器。
  • 协议支持:HTTP、Protobuf、二进制、文本、WebSocket 等,支持自定义协议。

 BIO和NIO的区别:

  image

 Netty框架包含如下的组件:

  • ServerBootstrap :用于接受客户端的连接以及为已接受的连接创建子通道,一般用于服务端。
  • Bootstrap:不接受新的连接,并且是在父通道类完成一些操作,一般用于客户端的。
  • Channel:对网络套接字的I/O操作,例如读、写、连接、绑定等操作进行适配和封装的组件。
  • EventLoop:处理所有注册其上的channel的I/O操作。通常情况一个EventLoop可为多个channel提供服务。
  • EventLoopGroup:包含有多个EventLoop的实例,用来管理 event Loop 的组件,可以理解为一个线程池,内部维护了一组线程。
  • ChannelHandler和ChannelPipeline:例如一个流水线车间,当组件从流水线头部进入,穿越流水线,流水线上的工人按顺序对组件进行加工,到达流水线尾部时商品组装完成。流水线相当于ChannelPipeline,流水线工人相当于ChannelHandler,源头的组件当做event。
  • ChannelInitializer:用于对刚创建的channel进行初始化,将ChannelHandler添加到channel的ChannelPipeline处理链路中。
  • ChannelFuture:与jdk中线程的Future接口类似,即实现并行处理的效果。可以在操作执行成功或失败时自动触发监听器中的事件处理方法。

六、整合Netty和WebSocket

我们需要使用netty对接websocket连接,实现双向通信,这一步需要有服务端的netty程序,用来处理客户端的websocket连接操作,例如建立连接,断开连接,收发数据等。

1. 修改配置

 1.1 修改消息通知微服务模块tensquare_notice的pom文件,添加下面的dependency依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>       
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.5.Final</version>
</dependency>

 1.2 修改application.yml文件,添加下面的配置

spring:
  rabbitmq:
    host: 192.168.240.134

 这样消息通知微服务就引入了netty框架,并且具有了和Rabbitmq交互的能力

2. 实现Netty的整合

 2.1 整合分析

  现在的通讯模式如下:

   image

 因为使用到了WebSocket和Netty,整合方式和以前有所不同,整合步骤:

  1. 编写NettyServer,启动Netty服务。
  2. 使用配置Bean创建Netty服务。编写NettyConfig
  3. 编写和WebSocket进行通讯处理类MyWebSocketHandler,进行MQ和WebSocket的消息处理。
  4. 使用配置Bean创建Rabbit监听器容器,使用监听器。编写RabbitConfig
  5. 编写Rabbit监听器SysNoticeListener,用来获取MQ消息并进行处理。

 五个类的关系如下图:

  614d92050a7a5b585387a32c33e4dd70_1563766620774

 2.2 实现整合

  1) 在com.tensquare.notice.config包下创建ApplicationContextProvider类

   这个类是工具类,作用是获取Spring容器中的实例

package com.tensquare.notice.config;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextProvider implements ApplicationContextAware {
    /**
     * 上下文对象实例
     */
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 获取applicationContext
     *
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 通过name获取 Bean.
     *
     * @param name
     * @return
     */
    public Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    /**
     * 通过class获取Bean.
     *
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * 通过name,以及Clazz返回指定的Bean
     *
     * @param name
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}
View Code

  2) 编写NettyServer

   创建com.tensquare.notice.netty包,包下创建NettyServer类

package com.tensquare.notice.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

public class NettyServer {

    public void start(int port) {
        System.out.println("准备启动Netty。。。");
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        //用来处理新连接的
        EventLoopGroup boos = new NioEventLoopGroup();
        //用来处理业务逻辑的,读写。。。
        EventLoopGroup worker = new NioEventLoopGroup();

        serverBootstrap.group(boos, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        //请求消息解码器
                        ch.pipeline().addLast(new HttpServerCodec());
                        // 将多个消息转换为单一的request或者response对象
                        ch.pipeline().addLast(new HttpObjectAggregator(65536));
                        //处理WebSocket的消息事件
                        ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));

                        //创建自己的webSocket处理器,就是用来编写业务逻辑的
                        MyWebSocketHandler myWebSocketHandler = new MyWebSocketHandler();
                        ch.pipeline().addLast(myWebSocketHandler);
                    }
                }).bind(port);
    }
}

  3) 编写NettyConfig

   在com.tensquare.notice.config中编写

package com.tensquare.notice.config;

import com.tensquare.notice.netty.NettyServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class NettyConfig {

    @Bean
    public NettyServer createNettyServer() {
        NettyServer nettyServer = new NettyServer();

        //启动Netty服务,使用新的线程启动
        new Thread() {
            @Override
            public void run() {
                nettyServer.start(1234);//1024以上的端口
            }
        }.start();
        return nettyServer;
    }
}

  4) 编写MyWebSocketHandler

    com.tensquare.notice.netty包下编写

package com.tensquare.notice.netty;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tensquare.entity.Result;
import com.tensquare.entity.StatusCode;
import com.tensquare.notice.config.ApplicationContextProvider;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private static ObjectMapper MAPPER = new ObjectMapper();

    //从Spring容器中获取消息监听器容器,处理订阅消息sysNotice
    SimpleMessageListenerContainer sysNoticeContainer = (SimpleMessageListenerContainer) ApplicationContextProvider.getApplicationContext()
            .getBean("sysNoticeContainer");
    SimpleMessageListenerContainer userNoticeContainer = (SimpleMessageListenerContainer) ApplicationContextProvider.getApplicationContext()
            .getBean("userNoticeContainer");


    //从Spring容器中获取RabbitTemplate
    RabbitTemplate rabbitTemplate = ApplicationContextProvider.getApplicationContext().getBean(RabbitTemplate.class);

    //存放WebSocket连接Map,根据用户id存放
    public static ConcurrentHashMap<String, Channel> userChannelMap = new ConcurrentHashMap();

    //用户请求WebSocket服务端,执行的方法
    //第一次请求的时候,需要建立WebSocket连接
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        //约定用户第一次请求携带的数据:{"userId":"1"}
        //获取用户请求数据并解析
        String json = msg.text();
        //解析json数据,获取用户id
        String userId = MAPPER.readTree(json).get("userId").asText();

        //第一次请求的时候,需要建立WebSocket连接
        Channel channel = userChannelMap.get(userId);
        if (channel == null) {
            //获取WebSocket的连接
            channel = ctx.channel();

            //把连接放到容器中
            userChannelMap.put(userId, channel);
        }

        //只用完成新消息的提醒即可,只需要获取消息的数量
        //获取RabbitMQ的消息内容,并发送给用户
        RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate);
        //获取订阅类消息
        //拼接获取队列名称
        String queueName = "article_subscribe_" + userId;
        //获取Rabbit的Properties容器
        Properties queueProperties = rabbitAdmin.getQueueProperties(queueName);

        //获取消息数量
        int noticeCount = 0;
        //判断Properties是否不为空
        if (queueProperties != null) {
            // 如果不为空,获取消息的数量
            noticeCount = (int) queueProperties.get("QUEUE_MESSAGE_COUNT");
        }
        //获取点赞类消息
        String userQueueName = "article_thumbup_"+userId;
        Properties userQueueProperties = rabbitAdmin.getQueueProperties(userQueueName);
        int userNoticeCount = 0;
        if(userQueueProperties != null){
            userNoticeCount = (int) userQueueProperties.get("QUEUE_MESSAGE_COUNT");
        }

        //封装返回的数据
        HashMap countMap = new HashMap();
        //订阅类消息数量
        countMap.put("sysNoticeCount", noticeCount);
        //点赞类消息数量
        countMap.put("userNoticeCount",userNoticeCount);
        Result result = new Result(true, StatusCode.OK, "查询成功", countMap);

        //把数据发送给用户
        channel.writeAndFlush(new TextWebSocketFrame(MAPPER.writeValueAsString(result)));

        //把消息从队列里面清空,否则MQ消息监听器会再次消费一次
        if (noticeCount > 0) {
            rabbitAdmin.purgeQueue(queueName, true);
        }
        if(userNoticeCount > 0){
            rabbitAdmin.purgeQueue(userQueueName,true);
        }

        //为用户的消息通知队列注册监听器,便于用户在线的时候,
        //一旦有消息,可以主动推送给用户,不需要用户请求服务器获取数据
        sysNoticeContainer.addQueueNames(queueName); //运行时添加监听队列
        userNoticeContainer.addQueueNames(userQueueName);
    }
}

  5) 编写RabbitConfig

   在com.tensquare.notice.config中编写

package com.tensquare.notice.config;

import com.tensquare.notice.listener.SysNoticeListener;
import com.tensquare.notice.listener.UserNoticeListener;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {

    @Bean("sysNoticeContainer")
    public SimpleMessageListenerContainer createSys(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        //使用Channel监听,接收消息和发送消息用同一个channel。false,发消息时是用一个新的channel。
        container.setExposeListenerChannel(true);
        //设置自己编写的监听器
        container.setMessageListener(new SysNoticeListener());

        return container;
    }

    @Bean("userNoticeContainer")
    public SimpleMessageListenerContainer createUser(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        //使用Channel监听
        container.setExposeListenerChannel(true);
        //设置自己编写的监听器
        container.setMessageListener(new UserNoticeListener());

        return container;
    }
}

  6) 编写SysNoticeListener、UserNoticeListener 

   在com.tensquare.notice.listener中编写:

package com.tensquare.notice.listener;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import com.tensquare.entity.Result;
import com.tensquare.entity.StatusCode;
import com.tensquare.notice.netty.MyWebSocketHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import java.util.HashMap;

public class SysNoticeListener implements ChannelAwareMessageListener {

    private static ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        //获取用户id,可以通过队列名称获取
        String queueName = message.getMessageProperties().getConsumerQueue();
        String userId = queueName.substring(queueName.lastIndexOf("_") + 1);

        io.netty.channel.Channel wsChannel = MyWebSocketHandler.userChannelMap.get(userId);

        //判断用户是否在线
        if (wsChannel != null) {
            //如果连接不为空,表示用户在线
            //封装返回数据
            HashMap countMap = new HashMap();
            countMap.put("sysNoticeCount", 1);
            Result result = new Result(true, StatusCode.OK, "查询成功", countMap);

            // 把数据通过WebSocket连接主动推送用户
            wsChannel.writeAndFlush(new TextWebSocketFrame(MAPPER.writeValueAsString(result)));
        }
    }
}
package com.tensquare.notice.listener;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import com.tensquare.entity.Result;
import com.tensquare.entity.StatusCode;
import com.tensquare.notice.netty.MyWebSocketHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import java.util.HashMap;

public class UserNoticeListener implements ChannelAwareMessageListener {

    private static ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        //获取用户id,可以通过队列名称获取
        String queueName = message.getMessageProperties().getConsumerQueue();
        String userId = queueName.substring(queueName.lastIndexOf("_") + 1);

        io.netty.channel.Channel wsChannel = MyWebSocketHandler.userChannelMap.get(userId);

        //判断用户是否在线
        if (wsChannel != null) {
            //如果连接不为空,表示用户在线
            //封装返回数据
            HashMap countMap = new HashMap();
            countMap.put("userNoticeCount", 1);
            Result result = new Result(true, StatusCode.OK, "查询成功", countMap);

            // 把数据通过WebSocket连接主动推送用户
            wsChannel.writeAndFlush(new TextWebSocketFrame(MAPPER.writeValueAsString(result)));
        }
    }
}

  7)修改启动类,添加Netty服务的启动

public static void main(String[] args) {
    SpringApplication.run(NoticeApplication.class, args);
​
    NettyServer server = ApplicationContextProvider.getApplicationContext().getBean(NettyServer.class);
    try {
        server.start(12345);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

  8)在resources目录下创建index.html,这个html是测试页面

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>测试 notice 微服务与页面 websocket 交互</title>
</head>
<body>
<h1>
    websocket连接服务器获取mq消息测试
</h1>
<form onSubmit="return false;">
    <table><tr>
        <td><span>服务器地址:</span></td>
        <td><input type="text" id="serverUrl" value="ws://127.0.0.1:1234/ws" /></td>
      </tr>
      <tr>
        <td><input type="button" id="action" value="连接服务器" onClick="connect()" /></td>
        <td><input type="text" id="connStatus" value="未连接 ......" /></td>
    </tr></table>
    <br />
    <hr color="blue" />
    <div>
        <div style="width: 50%;float:left;">
            <div>
                <table><tr>
                    <td><h3>发送给服务端的消息</h3></td>
                    <td><input type="button" value="发送" onClick="send(this.form.message.value)" /></td>
                </tr></table>
            </div>
            <div><textarea type="text" name="message" style="width:500px;height:300px;">
{
    "userId":"1"
}
                </textarea></div>
        </div>
        <div style="width: 50%;float:left;">
            <div><table>
                <tr>
                    <td><h3>服务端返回的应答消息</h3></td>
                </tr>
            </table></div>
            <div><textarea id="responseText" name="responseText" style="width: 500px;height: 300px;" onfocus="this.scrollTop = this.scrollHeight ">
这里显示服务器推送的信息
            </textarea></div>
        </div>
    </div>

</form>

<script type="text/javascript">
    var socket;
    var connStatus = document.getElementById('connStatus');;
    var respText = document.getElementById('responseText');
    var actionBtn = document.getElementById('action');
    var sysCount = 0;
    var userCount = 0;

    function connect() {
        connStatus.value = "正在连接 ......";

        if(!window.WebSocket){
            window.WebSocket = window.MozWebSocket;
        }
        if(window.WebSocket){

            socket = new WebSocket("ws://127.0.0.1:1234/ws");

            socket.onmessage = function(event){
                respText.scrollTop = respText.scrollHeight;
                respText.value += "\r\n" + event.data;
                var sysData = JSON.parse(event.data).data.sysNoticeCount;
                if(sysData){
                    sysCount = sysCount + parseInt(sysData)
                }
                var userData = JSON.parse(event.data).data.userNoticeCount;
                if(userData){
                    userCount = userCount + parseInt(userData)
                }
                respText.value += "\r\n现在有" + sysCount + "条订阅新消息";
                respText.value += "\r\n现在有" + userCount + "条点赞新消息";
                respText.scrollTop = respText.scrollHeight;
            };
            socket.onopen = function(event){
                respText.value += "\r\nWebSocket 已连接";
                respText.scrollTop = respText.scrollHeight;

                connStatus.value = "已连接 O(∩_∩)O";

                actionBtn.value = "断开服务器";
                actionBtn.onclick =function(){
                    disconnect();
                };

            };
            socket.onclose = function(event){
                respText.value += "\r\n" + "WebSocket 已关闭";
                respText.scrollTop = respText.scrollHeight;

                connStatus.value = "已断开";

                actionBtn.value = "连接服务器";
                actionBtn.onclick = function() {
                    connect();
                };
            };

        } else {
            //alert("您的浏览器不支持WebSocket协议!");
            connStatus.value = "您的浏览器不支持WebSocket协议!";
        }
    }

    function disconnect() {
        socket.close();
    }

    function send(message){
        if(!window.WebSocket){return;}
        if(socket.readyState == WebSocket.OPEN){
            socket.send(message);
        }else{
            alert("WebSocket 连接没有建立成功!");
        }
    }
</script>
</body>
</html>
View Code

  9)启动tensquare-eureka,tensquare-user,tensquare-article,tensquare-notice四个微服务进行测试

   当新增一个文章数据的时候,就会发消息,最终页面显示的效果:

   image

七、文章点赞点对点消息改进

1. 文章点赞功能改进

 在ArticleService中原有的thumbup方法中,增加向用户的点对点消息队列发送消息 的功能

 改进后完整的代码如下

    public void thumbup(String articleId,String userId) {
        //文章点赞
        Article article = articleDao.selectById(articleId);
        article.setThumbup(article.getThumbup()+1);
        articleDao.updateById(article);

        //消息通知
        Notice notice = new Notice();
        notice.setReceiverId(article.getUserid());
        notice.setOperatorId(userId);
        notice.setAction("thumbup");
        notice.setTargetType("article");
        notice.setTargetId(articleId);
        notice.setCreatetime(new Date());
        notice.setType("user");
        notice.setState("0");
        noticeClient.save(notice);

        //点赞成功后,需要发送消息给文章作者(点对点消息)
        //1. 创建Rabbit管理器
        RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate.getConnectionFactory());
        //2. 创建队列,每个作者都有自己的队列,通过作者id进行区分
        Queue queue = new Queue("article_thumbup_" + article.getUserid(), true);
        rabbitAdmin.declareQueue(queue);
        //3. 发送消息到队列中
        rabbitTemplate.convertAndSend("article_thumbup_"+article.getUserid(),articleId);
    }

2. 消息通知改进

 1) 在com.tensquare.notice.listeners包下新建 UserNoticeListener类,添加代码

 2)RabbitConfig改造

package com.tensquare.notice.config;

import com.tensquare.notice.listener.SysNoticeListener;
import com.tensquare.notice.listener.UserNoticeListener;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {

    @Bean("sysNoticeContainer")
    public SimpleMessageListenerContainer createSys(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        //使用Channel监听,接收消息和发送消息用同一个channel。false,发消息时是用一个新的channel。
        container.setExposeListenerChannel(true);
        //设置自己编写的监听器
        container.setMessageListener(new SysNoticeListener());

        return container;
    }

    @Bean("userNoticeContainer")
    public SimpleMessageListenerContainer createUser(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        //使用Channel监听
        container.setExposeListenerChannel(true);
        //设置自己编写的监听器
        container.setMessageListener(new UserNoticeListener());

        return container;
    }
}
View Code

 3) MyWebSocketHandler改造

package com.tensquare.notice.netty;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tensquare.entity.Result;
import com.tensquare.entity.StatusCode;
import com.tensquare.notice.config.ApplicationContextProvider;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private static ObjectMapper MAPPER = new ObjectMapper();

    //从Spring容器中获取消息监听器容器,处理订阅消息sysNotice
    SimpleMessageListenerContainer sysNoticeContainer = (SimpleMessageListenerContainer) ApplicationContextProvider.getApplicationContext()
            .getBean("sysNoticeContainer");
    SimpleMessageListenerContainer userNoticeContainer = (SimpleMessageListenerContainer) ApplicationContextProvider.getApplicationContext()
            .getBean("userNoticeContainer");


    //从Spring容器中获取RabbitTemplate
    RabbitTemplate rabbitTemplate = ApplicationContextProvider.getApplicationContext().getBean(RabbitTemplate.class);

    //存放WebSocket连接Map,根据用户id存放
    public static ConcurrentHashMap<String, Channel> userChannelMap = new ConcurrentHashMap();

    //用户请求WebSocket服务端,执行的方法
    //第一次请求的时候,需要建立WebSocket连接
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        //约定用户第一次请求携带的数据:{"userId":"1"}
        //获取用户请求数据并解析
        String json = msg.text();
        //解析json数据,获取用户id
        String userId = MAPPER.readTree(json).get("userId").asText();

        //第一次请求的时候,需要建立WebSocket连接
        Channel channel = userChannelMap.get(userId);
        if (channel == null) {
            //获取WebSocket的连接
            channel = ctx.channel();

            //把连接放到容器中
            userChannelMap.put(userId, channel);
        }

        //只用完成新消息的提醒即可,只需要获取消息的数量
        //获取RabbitMQ的消息内容,并发送给用户
        RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate);
        //获取订阅类消息
        //拼接获取队列名称
        String queueName = "article_subscribe_" + userId;
        //获取Rabbit的Properties容器
        Properties queueProperties = rabbitAdmin.getQueueProperties(queueName);

        //获取消息数量
        int noticeCount = 0;
        //判断Properties是否不为空
        if (queueProperties != null) {
            // 如果不为空,获取消息的数量
            noticeCount = (int) queueProperties.get("QUEUE_MESSAGE_COUNT");
        }
        //获取点赞类消息
        String userQueueName = "article_thumbup_"+userId;
        Properties userQueueProperties = rabbitAdmin.getQueueProperties(userQueueName);
        int userNoticeCount = 0;
        if(userQueueProperties != null){
            userNoticeCount = (int) userQueueProperties.get("QUEUE_MESSAGE_COUNT");
        }

        //封装返回的数据
        HashMap countMap = new HashMap();
        //订阅类消息数量
        countMap.put("sysNoticeCount", noticeCount);
        //点赞类消息数量
        countMap.put("userNoticeCount",userNoticeCount);
        Result result = new Result(true, StatusCode.OK, "查询成功", countMap);

        //把数据发送给用户
        channel.writeAndFlush(new TextWebSocketFrame(MAPPER.writeValueAsString(result)));

        //把消息从队列里面清空,否则MQ消息监听器会再次消费一次
        if (noticeCount > 0) {
            rabbitAdmin.purgeQueue(queueName, true);
        }
        if(userNoticeCount > 0){
            rabbitAdmin.purgeQueue(userQueueName,true);
        }

        //为用户的消息通知队列注册监听器,便于用户在线的时候,
        //一旦有消息,可以主动推送给用户,不需要用户请求服务器获取数据
        sysNoticeContainer.addQueueNames(queueName); //运行时添加监听队列
        userNoticeContainer.addQueueNames(userQueueName);
    }
}
View Code

3. 测试点对点消息

 1) 启动tensquare-eureka,tensquare-user,tensquare-article,tensquare-notice四个微服务进行测试

   当作者的文章被点赞时,会收到点赞的新消息提示,最终页面显示的效果:

   375676ee31e5a0125fb4e1e3fa1d399a_1563794270996

 

posted on 2025-12-04 22:39  花溪月影  阅读(2)  评论(0)    收藏  举报