一、Netty的核心机制
Netty的核心机制是基于异步和事件驱动的方式,来实现I/O通信。当然Netty也实现了阻塞式I/O的功能。对网络应用来说,IO一般是性能的瓶颈,使用异步IO可以较大程度上提高程序性能,异步处理提倡更有效的使用资源,它允许你创建一个任务,当有事件发生时将获得通知并等待事件完成。这样就不会阻塞,不管事件完成与否都会及时返回,资源利用率更高,程序可以利用剩余的资源做一些其他的事情。
1、回调
在使用Ajax的时候,经常需要使用回调函数。一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。
代码示例:
public interface Callback {
public void success();
public void exception();
}
public class Work {
private Callback callback;
public Work(Callback callback) {
this.callback = callback;
}
public void doWork(){
try {
System.out.println("doWork success!");
this.callback.success();
} catch (Exception e) {
System.out.println("throw exception!");
this.callback.exception();
}
}
}
public class Demo {
public static void main(String[] args) {
Work work = new Work(new Callback() {
@Override
public void success() {
System.out.println("success method callback!");
}
@Override
public void exception() {
System.out.println("exception method callback!");
}
});
work.doWork();
}
}
在Netty中当事件被触发后,就会回调相应的事件方法。而相关的事件由ChannelHandler接口的实现类处理。
2、Future
java内置的java.util.concurrent.Future,用于接收有返回值的线程的返回值。但是Future需要自己手动检查操作是否完成。而Netty提供了自己的Future实现———ChannelFuture,ChannelFuture可以通过注册ChannelFutureListener实例,在操作完成时会调用监听器的回调方法operationComplete。可以在回调方法里获取到操作是否完成成功或者出了异常。
代码示例:
Channel channel = ...;
ChannelFuture future = channel.connect(new InetSocketAddress("localhost", 8080));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess()){
//操作成功就走这里
....
} else {
//操作失败,打印异常信息
Throwable ex = future.cause();
ex.printStackTrace();
}
}
});
3、事件和ChannelHandler
Netty使用不同的事件来通知我们状态改变,而我们可以根据事件来进行相应的操作,如:日志记录、程序逻辑处理、错误处理等。Netty的事件根据入站或者出站数据流的相关性进行分类的。入站事件有:连接已被激活或者连接失活;数据读取;用户事件;错误事件等。出站事件包括:打开或者关闭到远程节点的连接;将数据写到或者冲刷到套接字。每个事件都可以通过实现ChannelHandler接口来实现自己的事件处理逻辑。而ChannelHandler添加完后在Netty中是链式的。
4、Channel和EventLoop
Channel和NIO中相同,都是通道,表示客户端与服务端之间的一个连接通道。相关的I/O操作都在这个通道中完成。EventLoop是用来处理事件的。一个EventLoop在它的生命周期内只和一个Thread 绑定,而每个Channel也只会注册于一个 EventLoop,但是EventLoop可以被分配多个Channel,所有由EventLoop处理的 I/O 事件都将在它专有的 Thread 上被处理。
二、Netty的组件
1、ByteBuf
(1)简介
在实现数据传输时,需要用到缓冲区。我们在学习NIO的时候用到过ByteBuffer。Netty实现了自己的缓冲区ByteBuf,与JDK自带的ByteBuffer相比。ByteBuf的优点如下:
1、可以自定义扩展。
2、实现了零拷贝
3、容量可以按需增长(类似于 JDK 的 StringBuilder)
4、在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法
5、分别实现了读索引和写索引
6、支持方法的链式调用
7、支持引用计数
8、支持池化
Netty的缓冲API有两个接口:ByteBuf、ByteBufHolder。
(2)原理
ByteBuf分别实现了读索引和写索引。从ByteBuf读取数据,readerIndex会增加被读取的字节数,写入数据到ByteBuf时,writerIndex增加写入的字节数。初始的ByteBuf如下所示(假设大小为16)
当readerIndex和writerIndex相等时,表示ByteBuf里的数据已经被读完了,这时如果再次读取时,会触发一个 IndexOutOfBoundsException。名称以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get开头的操作则不会。
(3)类型
ByteBuf有三种类型,分别是:堆缓冲区、直接缓冲区、复合缓冲区。
堆缓冲区(Heap Buffer):
将数据存储在 JVM 的堆空间中。Heap Buffer含义一个数组(backing array),堆缓冲区可以快速分配,当不使用时也可以快速释放。可以通过ByteBuf.array()来获得数组。但是如果ByteBuf不是堆缓冲区,访问支撑数组会导致UnsupportedOperationException,可以通过ByteBuf.hasArray()来确认ByteBuf是否支持数组。
ByteBuf heapBuf = ...;
if (heapBuf.hasArray()) {
byte[] array = heapBuf.array();
...
}
直接缓冲区(Direct Buffer):
在堆内存外直接分配内存,直接缓冲区在使用Socket传递数据时性能很好,因为若使用间接缓冲区,JVM会先将数据复制到直接缓冲区再进行传递。直接缓冲区的主要缺点是在分配内存空间和释放内存时比堆缓冲区更复杂,而Netty使用内存池来解决这样的问题,这也是Netty使用内存池的原因之一。直接缓冲区不支持数组存储,但是可以通过getBytes方法,将数据存到我们自己建的数组中。
ByteBuf directBuf = ...;
if (!directBuf.hasArray()) {
int length = directBuf.readableBytes();
byte[] array = new byte[length];
directBuf.getBytes(directBuf.readerIndex(), array);
...
}
复合缓冲区(Composite Buffer):
将多个ByteBuf组合成一个缓冲区。因为CompositeByteBuf 中的 ByteBuf 实例可能同时包含直接内存分配和非直接内存分配。如果其中只有一个实例,那么对 CompositeByteBuf 上的 hasArray()方法的调用将返回该组件上的 hasArray()方法的值;否则它将返回 false。
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
ByteBuf heapBuf = Unpooled.buffer(8);
ByteBuf directBuf = Unpooled.directBuffer(16);
//添加ByteBuf到CompositeByteBuf
compBuf.addComponents(heapBuf,directBuf);
//删除第一个ByteBuf
compBuf.removeComponent(0);
Iterator<ByteBuf> iter = compBuf.iterator();
while(iter.hasNext()){
System.out.println(iter.next().toString());
}
//使用数组访问数据
if(!compBuf.hasArray()){
int len = compBuf.readableBytes();
byte[] arr = new byte[len];
compBuf.getBytes(0, arr);
}
(4)操作
访问索引:
ByteBuf的索引是从0开始到capacity() - 1结束。可以和数组一样使用索引号来访问ByteBuf里的数据.
//创建16个字节的 ByteBuf
ByteBuf buf = Unpooled.buffer(16);
//写数据
for(int i=0;i<16;i++){
buf.writeByte(i+1);
}
//获取数据
for(int i=0;i<buf.capacity();i++){
System.out.println(buf.getByte(i));
}
使用索引访问数据时,ByteBuf的读索引和写索引不会变化,可以通过ByteBuf的readerIndex()或writerIndex()来分别推进读索引或写索引。
ByteBuf在内存中的状态如下所示:
可丢弃字节:
可丢弃字节表示这部分的数据已经被读取过,可以丢弃。通过调用 discardReadBytes()方法,可以丢弃它们并回收空间。执行方法后的状态如下:
可读字节被移动到缓冲区最前端,writerIndex也相应向前移。由此可知discardReadBytes方法可能会导致内存复制,最好不要频繁调用,以免影响性能。
可读字节:
可读字节表示已经存储了具体数据,但是还没有被读取过。新分配的、包装的或者复制的缓冲区的默认的readerIndex值为0,任何以read或者skip开头的操作都将移动readerIndex。如果read操作的参数也是一个ByteBuf作为目标缓冲区,表示将源ByteBuf的的数据读取出来并写入到目标ByteBuf里,目标ByteBuf的writerIndex也会随之增加,没有可读数据时尝试读取会报IndexOutOfBoundsException。
ByteBuf buf = Unpooled.buffer(16);
while(buf.isReadable()){
System.out.println(buf.readByte());
}
可写字节:
可写字节表示还未被写入的字节。任何以write开头的操作都会增加writerIndex的值。新分配的缓冲区writerIndex的默认值是0,如果write操作的参数也是一个ByteBuf作为目标缓冲区,表示将目标缓冲区的数据写入到源缓冲区,目标缓冲区的readerIndex也会随之增加。
//写入整数类型的数据
Random random = new Random();
ByteBuf buf = Unpooled.buffer(16);
while(buf.writableBytes() >= 4){
buf.writeInt(random.nextInt());
}
索引的重置与恢复:
ByteBuf提供了markReaderIndex()、markWriterIndex()、resetWriterIndex()、resetReaderIndex()等方法来标记和重置readerIndex和writerIndex。也可以通过调用readerIndex(int)或者 writerIndex(int)来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException。还可以通过调用 clear()方法来将readerIndex和writerIndex都设置为 0,但是clear()方法不会清除数据。
调用clear()方法比discardReadBytes()节省资源,因为它只操作索引而不会复制内容。
查找操作:
ByteBuf提供了indexOf()方法,可以查找。对于一些复杂的操作,可以通过那些需要一个ByteBufProcessor(Netty 4.1.x中被弃用,使用io.netty.util.ByteProcessor替代)类型的参数的方法。
//查找回车符(\r)
ByteBuf buffer = Unpooled.buffer(16);
int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);
派生缓冲区:
ByteBuf提供了以下几种方法来创建一个派生缓冲区:
duplicate();
slice();
slice(int, int);
Unpooled.unmodifiableBuffer(…);
order(ByteOrder);
readSlice(int)。
这些方法会返回一个ByteBuf实例,它具有自己的读索引、写索引和标记索引。但是实际存储是和源ByteBuf共享的。修改了内容会导致源ByteBuf也会被修改。如果需要完全复制,需要调用copy()或者 copy(int, int)方法,会返回一个拥有独立数据副本的ByteBuf。
Charset utf8 = Charset.forName("UTF-8");
//创建一个ByteBuf
ByteBuf buf = Unpooled.copiedBuffer("“Netty in Action rocks!“", utf8);
ByteBuf sliced = buf.slice(0, 14);
ByteBuf copy = buf.copy(0, 14);
// 输出"“Netty in Action rocks!“"
System.out.println(buf.toString(utf8));
// print "“Netty in Act"
System.out.println(sliced.toString(utf8));
// print "“Netty in Act"
System.out.println(copy.toString(utf8));
读写操作:
ByteBuf有两种读写操作:
get()和 set()操作,从给定的索引开始,并且保持索引不变;
read()和 write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。
get方法:
名称 | 描述 |
---|---|
getBoolean(int) | 返回给定索引处的 Boolean 值 |
getByte(int) | 返回给定索引处的字节 |
getUnsignedByte(int) | 将给定索引处的无符号字节值作为 short 返回 |
getMedium(int) | 返回给定索引处的 24 位的中等 int 值 |
getUnsignedMedium(int) | 返回给定索引处的无符号的 24 位的中等 int 值 |
getInt(int) | 返回给定索引处的 int 值 |
getUnsignedInt(int) | 将给定索引处的无符号 int 值作为 long 返回 |
getLong(int) | 返回给定索引处的 long 值 |
getShort(int) | 返回给定索引处的 short 值 |
getUnsignedShort(int) | 将给定索引处的无符号 short 值作为 int 返回 |
getBytes(int, …) | 将该缓冲区中从给定索引开始的数据传送到指定的目的地 |
set方法:
名称 | 描述 |
---|---|
setBoolean(int, boolean) | 设定给定索引处的 Boolean 值 |
setByte(int index, int value) | 设定给定索引处的字节值 |
setMedium(int index, int value) | 设定给定索引处的 24 位的中等 int 值 |
setInt(int index, int value) | 设定给定索引处的 int 值 |
setLong(int index, long value) | 设定给定索引处的 long 值 |
setShort(int index, int value) | 设定给定索引处的 short 值 |
read方法:
名称 | 描述 |
---|---|
readBoolean() | 返回当前readerIndex处的Boolean,并将readerIndex增加 1 |
readByte() | 返回当前readerIndex处的字节,并将readerIndex增加 1 |
readUnsignedByte() | 将当前readerIndex处的无符号字节值作为short返回,并将readerIndex增加 1 |
readMedium() | 返回当前 readerIndex 处的 24 位的中等 int 值,并将 readerIndex增加 3 |
readUnsignedMedium() | 返回当前 readerIndex 处的 24 位的无符号的中等 int 值,并将readerIndex 增加 3 |
readInt() | 返回当前 readerIndex 的 int 值,并将 readerIndex 增加 4 |
readUnsignedInt() | 将当前 readerIndex 处的无符号的 int 值作为 long 值返回,并将readerIndex 增加 4 |
readLong() | 返回当前 readerIndex 处的 long 值,并将 readerIndex 增加 8 |
readShort() | 返回当前 readerIndex 处的 short 值,并将 readerIndex 增加 2 |
readUnsignedShort() | 将当前 readerIndex 处的无符号 short 值作为 int 值返回,并将readerIndex 增加 2 |
readBytes(ByteBuf | byte[] destination,int dstIndex [,int length]) | 将当前 ByteBuf 中从当前 readerIndex 处开始的(如果设置了,length 长度的字节)数据传送到一个目标 ByteBuf 或者 byte[],从目标的 dstIndex 开始的位置。本地的 readerIndex 将被增加已经传输的字节数 |
write方法:
名称 | 描述 |
---|---|
writeBoolean(boolean) | 在当前 writerIndex 处写入一个 Boolean,并将 writerIndex 增加 1 |
writeByte(int) | 在当前 writerIndex 处写入一个字节值,并将 writerIndex 增加 1 |
writeMedium(int) | 在当前 writerIndex 处写入一个中等的 int 值,并将 writerIndex增加 3 |
writeInt(int) | 在当前 writerIndex 处写入一个 int 值,并将 writerIndex 增加 4 |
writeLong(long) | 在当前 writerIndex 处写入一个 long 值,并将 writerIndex 增加 8 |
writeShort(int) | 在当前 writerIndex 处写入一个 short 值,并将 writerIndex 增加 2 |
writeBytes(source ByteBuf |byte[] [,int srcIndex,int length]) | 从当前 writerIndex 开始,传输来自于指定源(ByteBuf 或者 byte[])的数据。如果提供了 srcIndex 和 length,则从 srcIndex 开始读取,并且处理长度为 length 的字节。当前 writerIndex 将会被增加所写入的字节数 |
其他方法:
名称 | 描述 |
---|---|
isReadable() | 如果至少有一个字节可供读取,则返回 true |
isWritable() | 如果至少有一个字节可被写入,则返回 true |
readableBytes() | 返回可被读取的字节数 |
writableBytes() | 返回可被写入的字节数 |
capacity() | 返回 ByteBuf 可容纳的字节数。在此之后,它会尝试再次扩展直到达到 maxCapacity() |
maxCapacity() | 返回 ByteBuf 可以容纳的最大字节数 |
hasArray() | 如果 ByteBuf 由一个字节数组支撑,则返回 true |
array() | 如果 ByteBuf 由一个字节数组支撑则返回该数组;否则,它将抛出一个UnsupportedOperationException 异常 |
(5)工具类
ByteBufHolder接口:
ByteBufHolder是一个辅助类,默认实现类是DefaultByteBufHolder。ByteBufHolder的作用就是帮助更方便的访问ByteBuf中的数据。比如,拷贝、释放资源、缓冲区池化等。
名称 | 描述 |
---|---|
content() | 返回由这个 ByteBufHolder 所持有的 ByteBuf |
copy() | 返回这个 ByteBufHolder 的一个深拷贝,包括一个其所包含的 ByteBuf 的非共享拷贝 |
duplicate() | 返回这个 ByteBufHolder 的一个浅拷贝,包括一个其所包含的 ByteBuf 的共享拷贝 |
ByteBufAllocator接口:
ByteBufAllocator用来实现ByteBuf的池化,他可以用来分配任意类型的ByteBuf实例。
ByteBufAllocator 的方法:
名称 | 描述 |
---|---|
buffer() buffer(int initialCapacity); buffer(int initialCapacity, int maxCapacity); | 返回一个基于堆或者直接内存存储的 ByteBuf |
heapBuffer() heapBuffer(int initialCapacity) heapBuffer(int initialCapacity, int maxCapacity) | 返回一个基于堆内存存储的ByteBuf |
directBuffer() directBuffer(int initialCapacity) directBuffer(int initialCapacity, int maxCapacity) | 返回一个基于直接内存存储的ByteBuf |
compositeBuffer() compositeBuffer(int maxNumComponents) compositeDirectBuffer() compositeDirectBuffer(int maxNumComponents); compositeHeapBuffer() compositeHeapBuffer(int maxNumComponents); | 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf |
ioBuffer() | 返回一个用于套接字的 I/O 操作的 ByteBuf |
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();
ByteBufAllocator有两种不同的实现:PooledByteBufAllocator和UnpooledByteBufAllocator,前者池化了ByteBuf的实例将分配和回收成本以及内存使用降到最低;后者是每次使用都创建一个新的ByteBuf实例。Netty默认使用PooledByteBufAllocator,可以通ChannelConfig或通过引导设置一个不同的实现来改变。
Unpooled:
Unpooled也是用来创建缓冲区的工具类,提供了静态的辅助方法来创建未池化的 ByteBuf
实例。
名称 | 描述 |
---|---|
buffer()、buffer(int initialCapacity)、buffer(int initialCapacity, int maxCapacity) | 返回一个未池化的基于堆内存存储的 |
ByteBuf、directBuffer()、directBuffer(int initialCapacity)、directBuffer(int initialCapacity, int maxCapacity) | 返回一个未池化的基于直接内存存储的 ByteBuf |
wrappedBuffer() | 返回一个包装了给定数据的 ByteBuf |
copiedBuffer() | 返回一个复制了给定数据的 ByteBuf |
compositeBuffer() | 返回一个复合ByteBuf |
//创建复合缓冲区
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
//创建堆缓冲区
ByteBuf heapBuf = Unpooled.buffer(8);
//创建直接缓冲区
ByteBuf directBuf = Unpooled.directBuffer(16);
(6)引用计数
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty 在第 4 版中为 ByteBuf 和 ByteBufHolder 引入了引用计数技术,它们都实现了 interface ReferenceCounted。
它的设计思路是跟踪某个特定对象的活动引用的数量。活动的引用计数以1作为开始,只要引用计数大于 0,就能保证对象不会被释放。当活动引用的数量减少到 0 时,该实例就会被释放。访问一个已经释放的对象,将会导致一个IllegalReferenceCountException。
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
ByteBuf buffer = allocator.directBuffer();
//检查引用计数是否为预期的 1
assert buffer.refCnt() == 1;
//释放资源
boolean released = buffer.release();
2、ChannelHandler和ChannelPipeline
(1)简介
ChannelHandler:
Netty的核心是是基于事件驱动的,而ChannelHandler就是用来响应事件和处理数据的容器。ChannelHandler可以处理几乎任何事件。
Netty的数据流有两个方向,从服务端的角度来讲,数据从客户端到服务端称为入站数据,反之,称为出站数据。
ChannelHandler派生出ChannelInboundHandler和ChannelOutboundHandler接口,分别表示入站数据处理Handler和出站数据处理Handler。
ChannelPipeline:
ChannelPipeline是一个将ChannelHandler组装成链,并定义了用于在该链上传播入站和出站事件流的 API。当Channel创建时,会自动分配一个专属的ChannelPipeline。我们可以将ChannelHandler按顺序依次添加到ChannelPipeline中,组成ChannelHandler链。
ChannelPipeline入站和出站的ChannelHandler:
(2)ChannelHandler
Channel 的生命周期:
Channel接口定义了4个与入站相关的状态:
状态 | 描述 |
---|---|
ChannelUnregistered | Channel 已经被创建,但还未注册到 EventLoop |
ChannelRegistered | Channel 已经被注册到了 EventLoop |
ChannelActive | Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了 |
ChannelInactive | Channel 没有连接到远程节点 |
当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给ChannelPipeline的 ChannelHandler,其可以随后对它们做出响应。 | |
ChannelHandler的生命周期: | |
ChannelHandler中定义了关于ChannelHandler的生命周期事件方法,在ChannelHandler被添加到ChannelPipeline中或者被从ChannelPipeline中移除时会调用这些事件方法。 | |
类型 | 描述 |
– | – |
handlerAdded | 当把 ChannelHandler 添加到 ChannelPipeline 中时被调用 |
handlerRemoved | 当从 ChannelPipeline 中移除 ChannelHandler 时被调用 |
exceptionCaught | 当处理过程中在 ChannelPipeline 中有错误产生时被调用 |
ChannelInboundHandler接口:
ChannelInboundHandler接口表示入站事件处理的Handler。定义了一组入站事件方法,当对应的事件触发时,就会调用对应的事件方法。
方法 | 描述 |
---|---|
channelRegistered | 当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用 |
channelUnregistered | 当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用 |
channelActive | 当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪 |
channelInactive | 当 Channel 离开活动状态并且不再连接它的远程节点时被调用 |
channelReadComplete | 当Channel上的一个读操作完成时被调用(最后一节数据被读完时) |
channelRead | 当从 Channel 读取数据时被调用 |
ChannelWritabilityChanged | 当 Channel 的可写状态发生改变时被调用。用户可以确保写操作不会完成得太快(以避免发生 OutOfMemoryError)或者可以在 Channel 变为再次可写时恢复写入。可以通过调用 Channel 的 isWritable()方法来检测Channel 的可写性。与可写性相关的阈值可以通过 Channel.config().setWriteHighWaterMark()和 Channel.config().setWriteLowWaterMark()方法来设置 |
userEventTriggered | 当 ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用,因为一个 POJO 被传经了 ChannelPipeline |
当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,需要显式的释放资源。
Netty提供了ReferenceCountUtil.release()用来释放资源。或者使用Netty自带的SimpleChannelInboundHandler,它的channelRead0方法会自动释放资源。
注意:使用SimpleChannelInboundHandler时,如果指定了泛型的类型,那么SimpleChannelInboundHandler的上一个InboundHandler处理完后并传到下一个inboundHandler(也就是SimpleChannelInboundHandler)的消息类型需要和指定泛型的类型一致,否则会跳过SimpleChannelInboundHandler执行下一个InboundHandler。
public class DemoHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ReferenceCountUtil.release(msg); //手动释放资源
}
}
public class SimpleDemoHandler extends SimpleChannelInboundHandler<Object> {
@Override
public void channelRead0(ChannelHandlerContext ctx,Object msg) {
// 不需要手动释放
}
}
ChannelOutboundHandler接口:
ChannelOutboundHandler表示出站事件处理的Handler。
方法 | 描述 |
---|---|
bind(ChannelHandlerContext,SocketAddress,ChannelPromise) | 当请求将 Channel 绑定到本地地址时被调用 |
connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise) | 当请求将 Channel 连接到远程节点时被调用 |
disconnect(ChannelHandlerContext,ChannelPromise) | 当请求将 Channel 从远程节点断开时被调用 |
close(ChannelHandlerContext,ChannelPromise) | 当请求关闭 Channel 时被调用 |
deregister(ChannelHandlerContext,ChannelPromise) | 当请求将 Channel 从它的 EventLoop 注销时被调用 |
read(ChannelHandlerContext) | 当请求从 Channel 读取更多的数据时被调用 |
flush(ChannelHandlerContext) | 当请求通过 Channel 将入队数据冲刷到远程节点时被调用 |
write(ChannelHandlerContext,Object,ChannelPromise) | 当请求通过 Channel 将数据写到远程节点时被调用 |
ChannelOutboundHandler中的大部分方法都需要一个ChannelPromise参数,以便在操作完成时得到通知。 |
ChannelHandler 适配器:
Netty提供ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter两个适配器类。分别实现了ChannelInboundHandler和ChannelOutboundHandler接口。他们重写的方法里通过调用ChannelHandlerContext的等效方法,使得事件可以传播下去。
资源释放:
通过调用 ChannelInboundHandler.channelRead()或ChannelOutboundHandler.write()方法来处理数据时,如果我们没有将事件往下传播,事件最后没有传到head或者tail处时,我们需要手动释放资源,或者我们重写异常事件时,也要手动释放资源。资源指的是入站或者出站的数据。
public class DemoInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
//通过调用 ReferenceCountUtil.release()释放资源
ReferenceCountUtil.release(msg);
}
}
public class DemoOutboundHandlerextends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx,Object msg, ChannelPromise promise) {
//通过调用 ReferenceCountUtil.release()释放资源
ReferenceCountUtil.release(msg);
//通知知 ChannelPromise数据已经被处理了
promise.setSuccess();
}
}
Netty提供了SimpleChannelInboundHandler类,它的channelRead事件响应方法为channelRead0(),在处理完后会自动释放资源。
(3)ChannelPipeline
前面说到每个Channel都有且仅有一个 ChannelPipeline 与之对应。ChannelPipeline 组装了一条Handler链,也可以说是ChannelHandlerContext链,因为每个Handler都会有一个ChannelHandlerContext,用来与同一个ChannelPipeline中的其他ChannelHandler进行交互。这个链表的头是HeadContext,链表的尾是TailContext,并且每个contex都与一个Handler对应。head和tail都实现了ChannelHandlerContext接口,而head实现了ChannelOutboundHandler接口,tail实现了ChannelInboundHandler,所以head和tail既是contex也是Handler,head是OutBoundHandler,tail是InboundHandler(在代码中有两个属性:boolean inbound ,boolean outbound。表示是什么类型的Handler)。
Netty中的传播事件可以分为两种:Inbound(入站)事件和Outbound(出站)事件。
Outbound事件传播机制:
Outbound事件的传播机制是tail->OutboundContext->head。Outbound事件的传播以tail开始,从后往前寻找第一个OutboundContext,而context会找到其关联的Handler调用对应的事件方法。如果想继续往前传,需要通过context调用对应事件的方法,就会传到下一个OutboundContext中(如果实现的是适配器类ChannelOutboundHandlerAdapter,可以不用重写对应事件方法,适配器已经重写了事件方法,并且调用了传播方法,但是如果需要重写事件方法,还是需要自己手动调用传播),Outbound事件最后会传到head中,由head(head是一个OutBoundHandler)的对应事件方法处理
Inbound事件传播机制:
Inbound 事件和 Outbound 事件的处理过程是类似的,只是传播方向不同。head>InboundContext->tail。Inbound 事件以head开始,从头开始寻找第一个InboundContext,而context会找到其关联的Handler调用对应的事件方法。同Outbound一样,也需要调用context对应事件的方法,将事件传播到下一个InboundContext。(同样有ChannelInboundHandlerAdapter适配器类,作用和OutboundHandler的适配器相同),最后传到tail里,而tail是一个InboundHandler,所以由tail最后处理。
ChannelHandlerContext使得ChannelHandler能够和它的ChannelPipeline以及其他的
ChannelHandler交互。 ChannelHandler可以通知其所属的ChannelPipeline中的下一 个
ChannelHandler,甚至可以动态修改它所属的ChannelPipeline。
修改 ChannelPipeline:
ChannelHandler可以通过添加、删除或者替换其他的 ChannelHandler 来实时地修改
ChannelPipeline的布局。(它也可以将它自己从 ChannelPipeline 中移除)。
名称 | 描述 |
---|---|
addFirst | 将一个ChannelHandler 添加到ChannelPipeline开头 |
addBefore | 将ChannelHandler添加到ChannelPipeline中指定名称的ChannelHandler之前 |
addAfter | 将ChannelHandler添加到ChannelPipeline中指定名称的ChannelHandler之后 |
addLast | 将一个ChannelHandler添加到ChannelPipeline的末尾 |
remove | 将一个ChannelHandler 从ChannelPipeline 中移除 |
replace | 将 ChannelPipeline 中的一个 ChannelHandler 替换为另一个 ChannelHandler |
ChannelPipeline pipeline = ch.pipeline();
FirstHandler firstHandler = new FirstHandler();
pipeline.addLast("handler1", firstHandler);
pipeline.addFirst("handler2", new SecondHandler());
pipeline.addLast("handler3", new ThirdHandler());
pipeline.remove("“handler3“");
pipeline.remove(firstHandler);
pipeline.replace("handler2", "handler4", new FourthHandler());
ChannelPipeline用于访问ChannelHandler的方法:
名称 | 描述 |
---|---|
get | 通过类型或者名称返回 ChannelHandler |
context | 返回和 ChannelHandler 绑定的 ChannelHandlerContext |
names | 返回 ChannelPipeline 中所有 ChannelHandler 的名称 |
ChannelPipeline的事件:
ChannelPipeline 的入站事件方法:
名称 | 描述 |
---|---|
fireChannelRegistered | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelRegistered(ChannelHandlerContext)方法 |
fireChannelUnregistered | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelUnregistered(ChannelHandlerContext)方法 |
fireChannelActive | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelActive(ChannelHandlerContext)方法 |
fireChannelInactive | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelInactive(ChannelHandlerContext)方法 |
fireExceptionCaught | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的exceptionCaught(ChannelHandlerContext, Throwable)方法 |
fireUserEventTriggered | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的userEventTriggered(ChannelHandlerContext, Object)方法 |
fireChannelRead | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelRead(ChannelHandlerContext, Object msg)方法 |
fireChannelReadComplete | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelReadComplete(ChannelHandlerContext)方法 |
fireChannelWritabilityChanged | 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelWritabilityChanged(ChannelHandlerContext)方法 |
ChannelPipeline 的出站事件方法:
名称 | 描述 |
---|---|
bind | 将 Channel 绑定到一个本地地址,这将调用 ChannelPipeline 中的下一个ChannelOutboundHandler 的 bind(ChannelHandlerContext, SocketAddress, ChannelPromise)方法 |
connect | 将 Channel 连接到一个远程地址,这将调用 ChannelPipeline 中的下一个ChannelOutboundHandler 的 connect(ChannelHandlerContext, SocketAddress, ChannelPromise)方法 |
disconnect | 将Channel 断开连接。这将调用ChannelPipeline 中的下一个ChannelOutboundHandler 的 disconnect(ChannelHandlerContext, Channel Promise)方法 |
close | 将 Channel 关闭。这将调用 ChannelPipeline 中的下一个 ChannelOutboundHandler 的 close(ChannelHandlerContext, ChannelPromise)方法 |
deregister | 将 Channel 从它先前所分配的 EventExecutor(即 EventLoop)中注销。这将调用 ChannelPipeline 中的下一个 ChannelOutboundHandler 的 deregister(ChannelHandlerContext, ChannelPromise)方法 |
flush | 冲刷Channel所有挂起的写入。这将调用ChannelPipeline中的下一个ChannelOutboundHandler 的 flush(ChannelHandlerContext)方法 |
write | 将消息写入 Channel。这将调用 ChannelPipeline 中的下一个 ChannelOutboundHandler的write(ChannelHandlerContext, Object msg, ChannelPromise)方法。注意:这并不会将消息写入底层的 Socket,而只会将它放入队列中。要将它写入 Socket,需要调用 flush()或者 writeAndFlush()方法 |
writeAndFlush | 这是一个先调用 write()方法再接着调用 flush()方法的便利方法 |
read | 请求从 Channel 中读取更多的数据。这将调用 ChannelPipeline 中的下一个ChannelOutboundHandler 的 read(ChannelHandlerContext)方法 |
ChannelPipeline保存了与Channel相关联的 ChannelHandler;ChannelPipeline 可以根据需要,通过添加或者删除 ChannelHandler 来动态地修改;ChannelPipeline 有着丰富的 API 用以被调用,以响应入站和出站事件。
(4)ChannelHandlerContext
ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeline 之间的关联,每当有 ChannelHandler 添加到 ChannelPipeline 中时,都会创建 ChannelHandlerContext。ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。
ChannelHandlerContext定义了一些响应事件的方法。其中以fire开头的方法是响应入站事件的,其他的是出站事件的方法
名称 | 描述 |
---|---|
alloc | 返回和这个实例相关联的Channel 所配置的 ByteBufAllocator |
bind | 绑定到给定的 SocketAddress,并返回 ChannelFuture |
channel | 返回绑定到这个实例的 Channel |
close | 关闭 Channel,并返回 ChannelFuture |
connect | 连接给定的 SocketAddress,并返回 ChannelFuture |
deregister | 从之前分配的 EventExecutor 注销,并返回 ChannelFuture |
disconnect | 从远程节点断开,并返回 ChannelFuture |
executor | 返回调度事件的 EventExecutor |
fireChannelActive | 触发对下一个 ChannelInboundHandler 上的channelActive()方法(已连接)的调用 |
fireChannelInactive | 触发对下一个 ChannelInboundHandler 上的channelInactive()方法(已关闭)的调用 |
fireChannelRead | 触发对下一个 ChannelInboundHandler 上的channelRead()方法(已接收的消息)的调用 |
fireChannelReadComplete | 触发对下一个ChannelInboundHandler上的channelReadComplete()方法的调用 |
fireChannelRegistered | 触发对下一个 ChannelInboundHandler 上的channelRegistered()方法的调用 |
fireChannelUnregistered | 触发对下一个 ChannelInboundHandler 上的channelUnregistered()方法的调用 |
fireChannelWritabilityChanged | 触发对下一个 ChannelInboundHandler 上的channelWritabilityChanged()方法的调用 |
fireExceptionCaught | 触发对下一个 ChannelInboundHandler 上的exceptionCaught(Throwable)方法的调用 |
fireUserEventTriggered | 触发对下一个 ChannelInboundHandler 上的userEventTriggered(Object evt)方法的调用 |
handler | 返回绑定到这个实例的 ChannelHandler |
isRemoved | 如果所关联的 ChannelHandler 已经被从 ChannelPipeline中移除则返回 true |
name | 返回这个实例的唯一名称 |
pipeline | 返回这个实例所关联的 ChannelPipeline |
read | 将数据从Channel读取到第一个入站缓冲区;如果读取成功则触发 一个channelRead事件,并(在最后一个消息被读取完成后)通 知 ChannelInboundHandler 的 |
write | 通过这个实例写入消息并经过 ChannelPipeline |
writeAndFlush | 通过这个实例写入并冲刷消息并经过 ChannelPipeline |
Channel和ChannelPipeline也有一些响应事件的方法。但是调用ChannelHandlerContext的方法会将事件传播给下一个Handler(如果是入站事件,则是下一个InboundHandler,如果是出站事件,则是上一个OutboundHandler,和这些Handler注册的顺序有关)。如果调用的是Channel或ChannelPipeline的方法,则事件是从头或者尾开始传播。入站事件就从前向后沿着InboundHandler最后由tail(tail是一个InboundHandler)处理。出站事件就从后向前沿着outboundHandler最后由head(head是一个outboundHandler)处理。
注意如果Handler里重写了事件对应的方法,需要手动调用ChannelHandlerContext向下传播的方法,否则传播会中断。ChannelHandler适配器中对于每个事件方法,都手动调用了传播方法。
用法:
public class WriteHandler extends ChannelHandlerAdapter {
private ChannelHandlerContext ctx;
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
//保存ctx,以供稍后使用
this.ctx = ctx;
}
public void send(String msg) {
//发送消息
ctx.writeAndFlush(msg);
}
}
因为一个ChannelHandler可以属于多个ChannelPipeline,所以它也可以绑定到多
个ChannelHandlerContext实例,但是在一个ChannelPipeline中,ChannelHandler和ChannelHandlerContext是一一对应的。在多个ChannelPipeline中共享同一个ChannelHandler,对应的 ChannelHandler 必须要使用@Sharable 注解标注。
@Sharable
public class SharableHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("Channel read message: " + msg);
//将事件传播给下一个Handler
ctx.fireChannelRead(msg);
}
}
ChannelHandler被用于多个ChannelPipeline时,要注意ChannelHandler的线程安全问题。
(5)异常处理
异常处理是任何真实应用程序的重要组成部分,它也可以通过多种方式来实现。因此Netty提供了几种方式用于处理入站或者出站处理过程中所抛出的异常。
入站异常:
如果入站事件处理时出现异常,那么它会从当前InboundHandler的exceptionCaught方法开始,在ChannelPipeline的InboundHandler传播,所以最好在最后一个InboundHandler中处理异常信息
public class InboundExceptionHandler extends ChannelInboundHandlerAdapter {
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) {
cause.printStackTrace();
//关闭channel
ctx.close();
}
}
出站异常:
出站与入站不同。每个出站操作都将返回一个ChannelFuture,注册到 ChannelFuture的ChannelFutureListener将在操作完成时被通知该操作是成功了还是出错了。几乎所有的 ChannelOutboundHandler上的方法都会传入一个ChannelPromise的实例。作为ChannelFuture的子类,ChannelPromise 也可以被分配用于异步通知的监听器。但是,ChannelPromise 还具有提供立即通知的可写方法:
ChannelPromise setSuccess();
ChannelPromise setFailure(Throwable cause);
出站事件的异常处理有以下两种方式:
ChannelFuture future = channel.write(someMessage);
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) {
if (!f.isSuccess()) {
f.cause().printStackTrace();
f.channel().close();
}
}
});
public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg,ChannelPromise promise) {
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) {
if (!f.isSuccess()) {
f.cause().printStackTrace();
f.channel().close();
}
}
});
}
}
3、EventLoop和Reactor模型
(1)Reactor模型
Reactor是一种事件驱动的编程模型。处理机制是:主程序将事件以及对应事件处理的方法在Reactor上进行注册, 如果相应的事件发生,Reactor将会主动调用事件注册的接口,即回调函数。Reactor模型主要由以下几个核心组件组成:
事件:需要监控的具体动作。比如网络通信中的读事件、写事件、连接事件等。
Reactor (反应器):事件管理器,处理事件的注册,注销,事件“就绪”时,调用事件的回调函数。
Acceptor(监听器):用来监听事件的到达,采用多路复用机制,当事件到达后,通知Reactor调用事件处理程序进行处理。
Event Handler(事件处理程序):定义了事件的回调函数,在相应的事件发生时调用。
多路复用:可以简单的理解为,由一个服务端线程监听多个客户端,当监听到有连接就绪时,再开启其他线程去处理。而不是一个线程对应一个连接。
Reactor有三种线程模型:单线程模型、多线程模型、主从多线程模型。
单线程模型
单线程模型即Acceptor处理和Handler处理在一个线程里处理。缺点显而易见,当某个Handler阻塞时,将会导致整个服务(Acceptor也被阻塞了)不可用。所以这种模式用的比较少。
多线程模型
由一个专门的线程处理Acceptor监听。就绪的连接分配给一个线程池来处理,每个连接分配一个线程处理。
主从多线程模型
如果服务端和客户端的连接需要进行一些校验处理,那一个Acceptor线程可能处理不过来,会导致大量的客户端不能连接到服务器。所以就需要主从多线程模型来处理。与多线程模型相比,主从多线程模型的Acceptor处理由一个线程变成一个线程池来处理。
(2)EventLoopGroup
EventLoopGroup可以简单的认为就是一个线程池。
单线程模型示例:
//设定bossGroup中线程的数量为1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap server = new ServerBootstrap();
//只设置一个参数表示Acceptor和事件处理都由这一个线程池处理
server.group(bossGroup);
多线程模型:
//设置线程池的线程数量为128
EventLoopGroup bossGroup = new NioEventLoopGroup(128);
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup);
主从线程模型:
//不设置数量,默认线程数是CPU核心数乘以2
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
//设置两个参数,表示Acceptor和事件处理分别由两个不同的线程池处理
b.group(bossGroup, workerGroup);
(3)EventLoop
运行任务来处理在连接的生命周期内发生的事件是任何网络框架的基本功能。而Netty使用interface io.netty.channel.EventLoop来表示任务。
以NioEventLoop为例,它集成自SingleThreadEventLoop,而 SingleThreadEventLoop 又继承自 SingleThreadEventExecutor。而SingleThreadEventExecutor是Netty中对本地线程的抽象,它内部有一个Thread thread属性,存储了一个本地 Java线程。所以一个NioEventLoop绑定了一个Thread,在其生命周期内都不会变。
通常来说,NioEventLoop 负责执行两个任务:第一个任务是作为 IO 线程,执行与 Channel 相关的 IO 操作,包括调用 Selector 等待就绪的 IO 事件、读写数据与数据的处理等;而第二个任务是作为任务队列,执行 taskQueue 中的任务,例如用户调用 eventLoop.schedule 提交的定时任务也是这个线程执行的。
所以任务(Runnable 或者 Callable)可以直接提交给 EventLoop 实现,以立即执行或者调度执行,根据配置和可用核心的不同,可能会创建多个 EventLoop 实例用以优化资源的使用,并且单个EventLoop 可能会被指派用于服务多个 Channel。
Channel、EventLoop 和 EventLoopGroup的关系:
- 一个 EventLoopGroup 包含一个或者多个 EventLoop;
- 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
- 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
- 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
- 一个 EventLoop 可能会被分配给一个或多个 Channel。
因为Channel 的 I/O 操作都是由相同的 Thread 执行的,所以在同一个channel内可以不考虑同步。
任务调度
java自带的任务调度:
//创建一个其线程池具有 10 个线程的ScheduledExecutorService
ScheduledExecutorService executor =Executors.newScheduledThreadPool(10);
ScheduledFuture<?> future = executor.schedule(
new Runnable() {
@Override
public void run() {
System.out.println("60 seconds later");
}
}, 60, TimeUnit.SECONDS);//调度任务在从现在开始的 60 秒之后执行
...
//旦调度任务执行完成,就关闭ScheduledExecutorService 以释放资源
executor.shutdown();
Netty 通过 Channel 的 EventLoop 实现任务调度
Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop().schedule(
new Runnable() {
@Override
public void run() {
System.out.println("60 seconds later");
}
}, 60, TimeUnit.SECONDS);//调度任务在从现在开始的 60 秒之后执行
Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
System.out.println("Run every 60 seconds");
}
}, 60, 60, TimeUnit.Seconds);//调度在 60 秒之后,并且以后每间隔 60 秒运行
//取消任务
boolean mayInterruptIfRunning = false;
future.cancel(mayInterruptIfRunning);
4、Bootstrap
(1)Channel
在 Netty 中,Channel 是一个 Socket 的抽象,它为用户提供了关于 Socket 状态(是否是连接还是断开)以及对 Socket的读写等操作。每当 Netty 建立了一个连接后, 都创建一个对应的 Channel 实例。除了 TCP 协议以外,Netty 还支持很多其他的连接协议, 并且每种协议还有 NIO(非阻塞 IO)和 OIO(Old-IO, 即传统的阻塞 IO)版本的区别。不同协议不同的阻塞类型的连接都有不同的 Channel 类型与之对应下面是一些常用的 Channel:
类名 | 作用 |
---|---|
NioSocketChannel | 异步非阻塞的客户端 TCP Socket 连接 |
NioServerSocketChannel | 异步非阻塞的服务器端 TCP Socket 连接 |
NioDatagramChannel | 异步非阻塞的 UDP 连接 |
NioSctpChannel | 异步的客户端 Sctp(Stream Control Transmission Protocol,流控制传输协议)连接 |
NioSctpServerChannel | 异步的 Sctp 服务器端连接。 |
OioSocketChannel | 同步阻塞的客户端 TCP Socket 连接 |
OioServerSocketChannel | 同步阻塞的服务器端 TCP Socket 连接 |
OioDatagramChannel | 同步阻塞的 UDP 连接 |
OioSctpChannel | 同步的 Sctp客户器端连接。 |
OioSctpServerChannel | 同步的Sctp 服务器端连接 |
(2)Bootstrap
Netty中Bootstrap有两种实现。Bootstrap(客户端)和ServerBootstrap(服务端)。
Bootstrap类也叫引导类,是配置Netty服务器和客户端程序的一个过程。可以通过Bootstrap设置EventLoopGroup、Channel、Handler。Bootstrap的作用是使服务器致力于使用一个父 Channel 来接受来自客户端的连接,并创建子 Channel 以用于它们之间的通信;而客户端将最可能只需要一个单独的、没有父 Channel 的 Channel 来用于所有的网络交互。
客户端Bootstrap
客户端使用的是Bootstrap类。
方法 | 描述 |
---|---|
Bootstrap group(EventLoopGroup) | 设置用于处理 Channel 所有事件的 EventLoopGroup |
Bootstrap channel(Class<? extends C>) | channel()方法指定了Channel的实现类 |
Bootstrap channelFactory(ChannelFactory<? extends C>) | 如果channel实现类没提供默认的构造函数 ,可以通过调用channelFactory()方法来指定一个工厂类,它将会被bind()方法调用 |
Bootstrap localAddress(SocketAddress) | 指定 Channel 应该绑定到的本地地址。如果没有指定,则将由操作系统创建一个随机的地址。或者,也可以通过bind()或者 connect()方法指定 localAddress |
Bootstrap option(ChannelOption option,T value) | 设置 ChannelOption,其将被应用到每个新创建的Channel 的 ChannelConfig。这些选项将会通过bind()或者 connect()方法设置到 Channel,不管哪个先被调用。这个方法在 Channel 已经被创建后再调用将不会有任何的效果。支持的 ChannelOption 取决于使用的 Channel 类型。 |
Bootstrap attr(Attribute key, T value) | 指定新创建的 Channel 的属性值。这些属性值是通过bind()或者 connect()方法设置到 Channel 的,具体取决于谁最先被调用。这个方法在 Channel 被创建后将不会有任何的效果。 |
Bootstrap handler(ChannelHandler) | 设置将被添加到 ChannelPipeline 以接收事件通知的ChannelHandler |
Bootstrap clone() | 创建一个当前 Bootstrap 的克隆,其具有和原始的Bootstrap 相同的设置信息 |
Bootstrap remoteAddress(SocketAddress) | 设置远程地址。或者,也可以通过 connect()方法来指定它 |
ChannelFuture connect() | 连接到远程节点并返回一个 ChannelFuture,其将 会在连接操作完成后接收到通知 |
ChannelFuture bind() | 绑定 Channel 并返回一个 ChannelFuture,其将会在绑定操作完成后接收到通知,在那之后必须调用 Channel.connect()方法来建立连接 |
客户端Bootstrap实例:
Bootstrap 类负责为客户端和使用无连接协议的应用程序创建 Channel
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group) //将创建的group加到bootstrap中
.channel(NioSocketChannel.class) //指定要使用的Channel 实现
.handler(new SimpleChannelInboundHandler<ByteBuf>() {//添加handler
@Override
protected void channeRead0(ChannelHandlerContext ctx,
ByteBuf byteBuf)throws Exception {
System.out.println("Received data");
}
} );
//连接到服务器
ChannelFuture future = bootstrap.connect(new InetSocketAddress("localhost", 80));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture)throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Connection established");
} else {
System.err.println("Connection attempt failed");
channelFuture.cause().printStackTrace();
}
}
} );
注意:
1、group和channel的类型必须相同,不能混用具有不同前缀的组件。否则会导致会导致 IllegalStateException。
2、在调用 bind()或者 connect()方法之前,必须调用group()设置group、channel()或者 channelFactory()设置channel的类型,handler()方法设置handler,如果不这样做,也会导致 llegalStateException。
服务端ServerBootstrap
服务端使用的是ServerBootstrap。
方法 | 描述 |
---|---|
group | 设置 ServerBootstrap 要用的 EventLoopGroup。这个 EventLoopGroup将用于 ServerChannel 和被接受的子 Channel 的 I/O 处理 |
channel | 设置将要被实例化的 ServerChannel 类 |
channelFactory | 如果不能通过默认的构造函数 创建Channel,那么可以提供一个ChannelFactory |
localAddress | 指定 ServerChannel 应该绑定到的本地地址。如果没有指定,则将由操作系统使用一个随机地址。或者,可以通过 bind()方法来指定该 localAddress |
option | 指定要应用到新创建的 ServerChannel 的 ChannelConfig 的 ChannelOption。这些选项将会通过 bind()方法设置到 Channel。在 bind()方法被调用之后,设置或者改变 ChannelOption 都不会有任何的效果。所支持的 ChannelOption 取决于所使用的 Channel 类型。 |
childOption | 指定当子 Channel 被接受时,应用到子 Channel 的 ChannelConfig 的ChannelOption。所支持的 ChannelOption 取决于所使用的 Channel 的类型。 |
attr | 指定 ServerChannel 上的属性,属性将会通过 bind()方法设置给 Channel。在调用 bind()方法之后改变它们将不会有任何的效果 |
childAttr | 将属性设置给已经被接受的子 Channel。接下来的调用将不会有任何的效果 |
handler | 设置被添加到ServerChannel 的ChannelPipeline中的ChannelHandler。更加常用的方法参见 childHandler() |
childHandler | 设置将被添加到已被接受的子 Channel 的 ChannelPipeline 中的 ChannelHandler。handler()方法和 childHandler()方法之间的区别是:前者所添加的 ChannelHandler 由接受子 Channel 的ServerChannel 处理,而childHandler()方法所添加的 ChannelHandler 将由已被接受的子 Channel处理,其代表一个绑定到远程节点的套接字 |
clone | 克隆一个设置和原始的 ServerBootstrap 相同的 ServerBootstrap |
bind | 绑定 ServerChannel 并且返回一个 ChannelFuture,其将会在绑定操作完成后收到通知(带着成功或者失败的结果) |
服务端ServerBootstrap实例:
ServerBootstrap 在 bind()方法被调用时创建了一个 ServerChannel,并且该 ServerChannel 管理了多个子 Channel。
NioEventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class) //指定要使用的Channel 实现
//设置用于处理已被接受的子Channel的I/O及数据的 ChannelInboundHandler
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx,
ByteBuf byteBuf) throws Exception {
System.out.println("Received data");
}
} );
//通过配置好的ServerBootstrap的实例绑定该Channel
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture)throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Server bound");
} else {
System.err.println("Bound attempt failed");
channelFuture.cause().printStackTrace();
}
}
} );
添加多个Handler:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
//通过在实现ChannelInitializer在initChannel方法里通过ChannelPipeline的addLast方法添加多个Handler。
//ChannelInitializer也是一个Handler,但是在初始化时构建Handler链时会把自己从链中删除,所以不会影响
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
//自定义协议编码器
pipeline.addLast(new LengthFieldPrepender(4));
//对象参数类型编码器
pipeline.addLast("encoder",new ObjectEncoder());
//对象参数类型解码器
pipeline.addLast("decoder",new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
pipeline.addLast(new OutDataHandler())
.addLast(new RegistryHandler())
.addLast(new InDataOneHandler())
.addLast(new OutDataOneHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = b.bind(port).sync();
System.out.println("RPC Registry start listen at " + port );
future.channel().closeFuture().sync();
添加的 ChannelOption 和属性
//创建一个 AttributeKey以标识该属性
final AttributeKey<Integer> id = new AttributeKey<Integer>("ID");
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
public void channelRegistered(ChannelHandlerContext ctx)
throws Exception {
//使用 AttributeKey 检索属性以及它的值
Integer idValue = ctx.channel().attr(id).get();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx,ByteBuf byteBuf)
throws Exception {
System.out.println("Received data");
}
}
);
//设置 ChannelOption,其将在 connect()或者bind()方法被调用时被设置到已经创建的Channel 上
bootstrap.option(ChannelOption.SO_KEEPALIVE,true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
//设置id属性
bootstrap.attr(id, 123456);
ChannelFuture future = bootstrap.connect(new InetSocketAddress("localhost", 80));
future.syncUninterruptibly();
无连接的协议
Netty 提供了各种 DatagramChannel 的实现,用来实现无连接协议通信。和SocketChannel的区别是不再调用 connect()方法,而是只调用 bind()方法。
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new OioEventLoopGroup())
//设置channel为OioDatagramChannel
//channel的类型前缀应该和group一致
.channel(OioDatagramChannel.class)
.handler(new SimpleChannelInboundHandler<DatagramPacket>(){
@Override
public void channelRead0(ChannelHandlerContext ctx,DatagramPacket msg)
throws Exception {
}
}
);
ChannelFuture future = bootstrap.bind(new InetSocketAddress(0));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture)throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Channel bound");
} else {
System.err.println("Bind attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
关闭:
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class);
...
//shutdownGracefully()方法将释放所有的资源,并且关闭所有的当前正在使用中的 Channel
Future<?> future = group.shutdownGracefully();
// block until the group has shutdown
future.syncUninterruptibly();
5、编解码器
(1)TCP 粘包/拆包
TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
解决思路:
由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。业界的主流协议的解决方案如下:
1、消息定长,报文大小固定长度,例如每个报文的长度固定为 200 字节,如果不够空位补空格。
2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如 FTP 协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
3、将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段。
4、更复杂的自定义应用层协议。
(2)什么是编解码器
每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节,以及如何将其和目标应用程序的数据格式做相互转换。这种转换逻辑由编解码器处理,编解码器由编码器和解码器组成,它们每种都可以将字节流从一种格式转换为另一种格式。
编码器是将消息转换为适合于传输的格式(最有可能的就是字节流);而对应的解码器则是将网络字节流转换回应用程序的消息格式。因此,编码器操作出站数据,而解码器处理入站数据。
(3)解码器Decoder
Netty中的解码器分为两类:
1、将字节解码为消息——ByteToMessageDecoder 和 ReplayingDecoder;
2、将一种消息类型解码为另一种——MessageToMessageDecoder。
Netty中的解码器基本都是基于这两种类型实现的。解码器继承自ChannelInboundHandler,所以可以看做是一个Handler,使用添加Handler的方式加入到ChannelPipeline中使用。
(1)ByteToMessageDecoder
将字节解码为消息是网络通信时常用的操作。Netty为此提供了ByteToMessageDecoder抽象类。由于不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。ByteToMessageDecoder类的重要方法:
方法 | 描述 |
---|---|
decode(ChannelHandlerContext ctx,ByteBuf in,List out) | 这是你必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据的 ByteBuf,以及一个用来添加解码消息的 List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该 List,或者该 ByteBuf 中没有更多可读取的字节时为止。然后,如果该 List 不为空,那么它的内容将会被传递给ChannelPipeline 中的下一个 ChannelInboundHandler |
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List out) | Netty提供的这个默认实现只是简单地调用了decode()方法。当Channel的状态变为非活动时,这个方法将会被调用一次。可以重写该方法以提供特殊的处理 |
由于 ByteToMessageDecoder 并没有考虑 TCP 粘包和拆包等场景,用户自定义解码器需要自己处理“读半包”问题。正因为如此,大多数场景不会直接继承 ByteToMessageDecoder,而是继承另外一些更高级的解码器来屏蔽半包的处理。
示例:
//读取包含简单 int 的字节流,每个 int都需要被单独处理
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in,List<Object> out)
throws Exception {
if (in.readableBytes() >= 4) {//ByteBuf中是否至少有 4字节可读(一个 int的字节长度)
//从入站 ByteBuf 中读取一个 int,并将其添加到解码消息的 List 中
out.add(in.readInt());
}
}
}
(2)ReplayingDecoder
ReplayingDecoder扩展了ByteToMessageDecoder类,使得我们不必调用 readableBytes()方法。它通过使用一个自定义的ByteBuf(ReplayingDecoderByteBuf)实现了这一点,其将在内部执行时调用。
ReplayingDecoder完整声明如下:类型参数 S 指定了用于状态管理的类型,如果设为Void代表不需要状态管理。
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
public class ToIntegerDecoder2 extends ReplayingDecoder<Void> {
@Override
//传入的 ByteBuf 是ReplayingDecoderByteBuf
public void decode(ChannelHandlerContext ctx, ByteBuf in,List<Object> out)
throws Exception {
//从入站 ByteBuf 中读取一个 int,并将其添加到解码消息的 List 中
out.add(in.readInt());
}
}
(3)MessageToMessageDecoder
使用MessageToMessageDecoder抽象类可以在两个消息格式之间进行转换。
完整声明如下:
public abstract class MessageToMessageDecoder<I>extends ChannelInboundHandlerAdapter
I表示decode()方法的输入参数 msg 的类型,它是你必须实现的唯一方法。
方法 | 描述 |
---|---|
decode(ChannelHandlerContext ctx,I msg,List out) | 对于每个需要被解码为另一种格式的入站消息来说,该方法都将会被调用。解码消息随后会被传递给 ChannelPipeline中的下一个ChannelInboundHandler |
//将Int类型的消息转换为String类型的
public class IntegerToStringDecoder extends MessageToMessageDecoder<Integer> {
@Override
public void decode(ChannelHandlerContext ctx, Integer msgList<Object> out)
throws Exception {
//将 Integer 消息转换为它的 String 表示,并将其添加到输出的 List 中
out.add(String.valueOf(msg));
}
}
(4)编码器Encoder
与解码器相对应的,编码器也有两种类型:
1、将消息编码为字节——MessageToByteEncoder
2、将消息编码为消息——MessageToMessageEncoder
编码器继承自ChannelOutboundHandler,所以可以看做是一个Handler,使用添加Handler的方式加入到ChannelPipeline中使用。
(1)MessageToByteEncoder
使用MessageToByteEncoder将消息转换为字节
方法 | 描述 |
---|---|
encode(ChannelHandlerContext ctx,I msg,ByteBuf out) | encode()方法是你需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为 ByteBuf 的(类型为 I 的)出站消息。该 ByteBuf 随后将会被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler |
public class ShortToByteEncoder extends MessageToByteEncoder<Short> {
@Override
public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out)
throws Exception {
//将short消息写入到ByteBuf中
out.writeShort(msg);
}
}
(2)MessageToMessageEncoder
MessageToMessageEncoder将消息转换为另一种消息。
名称 | 描述 |
---|---|
encode(ChannelHandlerContext ctx,I msg,List out) | 这是你需要实现的唯一方法。每个通过 write()方法写入的消息都将会被传递给 encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler |
//将Integer类型的消息转换为String类型的消息
public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer> {
@Override
public void encode(ChannelHandlerContext ctx, Integer msgList<Object> out)
throws Exception {
out.add(String.valueOf(msg));
}
}
(5)常用的编解码器
LineBasedFrameDecoder:
LineBasedFrameDecoder 是回车换行解码器,如果发送的消息以回车换行符(以\r\n 或者直接以\n 结尾)作为消息结束的标识,则可以直接使用Netty的LineBasedFrameDecoder对消息进行解码。
DelimiterBasedFrameDecoder:
DelimiterBasedFrameDecoder是分隔符解码器,是按照指定分隔符进行解码的解码器, 通过分隔符, 可以将二进制流拆分成完整的数据包。回车换行解码器实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。
FixedLengthFrameDecoder:
FixedLengthFrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑 TCP 的粘包/拆包等问题。
利用 FixedLengthFrameDecoder 解码器,无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder 会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。
LengthFieldPrepender:
LengthFieldPrepender是通用编码器,它可以计算当前待发送消息的二进制字节长度,将该长度添加到 ByteBuf 的缓冲区头中和消息一起发送出去。
LengthFieldBasedFrameDecoder:
LengthFieldBasedFrameDecoder是通用解码器。它有五个参数:
maxFrameLength:帧的最大长度。如果帧的长度大于此值,则将抛出TooLongFrameException。
lengthFieldOffset:长度字段的偏移量:即对应的长度字段在整个消息数据中得位置
lengthFieldLength:长度字段的长度。如:长度字段是int型表示,那么这个值就是4(long型就是8)
lengthAdjustment:要添加到长度字段值的补偿值
initialBytesToStrip:从解码帧中去除的字节数
它主要用来解码被通用编码器编码的数据。
ObjectEncoder/ObjectDecoder:
对象序列化编解码器。
除了这些编解码器,Netty也实现了许多其他的包括http,protobuf,redis,xml等相关的编解码器。