Netty实现Websocket
前言
在日常开发中经常会有消息推送的需求,例如报警消息,状态消息等,在web端通常会有几种方式实现。
- 短轮询,即在前端定时向后端发起请求查询是否有新的数据。这种方式很简单但是需要不断向服务器请求,并且大多数请求根本没数据浪费资源也对服务器产生了压力
- 长轮询,也是前端定时发出请求,所不同的是长轮询查询到没数据时不会马上断开连接,而是会等待一段时间,如果等待时有数据了就可以返回。
- websocket协议基于TCP,全双工通信的协议可以支持双向数据的即时发送。
实现简单的websocket端点
1. 创建服务
public class WsServer {
private static Channel channel;
private static EventLoopGroup boss = new NioEventLoopGroup(1);
private static EventLoopGroup work = new NioEventLoopGroup();
public static void startServer() {
ServerBootstrap server = new ServerBootstrap();
server.group(boss, work);
server.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65535));
pipeline.addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatHandler());
pipeline.addLast(new WebSocketServerProtocolHandler("/netty/websocket", null,true));
pipeline.addLast(chatMessageHandler);
}
});
ChannelFuture channelFuture = server.bind(8088);
try {
System.out.println("websocket server started");
channel = channelFuture.sync().channel();
} catch (InterruptedException e) {
System.out.println("websocket server start failed");
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
startServer();
}
public static void destroy(){
System.out.println("websocket server closed");
if (channel!=null){
channel.close();
}
boss.shutdownGracefully();
work.shutdownGracefully();
}
- 我们添加了一个LoggingHandler 可以打印出Netty的详细日志,方便我们调试
- 我们同样需要添加Http协议的编码解码handler,因为websocket也是先通过http协议再升级提升的。
- IdleStateHandler 是一个空闲状态的检测处理类,当连接长时间未读写就会触发事件,因此可以用于触发心跳处理
- 我们还添加了一个心跳处理handler,在连接空闲时自动发送心跳数据保持连接的活跃状态
- 添加WebSocketServerProtocolHandler,它实现了websocket协议的编码解码,让我们只需要专注于业务数据
- 最后添加我们的业务处理handler。
2. 心跳handler
package com.coman404.ws;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import java.util.Objects;
public class HeartbeatHandler extends ChannelDuplexHandler {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
if (evt instanceof IdleStateEvent){
IdleStateEvent e = (IdleStateEvent) evt;
if (Objects.equals(e.state(), IdleState.READER_IDLE)){
ctx.writeAndFlush(new TextWebSocketFrame("heart"));
}
}
}
}
- userEventTriggered 在handler处理过程中产生事件就会触发该方法,我们可以根据事件类型的不同,做出不同的处理。示例中是读空闲,即长时间未收到数据,我们便向客户端发送一个 heart的内容数据包。
3. 业务处理handler
package com.coman404.ws;
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;
public class WsMessageHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String channelId = ctx.channel().id().toString();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("有连接建立");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("有连接断开");
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete){
WebSocketServerProtocolHandler.HandshakeComplete complete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
String subProtocol = complete.selectedSubprotocol();
String uri = complete.requestUri();
}
}
}
- 当握手成功后我们会收到HandshakeComplete 事件,这里我们可以获取到连接的端点uri和子协议信息,这里我们可以做一些鉴权操作。
问题
上面示例中我们实现了一个简单的websocket端点,但是我有一个需求,端点的uri不是固定的,可以像我们在spring中开发websocket一样,能够使用占位符或通配符的形式如:/netty/websocket/* 。
WebSocketServerProtocolHandler 并不能传递这样的通配符,只会精确匹配,不然会自动断开连接的
实现通配符格式的websocket端点
解决思路其实就是我们添加WebSocketServerProtocolHandler 前我们解析连接的地址,然后去匹配是否有对应的handler,如果有则使用请求地址来添加WebSocketServerProtocolHandler ,这样就可以匹配上了
1. 通配符处理handler
static class WebSocketPathValidatorHandler extends ChannelInboundHandlerAdapter {
private final String wildcardPath; // 例如 "/ws/*"
public WebSocketPathValidatorHandler(String wildcardPath) {
this.wildcardPath = wildcardPath;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
// 检查请求的 URI 是否符合通配符路径
String uri = request.uri();
boolean isMatch = uri.startsWith(wildcardPath.substring(0, wildcardPath.indexOf('*')-1));
String token = request.headers().get("Sec-Websocket-Protocol");
if (isMatch) {
// 路径匹配,继续 WebSocket 握手
// 添加 WebSocketServerProtocolHandler 到 pipeline
ctx.pipeline().addLast(new WebSocketServerProtocolHandler(uri, token, true));
ctx.pipeline().addLast(new WsMessageHandler());
} else {
ctx.channel().close();
}
super.channelRead(ctx,msg);
} else {
super.channelRead(ctx, msg);
}
}
}
- 在创建handler时我们传入一个端点地址,可以是通配符形式(当然也可以是占位符格式,解决方案都是类似的)
- 当我们接收到请求后,可以从http消息中获取uri,也可以获取子协议信息(Sec-Websocket-Protocol)
- 判断uri和子协议是否有对应匹配的,有则向pipeline中添加websocket协议的处理类和后续的业务处理,否则关闭连接
2.修改服务
在构建服务时我们就不再直接把websocket协议的handler添加到pipeline中而是使用我们自定义的WebSocketPathValidatorHandler
修改代码
server.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65535));
pipeline.addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatHandler());
pipeline.addLast(new WebSocketPathValidatorHandler("/netty/websocket/*"));
// pipeline.addLast(new WebSocketServerProtocolHandler("/netty/websocket", null,true));
// pipeline.addLast(chatMessageHandler);
}
});
结语
至此,服务已经可以提供正常的websocket服务了。上面提到的通配符形式的处理思路,也可以用于以后其它需要动态解决的问题上。在handler中我们仍然可以继续操作pipeline去实现更加复杂的数据流转。
浙公网安备 33010602011771号