springBoot +netty实现websocket支持URL参数
需求如下:
1:通过url(ws://ip:端口/?sid=xxxxxxx)形式连接websocket,并且发送数据(自己下面设置的端口,我下面设置端口是8888)
2:每次发送的数据有可能是1M以上。同时每1~2S发送一次数据
3:防止数据泄露等问题。
实现以上需求代码:
- 导入架包,注意:netty4.1.6版本底层会报内存泄露。需要我们自己去清空ByteBuf。但是官网说4.1.21版本优化了ByteBuf回收机制。我这使用的netty4.1.25。
maven导入:会自动下载一些依赖的
<dependency>
<groupId>org.yeauty</groupId>
<artifactId>netty-websocket-spring-boot-starter</artifactId>
<version>0.8.0</version>
</dependency>
- 搭建通道。使用http格式搭建通道,适用于高并发情况。
public class nettyConfig {
private final int port=8888; //端口
private void startServer() {
// 服务端需要2个线程组 boss处理客户端连接 work进行客服端连接之后的处理
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup work = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(boss, work).channel(NioServerSocketChannel.class)
.localAddress(port)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) //启用池化ByteBuff
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec()); // HTTP 协议解析,用于握手阶段
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(1024 * 1024 * 100));
pipeline.addLast(new NettyHandler()); //自己业务类
pipeline.addLast(new WebSocketServerProtocolHandler("/", null, true, 65535*100*100)); // WebSocket// 握手、控制帧处理
}
});
// ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); //用于打印内存是否泄露,如果泄露就会打印具体信息。PARANOID:代表每次发送数据都会检查是否内存泄露(测试时候推荐使用)
ChannelFuture f = b.bind(port).sync();
System.out.println(" 启动正在监听: " + f.channel().localAddress());
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
boss.shutdownGracefully();
work.shutdownGracefully();
}
}
/**
*在程序启动后开启一个线程去启用该方法即可
**/
@PostConstruct
public void init() {
// 需要开启一个新的线程来执行netty server 服务器
new Thread(new Runnable() {
public void run() {
startServer();
}
}).start();
}
}
- 自己的业务实现类 记住实现这个SimpleChannelInboundHandler类。
@Sharable
public class NettyHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
/**
* 连接websocekt
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().id());
System.out.println("连接的客户端地址:" + ctx.channel().remoteAddress());
super.channelActive(ctx);
}
/**
* 解析数据。第一次url参数也是走这个方法
* 注意:一定要回调父类该方法。不然内存会泄露。不要以为重写该方法后,在最后面添加清除ByteBuf告诉你这是无用的,如果谁成功了请呼唤我。
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
log.info("调用 channelRead request.uri() [ {} ]", request.uri());
String uri = request.uri();
if (null != uri && uri.contains("/") && uri.contains("?")) {
String[] uriArray = uri.split("\\?");
if (null != uriArray && uriArray.length > 1) {
String[] paramsArray = uriArray[1].split("=");
if (null != paramsArray && paramsArray.length > 1) {
//截取出sid后面的值
verificationConn(ctx, paramsArray[1]); //我自己方法 验证sid值是否符合我这边要求。符合后里面开启心跳等。
}
}
request.setUri("/");
} else {
log.info("传参格式有问题 ");
ctx.close();
}
super.channelRead(ctx, msg); //重新调用父类方法,父类该方法会让ByteBuf 计数清零。从而达到数据被GC回收。如果没有调用该方法内存必会泄露。同时注意:该父类会判断出如果是TextWebSocketFrame 数据类型会调用你重写的channelRead0;具体可以参考源码
}
/**
*
**/
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// TODO 处理每次发送过来的数据。
}
}
以上就是完成这个需求重要的步骤。缺一不可的。剩余的一些业务自己处理即可。