构建一个分布式的消息通知系统(Netty + SpringBoot + Dubbo )

前话

之前在接商业外包的时候,遇到了一个需要服务端主动推送消息的需求,当时由于对 Netty 的不熟悉以及没有很好的理解 Netty 的 websocket,因此用了SSE去解决了这个问题,但是缺点就是会占用大量的带宽

Netty 的 websocket

Netty 的一个链接就是一个channel(通道),因此,如果要实现服务端的主动消息推送,那么就要实现对 channel 的统一管理;幸运的是,Netty其内部已经实现了对 channel 的统一管理的容器——ChannelGroup,ChannelGroup 其内部用了一个ConcurrentMap<ChannelId, Channel>来作为 channel 的承载容器,而 channelId 是 channel 的一个属性,但是,如果仅仅依靠 ChannelGroup 还不能够实现真正可用于生产环境中的消息推送系统,因为 channelId 与业务是完全没有关联的,因此,我这里又用了一个ConcurrentMap<String, ChannelId>来实现用户的业务标识与 channelId 的对应关系:当客户端需要连接消息推送中心时,需要带上自己的业务编号信息,然后,服务端接收消息后将用户业务编号解析并与 channel 一同存储到自定义的管理容器中

代码实现

Netty 的 websocket 实现端

@Slf4j
@Component
public class NoticeChannelHandlerImpl extends SimpleChannelInboundHandler<TextWebSocketFrame>
        implements NoticeChannelHandler {

    /**
     * 仅仅通知学生用户
     */
    protected static ChannelGroup studentChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    /**
     * 仅仅通知教师用户
     */
    protected static ChannelGroup teacherChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    /**
     * 全局通知所有用户
     */
    protected static ChannelGroup globalChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 客户端首次接入时,需发送用户编号信息
     * 客户端发送的信息:{ChannelGroupType}-{user_uuid}
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        Channel channel = ctx.channel();
        String[] contexts = msg.text().split("-");
        int clientContextSplitNum = 2;
        if (contexts.length != clientContextSplitNum) {
            channel.writeAndFlush(new TextWebSocketFrame(StringsValue.CN.CLIENT_SEND_ERR_MSG));
        } else {
            ChannelIdPool.add(contexts[1], channel.id());
            addToChannelGroup(contexts[0], channel);
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("新接入编号为 [{}] 的客户端", ctx.channel().id());
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        log.info("客户端编号为 [{}] 已断开链接", ctx.channel().id());
        ChannelIdPool.remove(ctx.channel().id());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        log.error("客户端编号为 [{}] 报错:[{}]", ctx.channel().id(), cause.getMessage());
        ctx.close();
    }

}

ConcurrentMap<String, ChannelId> 容器的实现

public class ChannelIdPool {

    private static ConcurrentHashMap<String, ChannelId> channelIdMap;

    static {
        channelIdMap = new ConcurrentHashMap<>();
    }

    public ChannelIdPool() {}

    public static Optional<ChannelId> get(String key) {
        return Optional.ofNullable(channelIdMap.get(key));
    }

    public static void add(String key, ChannelId channelId) {
        channelIdMap.put(key, channelId);
    }

    public static void remove(String key) {
        channelIdMap.remove(key);
    }

    public static void remove(ChannelId channelId) {
        Iterator<Map.Entry<String, ChannelId>> iterator = channelIdMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, ChannelId> entry = iterator.next();
            if (entry.getValue().compareTo(channelId) == 0) {
                iterator.remove();
            }
        }
    }

}

|

如何设置 Netty 服务端相关配置

Netty 服务端的配置

@PropertySource(value = "classpath:netty-server.properties")
@Configuration
public class NettyServerConfigure {

    @Value("${netty.server.tcp.port}")
    private int tcpPort;

    @Value("${netty.server.boss.thread.count}")
    private int bossCount;

    @Value("${netty.server.worker.thread.count}")
    private int workerCount;

    @Value("${netty.server.so.keepalive}")
    private boolean keepAlive;

    @Value("${netty.server.so.backlog}")
    private int backlog;

    @Bean(value = "ServerBootstrap")
    public ServerBootstrap bootstrap() {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup(), workerGroup())
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(nettyWebSocketChannelInitializer);
        Map<ChannelOption<?>, Object> tcpChannelOptions = tcpChannelOptions();
        Set<ChannelOption<?>> keySet = tcpChannelOptions.keySet();
        for (@SuppressWarnings("rawtypes") ChannelOption option : keySet) {
            b.option(option, tcpChannelOptions.get(option));
        }
        return b;
    }

    @Autowired
    @Qualifier("ChannelInitializer")
    private NettyWebSocketChannelInitializer nettyWebSocketChannelInitializer;

    @Bean(name = "tcpChannelOptions")
    public Map<ChannelOption<?>, Object> tcpChannelOptions() {
        Map<ChannelOption<?>, Object> options = new HashMap<>();
        options.put(ChannelOption.SO_KEEPALIVE, keepAlive);
        options.put(ChannelOption.SO_BACKLOG, backlog);
        return options;
    }

    @Bean(name = "bossGroup", destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup bossGroup() {
        return new NioEventLoopGroup(bossCount);
    }

    @Bean(name = "workerGroup", destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup workerGroup() {
        return new NioEventLoopGroup(workerCount);
    }

    @Bean(name = "tcpSocketAddress")
    public InetSocketAddress tcpPort() {
        return new InetSocketAddress(tcpPort);
    }

}

Netty 的 websocket 的初始化配置

@Component
@Qualifier("ChannelInitializer")
public class NettyWebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new HttpObjectAggregator(65536));
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new WebSocketServerProtocolHandler("/notice"));
        // 心跳机制
//        pipeline.addLast("ping", new IdleStateHandler(25, 15, 10, TimeUnit.SECONDS));
        pipeline.addLast(new NoticeChannelHandlerImpl());
    }

}

|

真正的 Tcp 服务端启动相关配置

@Component
public class TcpServerConfigure {

    @Autowired
    @Qualifier("ServerBootstrap")
    private ServerBootstrap serverBootstrap;

    @Autowired
    @Qualifier("tcpSocketAddress")
    private InetSocketAddress tcpPort;

    private Channel serverChannel;

    public void start() throws Exception {
        serverChannel =  serverBootstrap.bind(tcpPort).sync().channel().closeFuture().sync().channel();
    }

    @PreDestroy
    public void stop() throws Exception {
        serverChannel.close();
        serverChannel.parent().close();
    }

    public ServerBootstrap getServerBootstrap() {
        return serverBootstrap;
    }

    public void setServerBootstrap(ServerBootstrap serverBootstrap) {
        this.serverBootstrap = serverBootstrap;
    }

    public InetSocketAddress getTcpPort() {
        return tcpPort;
    }

    public void setTcpPort(InetSocketAddress tcpPort) {
        this.tcpPort = tcpPort;
    }

}

消息发布、消费中心

消息发布中心

对于消息发布中心,其职能就只限与接收由发布者需要发布的消息,将消息缓存在一个消息发布容器中,为什么需要缓存起来?因为可能有些用户是这些消息的接收者,但是由于没有登陆,也就没有对应的 channel 注册,因此需要将消息缓存在容器中,当消息的所有接收者都接收到消息时,将该消息从容器中移除,至此,一个消息在消息发布中心的生命到此结束。

@Component
public class NoticePublishCenterImpl extends Observable implements NoticePublishCenter {

    private static final Object lock = new Object();

    @Autowired private NoticeConsumerCenter noticeConsumerCenter;

    protected static ConcurrentHashMap<String, NoticePackage> noticeCenterPool;

    static {
        noticeCenterPool = new ConcurrentHashMap<>();
    }

    public NoticePublishCenterImpl() {}

    @PostConstruct
    public void init() {
        addObserver(noticeConsumerCenter);
    }

    @Override
    public boolean createNoticeGroup(NoticePackage noticePackage) {
        if (noticeCenterPool.containsKey(noticePackage.getNoticeLabel())) {
            noticeCenterPool.put(noticePackage.getNoticeLabel(), noticePackage);
            notifyObservers(noticePackage);
            return true;
        }
        return false;
    }

    /**
     * 该推送任务已完成,从通知消息池中销毁该通知消息
     * @param groupName
     */
    private void finishNotice(String groupName) {
        synchronized (lock) {
            noticeCenterPool.remove(groupName);
        }
    }

    /**
     * 由消息消费者中心通知回调告知通知发布中心该消息消费结果
     * @param o
     * @param arg
     */
    @Override
    public void update(Observable o, Object arg) {
        NoticePackage noticePackage = (NoticePackage) arg;
        if (noticePackage.getTotalReceivers() == 0) {
            finishNotice(noticePackage.getNoticeLabel());
        } else {
            noticeCenterPool.put(noticePackage.getNoticeLabel(), noticePackage);
        }
    }

}

在代码中看到了观察者模式所特有的addObserver以及notifyObservers,在这里,我将消息发布中心与消费中心用了观察者模式分解;当消息发布中心接收到发布事件后,通知消息消费中心准备开始执行消息发布任务,通知有,消息发布中心就不需要阻塞等待消息消费中心的作业完成结果,而是继续做自己的事情;而消息消费中心同样也实现了观察者模式,当消息消费中心完了消息发布中心所通知的任务后,将任务完成结果通知消息发布中心,剩下的关于消息消费的结果的处理就完全扔给了消息发布中心,而消息消费中心则可以继续自己的其他消息消费任务

消息消费中心

@Slf4j
@Component
public class NoticeConsumerCenterImpl extends Observable implements NoticeConsumerCenter {

    private static ConcurrentLinkedQueue<String> receivers = new ConcurrentLinkedQueue<>();

    @Autowired
    private NoticeChannelHandler noticeChannelHandler;
    @Autowired
    private NoticePublishCenter publishCenter;

    private static ThreadPoolExecutor PublishThreadPool;

    @PostConstruct
    private void init() {
        addObserver(publishCenter);
        PublishThreadPool = new ThreadPoolExecutor(4,
                12,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100),
                new NoticeThreadFactory(),
                new RejectHandler());
    }

    @Override
    public void update(Observable o, Object arg) {
        NoticePackage noticePackage = (NoticePackage) arg;
        PublishThreadPool.execute(new NoticeCallable(noticePackage));
    }

    @Override
    public void addReceiver(String receiver) {
        if (!receivers.contains(receiver)) {
            receivers.add(receiver);
        }
    }

    @Override
    public void removeReceiver(String receiver) {
        receivers.remove(receiver);
    }

    private class NoticeCallable implements Runnable {

        private NoticePackage noticePackage;

        public NoticeCallable(NoticePackage noticePackage) {
            this.noticePackage = noticePackage;
        }

        @Override
        public void run() {
            List<String> tmpReceivers = noticePackage.getReceivers();
            tmpReceivers.stream().filter(receiver -> receivers.contains(receiver)).flatMap(receiver -> {
                noticePackage.setUserId(receiver);
                noticeChannelHandler.publishMsg(noticePackage);
                noticePackage.setTotalReceivers(noticePackage.getTotalReceivers() - 1);
                return Stream.of(noticePackage);
            }).flatMap(noticePackage -> {
                if (noticePackage.getTotalReceivers() == 0) {
                    notifyObservers(noticePackage);
                }
                return Stream.empty();
            }).count();
        }
    }

    /**
     * 线程池任务拒绝策略
     */
    private class RejectHandler implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            log.error("消息推送服务任务被拒绝。 {}", executor.toString());
        }
    }

    private class NoticeThreadFactory implements ThreadFactory {

        private final String namePrefix;
        private final AtomicInteger nextId = new AtomicInteger(1);

        NoticeThreadFactory() {
            namePrefix = "消息推送-工作线程-";
        }

        @Override
        public Thread newThread(Runnable r) {
            String name = namePrefix + nextId.getAndDecrement();
            return new Thread(r, name);
        }
    }
}

项目演示视频

项目演示视频:

利用 Netty、SpringBoot 以及 Dubbo 构建一个分布式的消息通知系统
https://www.bilibili.com/video/av36497338/


作者:春少

来源链接:

https://www.liaochuntao.cn/2018/11/19/java-web-17/

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值