Netty实现websocket

23 篇文章 1 订阅
8 篇文章 0 订阅

Netty实现websocket

项目中有一个需求,就是能够让服务端主动向客户端发送通知信息,大致过程如下
门柜设备开门成功 -》返回开门成功信息给服务端 -》服务端返回ACK确认 -》服务端同时通知小程序用户开锁成功信息…

由于需要服务端主动向客户端发送信息,这里可以使用websocket来实现(WebSocket相关文章链接),下面就来实现一个基于websocket的服务器的基本建立过程(这里使用的时Netty,其基于NIO,有许多优点,这是我另一篇关于Netty的博客链接

思路:

  • 先建立起Netty服务器的基本架构
  • 设计一个GlobalChannel来全局管理服务端与客户端之间的channel通信
  • 客户端在首次访问页面时发起对服务端请求,并建立起channel,通过userId与自己的channel绑定,服务端在通知时可以根据userId找到对应的channel识别用户

代码:

Netty服务器搭建

@Service("nettyWebsocketService")
public class NettyWebsocketServiceImpl implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(NettyWebsocketServiceImpl.class);

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Autowired
    private SystemConfig systemConfig;

    @Autowired
    private MessageQueueService messageQueueService;

    public static void main(String[] args) {
        new NettyWebsocketServiceImpl().run();
    }

    @PostConstruct
    public void initNetty(){
//        threadPoolTaskExecutor.execute(new NettyWebsocketServiceImpl());
        threadPoolTaskExecutor.execute(() -> run());
    }

    public void run(){
        if (SystemTypeEnum.Linux != SystemTypeEnum.getSystem()) {
            logger.info("只能在Linux服务器中执行监听");
            //return;
        }
        while(!ApplicationContextListener.isStrartUp) {
            try {
                logger.info("Spring还在加载中,等候一秒后再试。");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                logger.info("", e);
            }
        }
        logger.info("===========================Netty端口启动========");
        // Boss线程:由这个线程池提供的线程是boss种类的,用于创建、连接、绑定socket, (有点像门卫)然后把这些socket传给worker线程池。
        // 在服务器端每个监听的socket都有一个boss线程来处理。在客户端,只有一个boss线程来处理所有的socket。
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // Worker线程:Worker线程执行所有的异步I/O,即处理操作
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            // ServerBootstrap 启动NIO服务的辅助启动类,负责初始话netty服务器,并且开始监听端口的socket请求
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workGroup);
            // 设置非阻塞,用它来建立新accept的连接,用于构造serversocketchannel的工厂类
            b.channel(NioServerSocketChannel.class);
            // ChildChannelHandler 对出入的数据进行的业务操作,其继承ChannelInitializer
            b.childHandler(new ChildChannelHandler(messageQueueService));
            logger.info("服务端开启等待客户端连接 ... ...");
            int wssPort = 8888;
            logger.info("端口号:" + wssPort);
            Channel ch = b.bind(wssPort).sync().channel();
            ch.closeFuture().sync();
        } catch (Exception e) {
            logger.info("", e);
        }finally{
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

channel处理器:

public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
    private static final Logger logger = LoggerFactory.getLogger(GlobalChannel.class);

    MessageQueueService messageQueueService;

    public ChildChannelHandler(MessageQueueService messageQueueService) {
        this.messageQueueService = messageQueueService;
    }

    @Override
    protected void initChannel(SocketChannel e) throws Exception {
        logger.info("收到SocketChannel,现在进行channel初始化.");
        // 设置30秒没有读到数据,则触发一个READER_IDLE事件。
        e.pipeline().addLast(new IdleStateHandler(30, 0, 0));
        // HttpServerCodec:将请求和应答消息解码为HTTP消息
        e.pipeline().addLast("http-codec",new HttpServerCodec());
        // HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
        e.pipeline().addLast("aggregator",new HttpObjectAggregator(65536));
        // ChunkedWriteHandler:向客户端发送HTML5文件
        e.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
        // 在管道中添加我们自己的接收数据实现方法
        e.pipeline().addLast("handler",new MyWebSocketServerHandler(messageQueueService));
    }
}

处理数据的方法类:

public class MyWebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
    private static final Logger logger = LoggerFactory.getLogger(WebSocketServerHandshaker.class);

    private MessageQueueService messageQueueService;

    private WebSocketServerHandshaker handshaker;

    public MyWebSocketServerHandler(MessageQueueService messageQueueService) {
        this.messageQueueService = messageQueueService;
    }

    /**
     * channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 添加
        String remoteAddress = ctx.channel().remoteAddress().toString();
        logger.info("客户端与服务端连接开启:" + remoteAddress);
		//当发生活跃链接时,注册到group中!!
        GlobalChannel.group.add(ctx.channel());
    }
    /**
     * channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 移除
        Channel channel = ctx.channel();
        GlobalChannel.group.remove(channel);
        Long weixinUserId = GlobalChannel.weixinUserMap.get(channel.id());
        GlobalChannel.channelMap.remove(weixinUserId);
        GlobalChannel.weixinUserMap.remove(channel.id());
        logger.info("客户端与服务端连接关闭:" + ctx.channel().remoteAddress().toString());
    }

    /**
     * channel 通道 Read 读取 Complete 完成 在通道读取完成后会在这个方法里通知,对应可以做刷新操作 ctx.flush()
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
	private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
	
	//这里可以根据需要自定义处理方法...

	//标记建立连接的用户,通过这个标记来实现识别通知!
	if (request != null) {
            NettyMessageTypeEnum messageTypeEnum = NettyMessageTypeEnum.valueOf(request.getMessageType());
            switch (messageTypeEnum) {
                case 标记用户:
                    Long weixinUserId = request.getData().getLongValue("userId");
                    GlobalChannel.channelMap.put(weixinUserId, ctx.channel().id());
                    GlobalChannel.weixinUserMap.put(ctx.channel().id(), weixinUserId);
                    break;
                case 测试回复:
                    weixinUserId = request.getData().getLongValue("userId");
                    Boolean close = request.getData().getBoolean("close");
                    NettyResponse response = new NettyResponse();
                    response.setMessageType(NettyMessageTypeEnum.开锁成功.getType());
                    Map<String, Object> result = new HashMap<>();
                    result.put("当前时间:", new Date().toString());
                    response.setData(result);
                    response.setWeixinUserId(weixinUserId);

                    try {
                        messageQueueService.sendWebSocket(response);

                        if (close != null && close) {
                            Channel channel = GlobalChannel.getChannel(response);
                            channel.close();
                            logger.info("关闭ws:" + response.getWeixinUserId());
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    break;
            }
        }
	}

GlobalChannel设计:

public class GlobalChannel {

    private static final Logger logger = LoggerFactory.getLogger(GlobalChannel.class);
    public static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    public static Map<Long, ChannelId> channelMap = new HashMap<>();
    public static Map<ChannelId, Long> weixinUserMap = new HashMap<>();
    
    public static Channel getChannel(NettyResponse response) {
        ChannelId channelId = channelMap.get(response.getWeixinUserId());
        return group.find(channelId);
    }

    public static void sendMessage(NettyResponse response) {

        ChannelId channelId = channelMap.get(response.getWeixinUserId());
        if (channelId == null) {
            logger.info("IotMessageQueueListener 向用户[" + response.getWeixinUserId() + "]发送消息:[" + response.getMessageType() + "]但是该用户未连接");
            return;
        }
        Channel channel = group.find(channelId);
        if (channel == null) {
            logger.info("IotMessageQueueListener 向用户[" + response.getWeixinUserId() + "]发送消息:[" + response.getMessageType() + "]但是Channel不存在");
        } else {
            TextWebSocketFrame tws = new TextWebSocketFrame(response.toJSONString());
            channel.writeAndFlush(tws);//发送
            logger.info("IotMessageQueueListener 向用户[" + response.getWeixinUserId() + "]发送消息:[" + response.getMessageType() + "]SUCC");
        }

        return;
    }
}

大致过程如上所示,这里主要的设计时GlobalChannel关于用户与channel建立关系的方式,这里通过静态的HashMap进行关系映射,用户在第一次访问服务器时(可通过前端来实现websocket的建立),已经标记在服务器中,服务器再通过客户端用户的id准确地发出通知,实现了又服务端主动向客户端发送信息的效果。
补充
这里为了达到解耦,可以在服务端向客户端发送消息中间加一个MQ来实现,当需要发送消息时,由服务端发送消息到消息队列中(生产者),然后启动一个监听器,来监听队列中是否出现消息,然后进行处理发送个客户端(消费者)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值