直接进入正题,如果需要在新老项目中集成消息推送功能,阅读完本篇文章即可完美实现,适用于单体架构和微服务架构。
本项目是微服务架构,但是与单体架构时的消息推送后端代码差别不大
先是前端代码展示(这里创建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线程,如下图
至此结束。
如果还有什么问题或者文章存在错误,欢迎在评论区提问或指出,我将会尽快回复!!