【Netty】四、Netty服务端推送消息到客户端实现消息个性化推送

一、 Netty服务端推送消息到客户端

需求:

1、服务端从redis或数据库等存储层获取到要推送的消息;
2、服务端把获取到的消息主动推送给客户端(只要查询到了数据就推送给客户端);
3、如果有多个客户端连接到了服务端,要区分客户端,不同客户端发送不同的消息;
4、客户端接收到消息后给服务端一个应答;
5、服务端接收到应答之后,对消息状态进行修改,表示该消息已经处理;

应用场景

1、个性化推送,可以千人千面,女性用户可以推送化妆品优惠活动消息,男性用户可以推送电子科技类产品消息;
2、每个客户端(比如手机App),他们首先会登录,登录后有用户的id等唯一业务参数值,用户登录成功后,可以建立一个与netty服务端的长连接并向服务端写出一条信息,信息里面带上用户id等唯一业务参数值;
3、服务端收到客户端的用户id后,可以用一个全局的Map存放起来:
Map<String, Map<Integer, Channel>
Key: channelId, value: (key: userId, value: Channel)
4、然后服务端启动netty的定时任务,每隔多久从redis或数据库等存储层查询出业务数据,并把数据推送给对应的客户端;
5、如果连接channel断开了,把全局Map中的channel移除;

二、服务端代码

PushServer

import com.mytest.push.handler.PushAckHandler;
import com.mytest.push.handler.PushServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
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.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;


/**
 * 服务端向客户端推送消息
 * 
 * 需求:
 * 1、服务端从redis或数据库等存储层获取到要推送的消息;
 * 2、服务端把获取到的消息主动推送给客户端(只要查询到了数据就推送给客户端);
 * 3、如果有多个客户端连接到了服务端,要区分客户端,不同客户端发送不同的消息;
 * 4、客户端接收到消息后给服务端一个应答;
 * 5、服务端接收到应答之后,对消息状态进行修改,表示该消息已经处理;
 */
public class PushServer {

    private static final int PORT = 6868;

    public static void main(String[] args) {
        PushServer pushServer = new PushServer();
        pushServer.start();
    }

    private void start() {
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        serverBootstrap.group(boosGroup, workGroup);
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
            protected void initChannel(NioSocketChannel ch) {
                //ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
                ch.pipeline().addLast(new ObjectEncoder());
                ch.pipeline().addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));

                ch.pipeline().addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
                ch.pipeline().addLast(PushServerHandler.INSTANCE);
                ch.pipeline().addLast(PushAckHandler.INSTANCE);
            }
        });

        ChannelFuture channelFuture = serverBootstrap.bind(PORT).addListener((ChannelFutureListener) future -> {
            if (future.isSuccess()) {
                System.out.println("Netty server start success!");
            } else {
                System.out.println("Netty server start fail!");
            }
        });
        try {
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

PushServerHandler

import com.mytest.codec.LoginMessage;
import com.mytest.codec.PushAckMessage;
import com.mytest.codec.PushMessage;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@ChannelHandler.Sharable
public class PushServerHandler extends ChannelInboundHandlerAdapter {

    public static final PushServerHandler INSTANCE = new PushServerHandler();

    //key = channelID, value =(key:uid, value:channel)
    private static final Map<String, Map<String, Channel>> channelMap = new ConcurrentHashMap<String, Map<String, Channel>>();

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel channel = ctx.channel();
        String channelId = channel.id().asLongText();
        Map<String, Channel> uidMap = new ConcurrentHashMap<String, Channel>();

        //如果是 LoginMessage
        if (msg instanceof LoginMessage) {
            LoginMessage loginMessage = (LoginMessage) msg;
            //当前客户端登录的 用户id --> 当前的channel
            uidMap.put(loginMessage.getUid(), channel);
            channelMap.put(channelId, uidMap);

            // 每一条新连接,都是5秒之后发消息
            ctx.executor().scheduleAtFixedRate(() -> {
                String uid = loginMessage.getUid();

                //这样就是每个客户端发送每个客户端的消息
                Channel ch = channelMap.get(channelId).get(uid);

                if (ch != null) {
                    //TODO 可以根据uid查询业务数据,然后把业务数据封装成消息推送给客户端 (业务查询省略)
                    PushMessage pushMessage = new PushMessage();
                    pushMessage.setMessageId(UUID.randomUUID().toString());
                    pushMessage.setContent("尊敬的"+channelId+"童鞋,鉴于你2020年第二季度的卓越表现,公司奖励你1万元,于下月发放,期望再接再厉");
                    pushMessage.setTimestamp(System.currentTimeMillis());
                    pushMessage.setExt(String.valueOf(uid));

                    ch.writeAndFlush(pushMessage);
                }
                System.out.println(new Date() + " 服务端推送消息ok,currentUid=" + uid + ", channelMap" + channelMap);
            }, 5, 5, TimeUnit.SECONDS);
        }

        //如果是 LoginMessage
        if (msg instanceof PushAckMessage) {
            PushAckMessage pushAck = (PushAckMessage) msg;
            System.out.println("服务端接收到客户端的确认消息:" + pushAck.getMessageId());

        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        //空闲状态的事件
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state().equals(IdleState.READER_IDLE)) {
                String channelId = ctx.channel().id().asLongText();
                channelMap.remove(channelId);

                // 心跳包丢失,10秒没有收到客户端心跳 (断开连接)
                ctx.channel().close().sync();
                System.out.println("已与 "+ctx.channel().remoteAddress()+" 断开连接");
            }
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        String channelId = ctx.channel().id().asLongText();
        channelMap.remove(channelId);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.err.println(cause.getMessage());
    }
}

PushAckHandler

import com.mytest.codec.PushAckMessage;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

@ChannelHandler.Sharable
public class PushAckHandler extends SimpleChannelInboundHandler<PushAckMessage> {

    public static final PushAckHandler INSTANCE = new PushAckHandler();

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, PushAckMessage pushAck) {
        System.out.println("服务端接收到客户端的确认消息:" + pushAck.getMessageId());

        //TODO 更新消息状态 (业务处理,暂时省略)
    }
}

三、服务端代码

PushClient

import com.mytest.codec.LoginMessage;
import com.mytest.push.handler.PushClientHandler;
import io.netty.bootstrap.Bootstrap;
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.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.internal.StringUtil;

import java.util.Scanner;
import java.util.concurrent.TimeUnit;


/**
 * 客户端接收服务端推送的消息
 * 需求:
 * 1、服务端从redis或数据库等存储层获取到要推送的消息;
 * 2、服务端把获取到的消息主动推送给客户端(只要查询到了数据就推送给客户端);
 * 3、如果有多个客户端连接到了服务端,要区分客户端,不同客户端发送不同的消息;
 * 4、客户端接收到消息后给服务端一个应答;
 * 5、服务端接收到应答之后,对消息状态进行修改,表示该消息已经处理;
 */
public class PushClient {

    private static final String HOST = "127.0.0.1";

    private static final int PORT = 6868;

    public static void main(String[] args) throws Exception {
        //假设用户已经登录,登录消息
        LoginMessage loginMessage = new LoginMessage();
        loginMessage.setUid("1000");

        PushClient pushClient = new PushClient();
        pushClient.start(loginMessage);
    }

    private void start(LoginMessage loginMessage) throws Exception {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();
        bootstrap.group(group);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) {
                //ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
                ch.pipeline().addLast(new ObjectEncoder());
                ch.pipeline().addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
                ch.pipeline().addLast(new IdleStateHandler(0, 10, 0, TimeUnit.SECONDS));
                ch.pipeline().addLast(PushClientHandler.INSTANCE);
            }
        });

        //连netty服务端
        ChannelFuture channelFuture = bootstrap.connect(HOST, PORT).sync();
        if (channelFuture.isSuccess()) {
            System.out.println("Connect netty server 成功, 请输入用户ID:");
            //写出登录信息
            Scanner scanner = new Scanner(System.in);
            for (;;) {
                String uid = scanner.nextLine();
                if (!StringUtil.isNullOrEmpty(uid)) {
                    loginMessage.setUid(uid); //业务参数
                    //向服务端写出loginMessage(或者理解成向服务端注册当前app客户端)
                    channelFuture.channel().writeAndFlush(loginMessage);;
                    break;
                }
            }
        }
        channelFuture.channel().closeFuture().sync();
    }
}

PushClientHandler

import com.mytest.codec.PushAckMessage;
import com.mytest.codec.PushMessage;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;

import java.util.Date;

@ChannelHandler.Sharable
public class PushClientHandler extends SimpleChannelInboundHandler<PushMessage> {

    public static final PushClientHandler INSTANCE = new PushClientHandler();

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().attr(AttributeKey.newInstance("uid")).set(100);

        //TODO 重连
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, PushMessage msg) {
        PushMessage message = (PushMessage) msg;
        System.out.println(new Date() + " 接收到的消息:" + message);

        //发送消息确认
        PushAckMessage pushAck = new PushAckMessage();
        pushAck.setMessageId(msg.getMessageId());
        pushAck.setTimestamp(System.currentTimeMillis());

        ctx.channel().writeAndFlush(pushAck);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            if (((IdleStateEvent) evt).state() == IdleState.WRITER_IDLE) {
                ctx.writeAndFlush("ping");
            }
        }
    }
}

四、测试

客户端发送消息

在这里插入图片描述

服务端接受消息

在这里插入图片描述

  • 1
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
要在 Netty实现 WebSocket 服务端主动向客户端推送消息,可以使用 `ChannelGroup` 来管理连接到服务器的 WebSocket 客户端的 `Channel`,然后通过遍历 `ChannelGroup` 并将消息写入每个 `Channel` 来实现消息推送。 下面是一个示例代码,演示了如何在 Netty实现 WebSocket 服务端主动向客户端推送消息: ```java public class WebSocketServerHandler extends SimpleChannelInboundHandler<WebSocketFrame> { private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { // 处理 WebSocket 请求 if (frame instanceof TextWebSocketFrame) { // 处理文本消息 String text = ((TextWebSocketFrame) frame).text(); System.out.println("Received message: " + text); // 推送消息给所有连接的客户端 channelGroup.writeAndFlush(new TextWebSocketFrame("Server: " + text)); } else { // 其他类型的消息,如二进制消息、Ping/Pong 消息等 // ... } } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // 当有客户端连接时,将其添加到 ChannelGroup 中 Channel channel = ctx.channel(); channelGroup.add(channel); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // 当有客户端断开连接时,将其从 ChannelGroup 中移除 Channel channel = ctx.channel(); channelGroup.remove(channel); } // 主动向客户端推送消息的方法 public void pushMessageToClients(String message) { channelGroup.writeAndFlush(new TextWebSocketFrame("Server: " + message)); } } ``` 在上述示例中,我们创建了一个静态的 `ChannelGroup` 对象 `channelGroup`,用于存储连接到服务器的 WebSocket 客户端的 `Channel`。当有客户端连接时,将其添加到 `channelGroup` 中;当客户端断开连接时,将其从 `channelGroup` 中移除。 在处理 WebSocket 请求时,如果收到文本消息,我们可以通过调用 `channelGroup.writeAndFlush()` 方法将消息写入每个客户端的 `Channel` 中,实现消息推送。 此外,我们还添加了一个名为 `pushMessageToClients()` 的方法,用于在服务端主动向所有客户端推送消息。 你可以在适当的时候调用 `pushMessageToClients()` 方法来推送消息给所有连接的客户端。例如,可以在定时任务或其他事件触发的地方调用该方法来主动向客户端推送消息。 希望对你有所帮助!如果还有其他问题,请继续提问。
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值