概念
Netty是一个提供异步事件驱动的网络应用程序框架和工具,用于快速开发维护的高性能和高伸缩的客户端和服务端,Netty是一个NIO客户端服务器框架。
Netty相关类与应用场景
ByteBuf
ByteBuf
是一种零个或多个字节(8bit)组成的随机且顺序访问序列。这个接口为一个或多个基元字节数组(byte[])和NIO缓冲区提供了一个抽象视图。
ByteBuf
是使用Netty开发常用的基本数据结构。Netty也存在堆内存与直接内存的区别:
- 堆内存:用于对象的创建和存储,以及进行编码、解码等操作。特点是内存分配和回收速度较快,如果用于Socket的IO处理则需要做一次额外的内存复制。
- 直接内存:主要用于传输数据,直接内存可以直接与Socket进行交互而不需要额外的内存复制,其内存分配和回收的速度则较慢于堆内存。
ByteBuf
创建实例有以下几种方式:
- 通过
ByteBufAllocator
接口 - 通过
AbstractByteBufAllocator
类 - 通过
PooledByteBufAllocator
类 - 通过
UnpooledByteBufAllocator
类 - 通过
Unpooled
工具类 - 通过Netty的
Channel#alloc
方法
上述类与接口关系如下:
图中的类都实现了ByteBufAllocator
接口,接口有个静态常量DEFAULT
是引用了 ByteBufUtil.DEFAULT_ALLOCATOR
,在ByteBufUtil
类中实现如下图:
根据平台环境来选择Allocator的默认类型,当系统不存在io.netty.allocator.type
该属性的时候,安卓平台默认为unpooled
,其他则为pooled
,也就是选择UnpooledByteBufAllocator
、UnpooledByteBufAllocator
提供的默认实现的其中一种。Netty提供的Unpooled
工具类内部则也是使用了UnpooledByteBufAllocator
类的实现。
这里根据类的名称,可以看出Netty对于内存的分配进行了池化 与非池化的处理,两者的区别如下:
- 池化的ByteBuf可以减少内存分配和释放的开销,提高效率,但会造成更多的内存碎片。
- 非池化的ByteBuf则相反,它没有内存池的限制,但需要更多的管理开销。
内存碎片又衍生出了一个CompositeByteBuf
类,一种组合式ByteBuf实现。将多个缓冲区显示为单个合并缓冲区的虚拟缓冲区,避免多次分配和释放小块内存的开销,提高处理效率。
创建ByteBuf实例的代码如下:
java
/**
* 创建 ByteBuf 几种方式
*/
private static void generateByteBuf() {
// 通过 Unpooled
// Unpooled#buffer() 默认返回 heapBuffer
ByteBuf heapBuffer = Unpooled.buffer();
ByteBuf directBuffer = Unpooled.directBuffer();
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(new byte[128], new byte[256]);
ByteBuf copiedBuffer = Unpooled.copiedBuffer(ByteBuffer.allocate(128));
ByteBuf copiedBuffer1 = Unpooled.copiedBuffer(new byte[256],new byte[256]);
// 通过实现ByteBufAllocator相关类
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
ByteBuf abstractBuf = AbstractByteBufAllocator.DEFAULT.buffer();
ByteBuf poolBuf = PooledByteBufAllocator.DEFAULT.buffer();
ByteBuf unPooledBuf = UnpooledByteBufAllocator.DEFAULT.buffer();
System.out.println("heapBuffer:"+heapBuffer);
System.out.println("directBuffer:"+directBuffer);
System.out.println("wrappedBuffer:"+wrappedBuffer);
System.out.println(wrappedBuffer.maxCapacity());
System.out.println("copiedBuffer:"+copiedBuffer);
System.out.println("copiedBuffer maxCapacity:"+copiedBuffer.maxCapacity());
System.out.println("copiedBuffer1:"+copiedBuffer1);
System.out.println("copiedBuffer1 maxCapacity:"+copiedBuffer1.maxCapacity());
System.out.println("buffer:"+buffer);
System.out.println("abstractBuf:"+abstractBuf);
System.out.println("poolBuf:"+poolBuf);
System.out.println("unPooledBuf:"+unPooledBuf);
}
上述是一些创建ByteBuf
实例部分API,创建ByteBuf实例,如果不设置capacity
参数则默认为256,maxCapacity
默认为Integer.MAX_VALUE
。Unpooled#copiedBuffer
,Unpooled#copyInt
则会设置数据的长度为maxCapacity
。maxCapacity
限制ByteBuf
增长的最大容量。
CompositeByteBuf(ridx: 0, widx: 384, cap: 384, components=2)可知有两个ByteBuf
组合,不过最大components为16,如果超过16个则使用16个,下图为创建ByteBuf
的默认参数变量。
释放ByteBuf引用
ByteBuf
是一个引用计数对象,必须通过release()方法显式释放,处理程序有责任释放传递给处理程序的任何引用计数对象。ByteBuf
不释放会造成内存泄漏问题。
释放方式有以下几种:。
ByteBuf.release()
:释放一个refCnt引用计数,如果refCnt=0,调用该方法异常。ReferenceCountUtil.release(Object msg)
: 在第一个方式操作前判断是否引用计数对象再调用。ReferenceCountUtil.saveRelease(Object msg)
: 在第二个方式增加了try
、catch
来捕获异常。
可以使用ByteBufUtil.isAccessible(ByteBuf buffer)
方法先判断ByteBuf
是否还可以访问,再执行release()
释放引用计数。 同样的可以使用ByteBuf.retain()
来增加引用计数。
java
private static void referenceRelease() {
ByteBuf buffer = Unpooled.buffer();
// 增加引用计数
buffer.retain();
buffer.retain(2);
ReferenceCountUtil.retain(buffer);
ReferenceCountUtil.retain(buffer,2);
// 释放引用计数
ReferenceCountUtil.release(buffer,3);
ReferenceCountUtil.safeRelease(buffer,3);
buffer.release();
ReferenceCountUtil.safeRelease(buffer);
System.out.println("safeRelease 执行通过");
boolean accessible = ByteBufUtil.isAccessible(buffer);
if (accessible) {
System.out.println("buffer 可释放");
ReferenceCountUtil.release(buffer);
}else {
System.out.println("buffer 没有可释放");
}
ReferenceCountUtil.release(buffer);
System.out.println("不会执行该代码");
}
ByteBuf部分API
ByteBuf API | 描述 |
---|---|
copy() |
复制一个全新的ByteBuf 副本 |
readableBytes() |
返回ByteBuf 当前可读的字节数 |
isReadable() |
当前ByteBuf 是否可读 |
readInt() |
读取一个整型(4字节),并修改readerIndex |
readBytes(byte[] bytes) |
将ByteBuf 中数据写到bytes数组中,并修改readerIndex |
writeBytes(byte[] bytes) |
将bytes数据写入到ByteBuf 中,并修改writerIndex |
readerIndex() |
返回当前readerIndex |
writerIndex() |
返回当前writerIndex |
markReaderIndex() |
标记当前readerIndex |
markWriterIndex() |
标记当前writerIndex |
resetReaderIndex() |
重置readerIndex到上次markReaderIndex() 标记处,默认0 |
resetWriterIndex() |
重置writerIndex到上次markWriterIndex() 标记处,默认0 |
setIndex(int readerIndex,int writerIndex) |
设置readerIndex、writerIndex |
clear() |
等于setIndex(0,0) |
retain() |
增加ByteBuf refCnt一个引用计数,refCnt>=1才可以增加,否则抛出异常 |
retain(int increment) |
增加ByteBuf refCnt指定数量的引用计数,refCnt>=1才可以增加,否则抛出异常 |
release() |
释放ByteBuf refCnt一个引用计数 |
release(int decrement) |
释放ByteBuf refCnt指定数量引用计数 |
refCnt |
返回当前引用计数 |
nioBuffer() |
转换成NIO的ByteBuffer |
slice() |
返回此缓冲区的子区域的切片并设置maxCapacity() ,修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们保持单独的索引和标记 |
duplicate() |
返回一个共享此缓冲区的整个区域的缓冲区。修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们保持单独的索引和标记 |
java
/**
* ByteBuf、ByteBufUtil、Unpooled
* 一些相关API
*/
private static void byteBufApi() {
// Unpooled 申请 ByteBuf 并存入指定类型的数据int
// 类似 API long,float,boolean,short,double
ByteBuf intBuf = Unpooled.copyInt(1, 5, 5, 2134, 5454, 2345);
// Unpooled 申请 ByteBuf 并存入指定类型的数据,byte,char,可多个数组
// 这样的类型会限制 maxCapacity
ByteBuf charBuffer = Unpooled.copiedBuffer(new char[]{'a', 'b', 'c', 'd'}, StandardCharsets.UTF_8);
ByteBuf byteBuffer = Unpooled.copiedBuffer("SilverGravel".getBytes(StandardCharsets.UTF_8));
/*数据复制*/
// 复制是全新的ByteBuf
ByteBuf copyBuf = byteBuffer.copy();
/*数据读写*/
// 读取整型
// 可读字节,整型4位字节 4*6= 24
System.out.print("\n************数据读写************\n");
System.out.println("intBuf可读字节:" + intBuf.readableBytes());
System.out.print("读取intBuf数据:" + intBuf.readInt());
while (intBuf.isReadable()) {
System.out.print("," + intBuf.readInt());
}
byte[] bytes = new byte[byteBuffer.readableBytes()];
byteBuffer.readBytes(bytes);
System.out.println("\nbyteBuffer数据:" + new String(bytes, StandardCharsets.UTF_8));
// 此时readerIndex已经变更,同时writerIndex也会变更
System.out.printf("byteBuffer readerIndex: %d,writerIndex: %d\n",
byteBuffer.readerIndex(), byteBuffer.writerIndex());
byte[] copyBytes = ByteBufUtil.getBytes(copyBuf);
System.out.println("copyBuf:" + new String(copyBytes, StandardCharsets.UTF_8));
// 写数据
// 可以字节、InputStream、FileChannel
// ByteBuf、ScatteringByteChannel
ByteBuf operationBuf = Unpooled.buffer();
operationBuf.writeBytes("Silver".getBytes(StandardCharsets.UTF_8));
byte[] bytes2 = ByteBufUtil.getBytes(operationBuf);
System.out.println("\noperationBuf:" + new String(bytes2, StandardCharsets.UTF_8));
System.out.printf("operationBuf readerIndex: %d,writerIndex: %d\n",
operationBuf.readerIndex(), operationBuf.writerIndex());
operationBuf.writeBytes("Gravel".getBytes(StandardCharsets.UTF_8));
byte[] bytes3 = new byte[operationBuf.readableBytes()];
operationBuf.readBytes(bytes3);
System.out.println("operationBuf:" + new String(bytes3, StandardCharsets.UTF_8));
System.out.printf("operationBuf readerIndex: %d,writerIndex: %d\n",
operationBuf.readerIndex(), operationBuf.writerIndex());
operationBuf.resetReaderIndex();
operationBuf.writeBytes("Operation".getBytes(StandardCharsets.UTF_8));
byte[] bytes4 = new byte[operationBuf.readableBytes()];
operationBuf.readBytes(bytes4);
System.out.println("operationBuf:" + new String(bytes4, StandardCharsets.UTF_8));
System.out.printf("operationBuf readerIndex: %d,writerIndex: %d\n"
, operationBuf.readerIndex(), operationBuf.writerIndex());
operationBuf.writeBytes("ByteBuf".getBytes(StandardCharsets.UTF_8));
byte[] bytes5 = ByteBufUtil.getBytes(operationBuf);
System.out.println("operationBuf:" + new String(bytes5, StandardCharsets.UTF_8));
System.out.printf("operationBuf readerIndex: %d,writerIndex: %d\n",
operationBuf.readerIndex(), operationBuf.writerIndex());
System.out.println("由上述可知,ByteBuf#readBytes()会修改ReaderIndex和WriterIndex" +
",而ByteBufUtil#getBytes()则不会");
/*重置 ReaderIndex WriterIndex*/
System.out.print("\n************重置 ReaderIndex WriterIndex************\n");
ByteBuf resetBuf = Unpooled.buffer();
resetBuf.writeBytes("1".getBytes(StandardCharsets.UTF_8));
System.out.printf("\nresetBuf当前 readerIndex:%d,writerIndex:%d\n",
resetBuf.readerIndex(), resetBuf.writerIndex());
// 标记当前读位置
resetBuf.markReaderIndex();
System.out.println("resetBuf当前标记readerIndex:" + resetBuf.readerIndex());
resetBuf.writeBytes("2".getBytes(StandardCharsets.UTF_8));
System.out.printf("resetBuf当前 readerIndex:%d,writerIndex:%d\n",
resetBuf.readerIndex(), resetBuf.writerIndex());
// 读取数据
byte[] resetBytes = new byte[resetBuf.readableBytes()];
resetBuf.readBytes(resetBytes);
System.out.printf("resetBuf读取数据:%s, 当前 readerIndex:%d,writerIndex:%d\n",
new String(resetBytes), resetBuf.readerIndex(), resetBuf.writerIndex());
resetBuf.writeBytes("3".getBytes(StandardCharsets.UTF_8));
System.out.printf("resetBuf 当前 readerIndex:%d,writerIndex:%d\n",
resetBuf.readerIndex(), resetBuf.writerIndex());
System.out.printf("resetBuf当前readerIndex:%d,可读字节:%d\n"
, resetBuf.readerIndex(), resetBuf.readableBytes());
// 重置readerIndex到上个 markReaderIndex
resetBuf.resetReaderIndex();
System.out.printf("resetBuf重置readerIndex之后,readerIndex:%d,可读字节:%d\n"
, resetBuf.readerIndex(), resetBuf.readableBytes());
// 标记当前 WriterIndex
resetBuf.markWriterIndex();
System.out.printf("resetBuf标记之后 readerIndex:%d, writerIndex:%d\n"
, resetBuf.writerIndex(), resetBuf.writerIndex());
resetBuf.writeBytes("4".getBytes(StandardCharsets.UTF_8));
resetBytes = new byte[resetBuf.readableBytes()];
resetBuf.readBytes(resetBytes);
System.out.printf("resetBuf读取数据:%s, 当前 readerIndex:%d,writerIndex:%d\n",
new String(resetBytes), resetBuf.readerIndex(), resetBuf.writerIndex());
// 如果 WriterIndex > ReaderIndex 则抛出异常
// 这里再重置一下 readerIndex
resetBuf.resetReaderIndex();
resetBuf.resetWriterIndex();
System.out.printf("resetBuf重置读写索引之后,readerIndex:%d,writerIndex:%d\n"
, resetBuf.readerIndex(), resetBuf.writerIndex());
// 等于 ByteBuf#setIndex(0,0)
resetBuf.clear();
System.out.printf("resetBuf#clear之后,readerIndex:%d,writerIndex:%d\n"
, resetBuf.readerIndex(), resetBuf.writerIndex());
// 0 <= readerIndex <= writerIndex <= capacity
resetBuf.setIndex(1, 3);
System.out.printf("resetBuf设置读写索引之后,readerIndex:%d,writerIndex:%d\n"
, resetBuf.readerIndex(), resetBuf.writerIndex());
/* 增加或释放 ByteBuf refCnt引用计数*/
System.out.println("\n************增加或释放 ByteBuf refCnt引用计数************");
ByteBuf refBuf = resetBuf.copy();
refBuf.retain();
refBuf.retain(3);
// 1+1+3 = 5
System.out.println("\nrefBuf增加引用计数后:" + refBuf.refCnt());
refBuf.release();
refBuf.release(3);
System.out.println("refBuf释放引用计数后:" + refBuf.refCnt());
/*转成Java NIO ByteBuffer*/
System.out.println("\n************转成Java NIO ByteBuffer");
ByteBuffer nioBuffer = resetBuf.nioBuffer();
int remaining = nioBuffer.remaining();
byte[] nioBytes = new byte[remaining];
nioBuffer.get(nioBytes);
System.out.println(new String(nioBytes));
/*slice ByteBuf*/
System.out.println("\ns************lice、duplicate ByteBuf************");
ByteBuf sliceBuf = Unpooled.buffer();
sliceBuf.writeBytes("SliceBuf".getBytes(StandardCharsets.UTF_8));
ByteBuf slice1 = sliceBuf.slice();
// 进行数据读取
sliceBuf.readBytes(new byte[3]);
System.out.printf("sliceBuf writerIndex:%d,readerIndex:%d,maxCapacity:%d\n"
,sliceBuf.writerIndex(),sliceBuf.readerIndex(),sliceBuf.maxCapacity());
System.out.printf("slice1 writerIndex:%d,readerIndex:%d,maxCapacity:%d\n"
,slice1.writerIndex(),slice1.readerIndex(),slice1.maxCapacity());
ByteBuf slice2 = sliceBuf.slice(1, 3);
System.out.printf("slice2 writerIndex:%d,readerIndex:%d,maxCapacity:%d\n"
,slice2.writerIndex(),slice2.readerIndex(),slice2.maxCapacity());
ByteBuf duplicate = sliceBuf.duplicate();
System.out.printf("duplicate writerIndex:%d,readerIndex:%d,maxCapacity:%d\n"
,duplicate.writerIndex(),duplicate.readerIndex(),duplicate.maxCapacity());
}
java
byte[] bytes = new byte[byteBuffer.readableBytes()];
byteBuffer.readBytes(bytes);
java
byte[] bytes = ByteBufUtil.getBytes(byteBuffer);
使用ByteBuf.readBytes(bytes)
会修改ByteBuf
的readerIndex和writerIndex,而ByteBufUtil.getBytes(byteBuffer)
则不会修改ByteBuf
的readerIndex和writerIndex。
引导类ServerBootstrap、 Bootstrap
ServerBootstrap
是一个搭建服务器端程序的助手类,Boostrap
则是一个搭建客户端程序的助手类。
服务端客户端代码示例如下:
java
public class Server {
public static void main(String[] args) {
Server server = new Server();
server.init();
}
private void init() {
EventLoopGroup workerGroup = new NioEventLoopGroup();
EventLoopGroup bossGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new IdleStateHandler(20, 20, 20))
// 自定义处理器
.addLast(new ServerHandler());
}
});
try {
ChannelFuture future = serverBootstrap.bind(9000).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
java
public class Client {
public static void main(String[] args) {
Client client = new Client();
client.init();
}
private void init() {
EventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new IdleStateHandler(10, 10, 10))
// 自定义处理器
.addLast(new ClientHandler());
}
});
try {
ChannelFuture future = bootstrap.connect("localhost", 9000).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally {
workerGroup.shutdownGracefully();
}
}
}
对比ServerBootstrap
与Bootstrap
构建的参数可以发现它们的异同。
group
: 搭建服务端程序需要传入两个EventLoopGroup
,一个称为boss,用于接受传入的连接,另一个称为worker,一旦boos接受连接并将连接注册到worker,就处理该连接的流量。channel
:对于TCP程序,服务端使用的是NioServerSocketChannel
,客户端使用的是NioSocketChannel
。同理如果是Sctp,服务端使用NioSctpServerChannel
,客户端则是NioSctpChannel
。UDP则都是NioDatagramChannel
。option
、childOption
:option
用于NioServerSocketChannel
接受的连接配置相关参数,客户端同理,用于NioSocketChannel
。服务端还有一个childOption
方法,用于父ServerChannel
连接的Channels。对于TCP/IP Server,可以使用参数ChannelOption.SO_KEEPALIVE
用于是否保持长连接,默认是false。handler
:用于为请求提供服务的ChannelHandler。即自身请求。childHandler
:设置用于为Channel请求提供服务的ChannelHandler。即他人请求。bind
:服务端需要绑定一个端口用于接受客户端的连接。connect
:客户端需要连接服务器地址端口。sync
:Netty的IO操作是异步的,如果想操作执行完成之后在执行之后的代码,使用该方法异步转同步。
对于多个处理类,可以实现ChannelInitializer<SocketChannel>()
接口,调用SocketChannel
的pipeline
方法添加处理类,这里采用的责任链模式。
ChannelHandler接口
ChannelHandler
是一个处理I/O事件或截获I/O操作,并将其转发到其ChannelPipeline中的下一个处理程序。其本身没有提供太多方法,所以通常需要实现其子类型。
以下是与ChannelHandler
接口相关的部分类、接口关系图:
就像java中的InputStream
、OutputStream
一样,ChannelInboundHandler
只处理入站数据,ChannelOutboundHandler
只处理出站数据,也就是说如果没有在SocketChannel
的pipeline
中添加相关出站类,那么接下来在使用ChannelHandlerContext#ChannelFuture writeAndFlush(Object msg)
类似方法时候,都不能将数据输出到远端的主机。
通常情况下,通过继承ChannelInboundHandlerAdapter
类并重写相关方法来处理数据。其相关方法如下图:
其中ChannelHandlerContext
关于fire前缀的方法表示将跳转到下一个处理器对应的相同名称的方法。
ChannelInboundHandlerAdapter
、ChannelOutboundHandlerAdapter
,ChannelDuplexHandler
中msg默认的基本类型是ByteBuf
。而SimpleChannelInboundHandler
只允许显式处理特定类型的消息,如下面为一个只处理String
类型的实现:
java
public class StringHandler extends
SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String message)
throws Exception {
System.out.println(message);
}
}
管道对于数据的入站和出站是分开的,顺序如下:
ps: Netty提供了LoggingHandler
处理类用于打印相关入站出站数据,其默认日志等级为DEBUG,如果与Spring Boot集成可输出到控制台,需要设置LoggingHandler
等级为INFO以上或者设置Spring Boot的日志输出级别为DEBUG.
编码、解码、拆包、粘包
对于TCP/IP是基于流的传输,接收到的数据存储到Socket的缓存区中,该缓存区是基于字节队列而不是数据包队列,即使将两条消息作为两个独立的数据包进行发送,操作系统也只会将它们视为一堆字节,而不是数据包。那么就不能保证接收数据的正确性。
以数据包为定义,而接收数据的形式为字节队列,那么可能出现以下几种情况:
第一种为正常情况:有两个数据包AB、CD分别发送。
第二种称为粘包:本该两个数据包分别发送,现在则是合成一个数据包一起发送。
第三、四、五种称为拆包:即数据AB、CD本该整体发送,而在传输过程中则AB被拆成了A、B发送,CD被拆成了C、D发送。
为了得到第一种情况,同时避免其他的异常产生,通信的双方需要协商数据按照的指定格式进行处理。 发送方将原始数据处理之后再发送给接收方,这个过程称为编码 。接收方接收发送的数据并按照规定的格式将数据还原成原始数据的过程,称为解码 。 所以一般情况下,不直接继承ChannelOutboundHandlerAdapter
类用于输出数据, 而是在ChannelInitializer
实现中按照合理的顺序添加合适的编解码处理器,再使用自定义的处理器来处理经过解码后的数据。
实现一个编码,解码处理器,有以下部分类,它们的关系如下:
MessageToMessageEncoder<I>
:将一个消息编码成另一个消息MessageToMessageDecoder<I>
:将一个消息解码成另一个消息ByteToMessageDecoder
:类似流的方式将字节从一个ByteBuf解码到另一个Message类型。MessageToByteEncoder<I>
:以类似流的方式将消息从一条消息编码到ByteBuf
根据业务需要,继承上述相关类并重写decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
或encode(ChannelHandlerContext ctx, I msg, ByteBuf out)
方法。
Netty提供了一些协议编解码的相关实现,下面为其中的一部分:
HTTP协议:
HttpRequestEncoder
、HttpRequestDecoder
HttpResponseEncoder
、HttpResponseDecoder
HttpServerCodec
相当于(HttpRequestDecoder+HttpResponseEncoder)、HttpClientCodec
相当于(HttpResponseDecoder+HttpRequestEncoder)HttpObjectAggregator
:将HttpMessage及其后续的HttpContents聚合为一个单独的FullHttpRequest或FullHttpResponse。
Websocket协议:WebSocketServerProtocolHandler
、WebSocketClientProtocolHandler
其他编码器:
DelimiterBasedFrameDecoder
:通过一个或多个分隔符来分割接收到的字节。FixedLengthFrameDecoder
:通过固定的长度来分割字节。LengthFieldBasedFrame
:通过消息中长度字段的值动态拆分接收到的ByteBuf。对应的Netty提供了LengthFieldPrepender
提供了偏移位0的长度字段的支持,长度允许1,2,3,4,8。LineBasedFrameDecoder
:用于将接收到的ByteBuf在行结尾处进行拆分,可同时处理"\n"和"\r\n"。
具体使用可以参考Netty 源码中的注释。
超时空闲处理
在一些情况下,通信的双方的其中一方断开连接,而另一方没有及时的探测到,从而导致某些业务出现问题,如连接断开重连。为了判断连接是否有效,一个做法就是监测连接对应的管道是否有空闲,也就是Channel是否有数据流量。
检测管道是否空闲,可以使用Netty实现的IdleStateHandler
类
IdleStateHandler
提供了三个构造方法用于创建判断管道空闲策略,如果readerIdleTime设置为0,则表示禁用,其他两个参数同理。
同时,后续需要一个ChannelHandlerAdapter
类重写userEventTriggered(ChannelHandlerContext ctx, Object evt)
方法,如下:
java
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 处理空闲事件 JDK17 写法
if (evt instanceof IdleStateEvent event) {
if (event.state() == IdleState.READER_IDLE) {
ctx.close();
}
}
}
IdleStateHandler
类有两个子类:ReadTimeoutHandler
、WriteTimeoutHandler
,两者分别只启用了读空闲监测、写空闲监测。如果使用子类的实例,而不是IdleStateHandler
实例,可以不用重写userEventTriggered
方法。子类有相关实现如ReadTimeoutHandler
中的方法:
使用了上述的相关类,万一程序有段时间没有数据流量就会导致连接关闭,没有流量也是属于一种正常现象,所以需要定义协议用于防止出现上述情况,也就是心跳检测。比如客户端发送一个'ping'字符串,服务端接收到之后就给客户端发送一个'pong'字符串,客户端接收之后继续发送'ping',如此反复。
完整代码示例
java-skill-learn/netty-study at main · DawnSilverGravel/java-skill-learn (github.com)