在WebSocket实现前后端消息交互-CSDN博客的基础上采用Netty提供更稳健的交互。
Netty 官网 Netty: Home
1 什么是Netty?
Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序,是目前最流行的 NIO 框架。
2 为什么要用Netty?
首先,Netty天然地支持websocket。
-
高性能:Netty是一个专为高并发、高性能设计的网络应用程序框架。它利用非阻塞I/O和事件驱动模型,能够高效地处理大量并发连接,这对于需要维持长连接的WebSocket应用尤为重要。这通常意味着在处理大量实时通信时,Netty可以提供更低的延迟和更高的吞吐量。
-
异步编程模型:Netty的异步特性允许在不阻塞线程的情况下处理网络事件,这意味着服务器可以同时处理更多的请求,而无需为每个连接分配独立的线程,从而减少了资源消耗并提高了效率。
-
稳定性与成熟度:Netty作为一个成熟的开源项目,经过了广泛的实战检验,拥有活跃的社区支持和持续的更新维护。它解决了许多底层网络编程中的常见问题,如内存管理、线程模型优化等,使得开发者可以更专注于业务逻辑而非基础架构问题。
-
简化开发复杂度:相比原生WebSocket实现,Netty提供了更高级别的抽象,使得开发者能够以更简洁的代码实现复杂的网络通信功能。API的设计倾向于简洁易用,降低了开发门槛。
3 落地实现
后端:(Java)
(1)引入依赖
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
(2)创建Netty服务端类NettyServer
让Spring来管理
@Configuration
@Slf4j
@Data
public class NettyServer {
}
属性:
@Value("${netty-server.port}")
private int port;// netty服务器的端口可以自己指定
private ChannelFuture channelFuture;// 用于异步管理Netty服务器的channel
//负责处理接受进来的链接
private EventLoopGroup bossGroup;
//负责处理已经被接收的连接上的I/O操作
private EventLoopGroup workerGroup;
@Resource
private ThreadPoolExecutor threadPoolExecutor;// 线程池,后面说明
方法:
@Async("threadPoolExecutor")// 实现线程池,让这个方法异步执行
public void start() throws Exception {
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
sb.group(bossGroup, workerGroup) // 绑定线程池
.channel(NioServerSocketChannel.class) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) {
log.info("收到新连接");
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast(new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new MyWebSocketHandler());
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/websocket", null, true, 65536 * 10));
// 客户端连接到服务端的地址:ip:port/websocket
}
});
channelFuture = sb.bind().sync(); // 服务器异步创建绑定
log.info(NettyServer.class + " 启动正在监听: " + channelFuture.channel().localAddress());
channelFuture.channel().closeFuture().sync(); // 关闭服务器通道
} finally {
workerGroup.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
@PreDestroy // Server实例被销毁时执行的方法,主要是关闭资源
public void stopServer(){
if (channelFuture != null && !channelFuture.isDone()) {
channelFuture.cancel(true);
}
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
(3)定义channelGroup
用来管理目前所有已连接的channel
public class ChannelHandlerPool {
public ChannelHandlerPool(){}
public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
(4)定义MyWebSocketHandler类
这个类用于处理连接开启、关闭、发送消息等操作,类似于controller。
@Slf4j
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 用于绑定channel和用户的ID
public static final Map<String, ChannelHandlerContext> webSocketMap = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("与客户端建立连接,通道开启!");
//添加到channelGroup通道组
ChannelHandlerPool.channelGroup.add(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("与客户端断开连接,通道关闭!");
//添加到channelGroup 通道组
ChannelHandlerPool.channelGroup.remove(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//首次连接是FullHttpRequest,处理参数
if (msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
Map paramMap=getUrlParams(uri);
webSocketMap.put(paramMap.get("id").toString(),ctx);
log.info("接收到的参数是:" + JSON.toJSONString(paramMap));
//如果url包含参数,需要处理
if(uri.contains("?")){
String newUri=uri.substring(0,uri.indexOf("?"));
log.info(newUri);
request.setUri(newUri);
}
}else if(msg instanceof TextWebSocketFrame){
//正常的TEXT消息类型
TextWebSocketFrame frame=(TextWebSocketFrame)msg;
log.info("客户端收到服务器数据:" +frame.text());
// sendAllMessage(frame.text());
ctx.writeAndFlush(new TextWebSocketFrame("Hello,与服务器的连接已建立!"));
}
super.channelRead(ctx, msg);
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
}
private void sendAllMessage(String message){
//收到信息后,群发给所有channel
ChannelHandlerPool.channelGroup.writeAndFlush( new TextWebSocketFrame(message));
}
public static void sendMessage(String id,String message){
ChannelHandlerContext ctx = webSocketMap.get(id);
if (ctx != null) {
log.info("发送给客户端消息:" + message);
ctx.channel().writeAndFlush(new TextWebSocketFrame(message));
}
}
private static Map getUrlParams(String url){
Map<String,String> map = new HashMap<>();
url = url.replace("?",";");
if (!url.contains(";")){
return map;
}
if (url.split(";").length > 0){
String[] arr = url.split(";")[1].split("&");
for (String s : arr){
String key = s.split("=")[0];
String value = s.split("=")[1];
map.put(key,value);
}
return map;
}else{
return map;
}
}
}
前端:
const WebSocketComponent = (userId: any) => {
useEffect(() => {
const id = userId.userId;
console.log(userId)
// 创建WebSocket连接
const url = 'ws://127.0.0.1:6848/websocket?id='+id
console.log(url)
const socket = new WebSocket(url);
if(!socket){
alert("您的浏览器不支持WebSocket协议!");
}
// 处理连接成功事件
socket.onopen = () => {
console.log('WebSocket连接已打开');
socket.send('Hello, WebSocket!'); // 发送一条消息
console.log('已发送消息');
};
// 处理接收到消息事件
socket.onmessage = (event) => {
const messageContent = event.data;
console.log('收到消息:', messageContent);
// 使用Ant Design的message组件显示消息
message.success(messageContent,5)
// 您的[]业务已处理完毕,请前往[]查看
};
// 处理连接关闭事件
socket.onclose = () => {
socket.send('Hello, WebSocket!'); // 发送一条消息
console.log('已发送消息');
console.log('WebSocket连接已关闭');
};
// 处理错误事件
socket.onerror = (error) => {
console.error('WebSocket发生错误:', error);
};
// 在组件卸载时关闭WebSocket连接
return () => {
socket.close();
};
}, [userId]);
return;
}
export default WebSocketComponent;
4 一些细节
(1)客户端连接会记录客户端ip、连接的channel信息,但这些与业务无关,我们真正需要的是知道用户是谁,即用户ID。
所以前端在发起请求时,需要携带请求参数id,后端接收时MyWebSocketHandler会把用户ID和channel用Map绑定,后面服务器推送消息可以用ID直接找到对应的channel。
(2)NettyServer启动监听客户端连接会阻塞Springboot项目的其他业务,所以应该用线程池让Netty异步启动。
在springboot的主启动类
@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
@MapperScan("com.polaris.project.mapper")
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@EnableAsync
public class MainApplication implements ApplicationRunner {
@Resource
private NettyServer nettyServer;
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
@Override
public void run (ApplicationArguments args) throws Exception{
Log.info("Netty Server started on port: {}", String.valueOf(nettyServer.getPort()));
nettyServer.start();
}
}
前面已经在start()加了@Async。
(3)关于Netty心跳保活
在server添加空闲状态处理器
// 这里设置5秒内没有从 Channel 读写数据时会触发一个 READER_IDLE 事件。
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
可以参考Netty 心跳机制详解_netty心跳机制-CSDN博客
参考:
SpringBoot2+Netty+WebSocket(netty实现websocket,支持URL参数)_netty websocket 加url参数-CSDN博客
使用springBoot初始化启动netty创建的两个服务,解决只能启动一个,另一个不能执行问题_springboot 多模块项目 一个能启动,一个不能启动-CSDN博客