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

  • 一、 Netty服务端推送消息到客户端
  • 需求:
  • 应用场景
  • 二、服务端代码
  • PushServer
  • PushServerHandler
  • PushAckHandler
  • 三、服务端代码
  • PushClient
  • PushClientHandler
  • 四、测试
  • 客户端发送消息
  • 服务端接受消息


一、 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");
            }
        }
    }
}

四、测试

客户端发送消息

Android 基于Netty的消息推送方案之对象的传递 netty发送消息指定客户端_netty

服务端接受消息

Android 基于Netty的消息推送方案之对象的传递 netty发送消息指定客户端_netty_02