Java集成基于Netty的WebSocket长连接实现服务端消息推送功能

        直接进入正题,如果需要在新老项目中集成消息推送功能,阅读完本篇文章即可完美实现,适用于单体架构和微服务架构。

本项目是微服务架构,但是与单体架构时的消息推送后端代码差别不大

       先是前端代码展示(这里创建ws连接,在真实的项目中可以是在登录时或者在其他需要消息推送业务的前端逻辑处建立)将这个方法绑定事件或运行一次后即可与后端请求建立ws连接,实际开发中前端应该封装一个webSocket类来进行更复杂的操作,例如发送心跳,超时重连等,这里简化了

const MessageHandle = () => {
  const currentUSer = "8zknvfg47t"
  window.ipcRenderer.send('ping')
//encodeURIComponent(currentUSer)为编码后的路径参数,后端可接收到
  const socket = new WebSocket("ws://localhost:9000/ws/" + encodeURIComponent(currentUSer))
  // 监听 WebSocket 连接打开事件
  socket.onopen = () => {
    // 在连接打开后发送消息
    socket.send("Hello!!");
  };

  // 监听 WebSocket 收到消息事件
  socket.onmessage = (e) => {
    console.log("接收到消息:", e.data);
  };

  // 监听 WebSocket 错误事件
  socket.onerror = (error) => {
    console.error("WebSocket 错误:", error);
  };

  // 监听 WebSocket 关闭事件
  socket.onclose = (event) => {
    console.log("WebSocket 连接关闭:", event);
  }
}

  然后是独立的WebSocket模块

pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.kk</groupId>
    <artifactId>WebSocket</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>WebSocket</name>
    <description>WebSocket</description>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--netty-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.50.Final</version>
        </dependency>
    </dependencies>


</project>

以下是这三个类的代码展示

InitWebSocket.class :

简化版启动类,实际中可在实例化时传入构造参数,例如webSocket的端口号,读写空闲时间等,目前为简化,这些信息都是写死的。

package com.kk.websocket;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class InitWebSocket {
    private WebSocketEndpoint webSocketEndpoint;

    /**
     * @param
     * @throws Exception on error
     */
    public InitWebSocket() {
        try {
            this.webSocketEndpoint = new WebSocketEndpoint();
            log.info("消息推送服务初始化中");
            new Thread(webSocketEndpoint).start();
        } catch (Exception e) {
            log.info("消息推送服务初始化失败,失败原因:{}", e.getMessage());
        }
    }
}

WebSocketEndpoint.class :

固定写法,注意这里的 channelFuture.channel().closeFuture().sync();将会阻塞当前线程,在使用时将阻塞主线程直到Netty通道关闭,所以需要在一个新的线程中运行,这也是InitWebSocket类的作用。

package com.kk.websocket;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
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;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class WebSocketEndpoint implements Runnable {

    private WebSocketServerHandler webSocketServerHandler = new WebSocketServerHandler();
    /**
     * boss线程组,用于处理连接
     */
    private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    /**
     * work线程组,用于处理消息
     */
    private EventLoopGroup workerGroup = new NioEventLoopGroup();

    @Override
    public void run() {
        try {
            //创建服务端启动助手
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChannelInitializer<Channel>() {
                        @Override
                        protected void initChannel(Channel channel) {
                            ChannelPipeline pipeline = channel.pipeline();
                            //设置几个重要的处理器
                            // 对http协议的支持,使用http的编码器,解码器
                            pipeline.addLast(new HttpServerCodec());
                            //聚合解码 httpRequest/htppContent/lastHttpContent到fullHttpRequest
                            //保证接收的http请求的完整性
                            pipeline.addLast(new HttpObjectAggregator(64 * 1024));
                            //心跳 long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit
                            //readerIdleTime(读空闲时间):表示通道在指定时间内没有读操作时触发读空闲事件的时间间隔。
                            //writerIdleTime(写空闲时间):表示通道在指定时间内没有写操作时触发写空闲事件的时间间隔。
                            //allIdleTime(所有类型的空闲时间):表示通道在指定时间内既没有读操作也没有写操作时触发所有类型的空闲事件的时间间隔。
                            //unit(时间单位):时间参数的单位,可以是秒(SECONDS)、毫秒(MILLISECONDS)等。
                            pipeline.addLast(new IdleStateHandler(6000, 0, 0, TimeUnit.SECONDS));
                            //将http协议升级为ws协议,对websocket支持
                            pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true, 64 * 1024, true, true, 10000L));
                            //设置ws连接的事件监听类
                            pipeline.addLast(webSocketServerHandler);
                        }
                    });
            //启动
            ChannelFuture channelFuture = serverBootstrap.bind(9000).sync();
            log.info("Netty服务端启动成功,端口:{}", 9000);
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

WebSocketServerHandler.class 

用于设置每个channel的事件监听,设置@ChannelHandler.Sharable后,表示WebSocketServerHandler这个事件处理类可被共享,每次建立新的连接都将使用同一个处理类,如果不设置,每个使用WebSocketServerHandler处理类的ws实例只能进行一次连接,多次连接将会报错,如下:

这里的ChannelHandlerContext 对象可以拿到channel信息,通过拿到的Channel对象可以进行一系列操作,例如发送信息,获取channel上下文等

package com.kk.websocket;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ChannelHandler.Sharable//设置通道(channel)共享
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    // 用于存储所有连接的 WebSocket 客户端的线程安全通道集合
    private static final Map<String, Channel> channels = new ConcurrentHashMap<>();

    /**
     * 当新的客户端连接到服务器时调用。
     * 将新连接的通道添加到通道组中。
     *
     * @param ctx 通道处理上下文
     * @throws Exception 异常
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

    }

    /**
     * 当客户端断开连接时调用。
     * 从通道组中移除断开连接的通道。
     *
     * @param ctx 通道处理上下文
     * @throws Exception 异常
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        channels.remove(ctx.channel());
    }

    /**
     * 当服务器接收到客户端发送的消息时调用。
     * 将接收到的消息广播给所有连接的客户端。
     *
     * @param ctx 通道处理上下文
     * @param msg 客户端发送的消息
     * @throws Exception 异常
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        String receivedMsg = msg.text();
        System.out.println("Received message: " + receivedMsg);

    }

    //监听WebSocket事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        //监听WebSocket事件,WebSocketServerProtocolHandler.HandshakeComplete表示WebSocket握手成功,进行自定义操作,例如保存用户Channel
        //用于推送消息
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
            WebSocketServerProtocolHandler.HandshakeComplete complete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
            String url = complete.requestUri();
            String userId = url.split("/")[2];
            System.out.println(userId);
            channels.put(userId, ctx.channel());
            System.out.println("用户:" + userId + "连接到客户端 ");
        }
    }

    /**
     * 当处理过程中发生异常时调用。
     * 打印异常堆栈信息并关闭相关通道。
     *
     * @param ctx   通道处理上下文
     * @param cause 异常原因
     * @throws Exception 异常
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 当客户端连接到服务器时调用。
     * 打印客户端连接的远程地址。
     *
     * @param ctx 通道处理上下文
     * @throws Exception 异常
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Client connected: " + ctx.channel().remoteAddress());
    }

    /**
     * 当客户端断开连接时调用。
     * 打印客户端断开连接的远程地址。
     *
     * @param ctx 通道处理上下文
     * @throws Exception 异常
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Client disconnected: " + ctx.channel().remoteAddress());
    }

    public void sendMessage(String userId, String message) {
        if (channels.get(userId) != null) {
            channels.get(userId).writeAndFlush(new TextWebSocketFrame(message));
        } else {
            System.out.println("被通知用户不在线");
        }

    }

}

接下来是使用这个WebSocket模块,分如下步骤:

1.引入当前WebSocket模块依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.parent</groupId>
        <artifactId>Parent</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath></relativePath>
    </parent>
    <groupId>com.kk</groupId>
    <artifactId>Order</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Order</name>
    <description>Order</description>

    <dependencies>
        <dependency>
            <groupId>com.kk</groupId>
            <artifactId>WebSocket</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.kk</groupId>
            <artifactId>Base</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2.创建WebSocket配置类进行配置

        这里以@Bean方式创建bean,Spring默认是单例的,所以在InitWebSocket 类中就不需要保证单例了,也就不会创建多个netty线程

package com.kk.order.config;

import com.kk.websocket.InitWebSocket;
import com.kk.websocket.WebSocketServerHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebSocketConfig {
    @Bean
    public WebSocketServerHandler webSocketServerHandler() {
        return new WebSocketServerHandler();
    }

    @Bean
    public InitWebSocket createWebSocket() {
        return new InitWebSocket();
    }

}

3.在需要推送消息的业务中调用注入的WebSocketServerHandler 实例的发送消息或其他自定义方法(实际上应该新建一个ChannelContextUtils类,用于对Channel的各种操作,发送消息、处理消息等,此处是简化版本)

    @Resource
    private WebSocketServerHandler webSocketServerHandler;

    @RabbitListener(queues = DEAD_MESSAGE_QUEUE)
    public void contextLoads(String orderId) {
        Order order = orderMapper.selectOne(new QueryWrapper<Order>().lambda().eq(Order::getId, orderId));
        if (order != null) {
            //延迟队列延迟过指定时间后的消息(id)通过交换机路由至此处,根据订单id删除订单
            if (!order.getStatus().equals("已支付")) {
                //如需保留历史取消订单数据则使用注释中的代码,更新状态字段进行逻辑删除
//                Order result = orderMapper.selectOne(new QueryWrapper<Order>().lambda().eq(Order::getId, orderId));
//                result.setStatus("已取消");
//                orderMapper.updateById(result);
                orderMapper.deleteById(orderId);
                log.info("订单超时未支付,解锁库存");
                //订单超时未支付通知Seckill解锁库存并将订单取消消息推送至客户端
                LockDto lockDto = new LockDto();
                lockDto.setMessage("unlockAndCallbackStock");
                lockDto.setGoodsId(order.getGoodsId());
                rabbitTemplate.convertAndSend(CENTER_EXCHANGE, "stock.lock", lockDto);
                //在此处调用自定义方法,实现消息推送
                webSocketServerHandler.sendMessage(order.getUserId(),"用户:" + order.getUserId() + "您好,您的订单因超时未支付,已自动取消");
            }
        }
    }

运行项目测试:

成功连接上netty服务器

超时订单提醒成功!

如果能理解这里WebSocket模块的三个主要类,在单体项目中使用也差不了多少,无非是在初始化时直接在实现ApplicationRunner接口的run方法中创建netty线程,如下图

至此结束。

如果还有什么问题或者文章存在错误,欢迎在评论区提问或指出,我将会尽快回复!!

要在 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()` 方法来推送消息给所有连接的客户端。例如,可以在定时任务或其他事件触发的地方调用该方法来主动向客户端推送消息。 希望对你有所帮助!如果还有其他问题,请继续提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值