文章目录
阻塞、非阻塞、同步和异步
当用户线程调用一次读写方法时,操作系统会由用户态切换至内核态,由内核态完成真正的数据读写,读写完成后操作系统再由内核态切换为用户态。数据读写过程又分为两个阶段,分别为等待数据阶段和复制数据阶段。如果在第一阶段用户线程在调用读写方法后立即返回,并通过轮询、多路复用等方式监听是否可以进入第二阶段,那么此过程就是非阻塞的,否则就是阻塞的。进入第二阶段后,如果调用读写方法的线程一直等待数据复制完成后返回,那么此过程就是同步的;如果调用读写方法的线程没有等待数据复制完成就直接返回,而是将等待过程交由另一个用户线程,待复制完成后通知调用读写方法的线程来获取数据,那么此过程就是异步的。
在此过程中一共进行了三次状态的切换和四次数据的复制。
NIO
NIO基于Channel和Buffer进行数据的传输,其中Channel是一条双向传输的数据通道,Buffer是一个内存缓冲区,用于暂存写入通道或从通道读取的数据。
Buffer
缓冲区的结构如下:
核心组成部分 | 说明 |
---|---|
位置(position) | 缓冲区中将读取或写入的下一个位置。这个位置值从0开始计,最大值等于缓冲区的大小。 |
容量(capacity) | 缓冲区可以保存的元素的最大数目。容量值在创建缓冲区时设置,此后不能改变。 |
限度((limit) | 缓冲区中可访问数据的末尾位置。只要不改变限度,就无法读/写超过这个位置的数据,即使缓冲区有更大的容量也没有用。 |
标记(mark) | 缓冲区中客户端指定的索引。 |
Buffer clear()//将位置设置为0,将限度设置为容量。
Buffer flip()//将位置设置为0,将限度设置为当前位置。
Buffer rewind()//将位置设置为0。
Buffer mark()//将当前位置作为标记
Buffer reset()//回到标记处
int capacity()//返回容量
int limit()//返回限度
Buffer limit(int newLimit)//设置限度
int position()//返回位置
Buffer position(int newPosition)//设置位置
int remaining()//返回限度减位置
boolean hasRemaining()//返回返回限度吉减位置是否等于零
XxxBuffer
ByteBuffer
是用于存放字节类型数据的缓冲区,其它类型的缓冲区API类似就不再展示。
static ByteBuffer allocate(int capacity)//在堆内存中分配指定大小缓冲区
static ByteBuffer allocateDirect(int capacity)//在直接内存中分配指定大小缓冲区实现零拷贝
static ByteBuffer wrap(byte[] array)//将字节数组包装到缓冲区中。
ByteBuffer compact()//压缩缓冲区,将位置和限度之间的数据移到最左端。位置处于数据的最右侧,限制为容量
ByteBuffer duplicate()//创建共享此缓冲区内容的新字节缓冲区
byte get()
byte get(int index)
ByteBuffer get(byte[] dst)
ByteBuffer put(byte b)
ByteBuffer put(int index, byte b)
ByteBuffer put(byte[] src)
ByteBuffer put(ByteBuffer src)
CharBuffer asCharBuffer()
DoubleBuffer asDoubleBuffer()
FloatBuffer asFloatBuffer()
IntBuffer asIntBuffer()
LongBuffer asLongBuffer()
ShortBuffer asShortBuffer()
Channel
void close()
boolean isOpen()
ReadableByteChannel
一种可以读取字节的通道。在任何给定时间,可读通道上只能进行一个读操作。如果一个线程在通道上发起读操作,那么任何其他试图发起另一个读操作的线程将阻塞,直到第一个操作完成。该接口read方法的返回值一定要注意:
- 阻塞通道:读至文件末尾返回-1,客户端异常关闭抛出异常,客户端正常关闭返回-1
- 非阻塞通道:读至文件末尾返回0,客户端异常关闭抛出异常,客户端正常关闭返回-1
int read(ByteBuffer dst)
WritableByteChannel
一种可以写入字节的通道。在任何给定时间,可写通道上只能进行一个写操作。如果一个线程在通道上发起一个写操作,那么任何其他试图发起另一个写操作的线程将阻塞,直到第一个操作完成。
int write(ByteBuffer src)//返回写入的字节数
InterruptibleChannel
一种可以异步关闭和中断的通道。如果一个线程在可中断通道上的I/O操作中被阻塞,那么另一个线程可能调用被阻塞线程的中断方法或调用通道的关闭方法,这将导致通道被关闭。如果线程的中断状态已经设置,并且它在通道上调用一个阻塞的I/O操作,那么通道将被关闭,它的中断状态将保持设置。
void close()
SelectableChannel
一种可通过选择器进行多路复用的信道。为了与选择器一起使用,这个类的实例必须首先通过register方法注册。此方法返回一个新的SelectionKey对象,该对象表示通道与选择器的注册。一旦向选择器注册,通道将保持注册状态,直到取消注册。通道不能直接注销,必须取消表示其注册的密钥。如果取消一个通道的密匙那么在选择器的下一个选择操作期间将该通道注销。可选择的通道可以处于阻塞模式或非阻塞模式。通道在被选择器注册之前必须处于非阻塞模式,一个通道最多可以向任何特定的选择器注册一次。可选择的通道对于多个并发线程来说是安全的。
SelectableChannel configureBlocking(boolean block)//设置此通道是否是阻塞通道
SelectionKey register(Selector sel, int ops)
SelectionKey register(Selector sel, int ops, Object att)//ops:注册通道感兴趣的操作;att:附加对象
int validOps()//返回此通道支持的操作集
NetworkChannel
一种到网络套接字的通道。
NetworkChannel bind(SocketAddress local)
ScatteringByteChannel
一种能将字节读入缓冲区序列的通道。
long read(ByteBuffer[] dsts)
long read(ByteBuffer[] dsts, int offset, int length)
GatheringByteChannel
一种可以从缓冲区序列中写入字节的通道。
long write(ByteBuffer[] srcs)
long write(ByteBuffer[] srcs, int offset, int length)
SeekableByteChannel
保持当前位置并允许更改该位置的一种字节通道。
long position()
SeekableByteChannel position(long newPosition)
long size()
FileChannel
一种用于读取、写入、映射和操作文件的通道。文件通道对于多个并发线程使用是安全的。在同一时间内只有一个改变文件大小的操作可以执行。
static FileChannel open(Path path, OpenOption... options)
MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)
long transferFrom(ReadableByteChannel src, long position, long count)
long transferTo(long position, long count, WritableByteChannel target)
FileLock lock()
FileLock lock(long position, long size, boolean shared)
FileLock tryLock()
FileLock tryLock(long position, long size, boolean shared)
SocketChannel
用于网络通信的socket通道,一旦socket建立连接,或尝试失败,套接字通道将成为可连接的,并且可以调用finishConnect
来完成连接。当该通道处于阻塞模式,如果连接操作失败,那么调用此方法将导致抛出异常。连接成功则返回true;当该通道处于非阻塞模式,如果连接成功则返回true,连接失败则返回false。这个方法可以在任何时候被调用。如果在此方法的调用过程中调用此通道上的读或写操作,那么该操作将首先阻塞,直到此调用完成。如果此方法抛出异常,则通道将被关闭。
static SocketChannel open()
boolean connect(SocketAddress remote)//使用非阻塞通道时,connect()方法会立即返回,甚至在连接建立之前就会返回。在等待操作系统建立连接时,程序可以做其他的操作。不过,程序在实际使用连接之前,必须调用finishConnect():
boolean finishConnect()
boolean isConnected()//当且仅当此通道的网络套接字打开并连接时为真
boolean isConnectionPending()//当且仅当已在此通道上启动连接操作,但尚未通过调用finishConnect方法完成连接操作时为真
SocketAddress getLocalAddress()
SocketAddress getRemoteAddress()
SocketChannel shutdownInput()
SocketChannel shutdownOutput()
ServerSocketChannel
用于服务器端socket监听通道。
static ServerSocketChannel open()
SocketChannel accept()//在阻塞模式下,accept()方法会一直阻塞等待入站连接。并返回连接到远程客户端的一个SocketChannel对象。在非阻塞模式下,如果没有入站连接,accept()方法会返回null。
Channels
Channels提供了大量的用于通道和流的实用方法。
static ReadableByteChannel newChannel(InputStream in)
static WritableByteChannel newChannel(OutputStream out)
static InputStream newInputStream(AsynchronousByteChannel ch)
static InputStream newInputStream(ReadableByteChannel ch)
static OutputStream newOutputStream(AsynchronousByteChannel ch)
static OutputStream newOutputStream(WritableByteChannel ch)
static Reader newReader(ReadableByteChannel ch, CharsetDecoder dec)
static Writer newWriter(WritableByteChannel ch, CharsetEncoder enc)
同步非阻塞通信示例
server
public class SynchronousNonblockingServer {
private static final Logger logger = Logger.getGlobal();
public static void main(String[] args) throws IOException {
//建立监听通道
try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
//配置非阻塞模式
ssc.configureBlocking(false);
//绑定监听端口
ssc.bind(new InetSocketAddress(8888));
//存储已建立连接的客户端通道
ArrayList<SocketChannel> socketChannels = new ArrayList<>();
//建立一个缓冲区用于暂存数据
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
// logger.info("正在与客户端建立连接");
//接收客户端连接
SocketChannel sc = ssc.accept();
//如果没有连接建立则不添加客户端通道
if (sc != null) {
//将客户端通道也设置为非阻塞
sc.configureBlocking(false);
socketChannels.add(sc);
logger.info("已与" + sc.getRemoteAddress() + "建立连接");
}
for (int i=0;i<socketChannels.size();i++) {
try {
//读取客户端消息
int count = socketChannels.get(i).read(buffer);
//如果客户端没有发送数据则不打印
if (count > 0) {
print(buffer);
}
//如果客户端关闭连接则关闭连接
if (count==-1){
logger.info("客户端"+socketChannels.get(i).getRemoteAddress()+"正常关闭");
socketChannels.get(i).close();
socketChannels.remove(i);
}
} catch (IOException e) {
logger.info("客户端"+socketChannels.get(i).getRemoteAddress()+"异常关闭");
//如果read方法抛出异常,则说明客户端已经异常关闭
socketChannels.get(i).close();
socketChannels.remove(i);
}
}
}
}
}
private static void print(ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
logger.info("客户端发送了:" + new String(bytes));
buffer.clear();
}
}
client
public class Client {
private static final Logger logger = Logger.getGlobal();
public static void main(String[] args){
try (SocketChannel sc = SocketChannel.open()) {
sc.connect(new InetSocketAddress("localhost", 8888));
if (sc.isConnected()) {
if (sc.finishConnect()) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
sc.write(ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)));
}
}
}
} catch (IOException e) {
logger.info("服务器无响应");
}
}
}
该示例较于同步阻塞通信示例而言一个线程可以处理多个客户端,但它也存在问题:当前线程一直在无限循环,而多路复用选择器解决了这个问题。
Selector
多路复用选择器可以选择读写时不阻塞的通道,为了实现选择,要将不同的通道注册到多路复用选择器中。每个通道分配并对应一个SelectionKey。一个多路复用选择器维护了三个SelectionKey键集:
- key:包含所有由
register
方法注册通道的SelectionKey。该集合由keys
方法返回。 - selected-key:包含所有由
select
方法选择的SelectionKey,它只会主动增加不会主动减少。该集合由selectedKeys
返回。 - cancelled-key :所有由
cancel
方法取消但其通道尚未注销的键集。取消一个键将导致它的通道在下一次选择操作期间注销,此时该键将从选择器的key-set键集和cancelled-key键集中删除。
多路复用选择器本身对于多个并发线程使用是安全的,但是它的键集并不是线程安全的。
static Selector open()
Set<SelectionKey> keys()
Set<SelectionKey> selectedKeys()
int select()//在返回前会等待,直到至少有一个注册的通道准备好可以进行处理。
int select(long timeout)//在返回0前只等待不超过timeout毫秒。如果没有通道就绪程序就不做任何操作
int selectNow()//selectNow()方法会完成非阻塞选择。如果当前没有准备好要处理的连接,它会立即返
Selector wakeup()//唤醒当前或未来阻塞在select方法的selector
其中select
方法的执行步骤如下:
- cancelled-key键集中的每个键从cancelled-key键集和key键集中移除,并将它们对应的通道注销。
- 向底层操作系统查询更新,以确定在选择操作开始时,每个剩余通道是否准备好执行由其兴趣集标识的任何操作。如果此步骤开始时的键集中的所有键都有空的兴趣集,则不进行任何操作,对于准备至少进行一个这样操作的通道,将执行以下两个操作中的一个:
-
- 如果通道的SelectionKey不在selected-key键集中,则将其添加到该键集中,并修改其就绪集,以前记录在就绪集中的任何信息将被丢弃。
-
- 如果通道的SelectionKey存在selected-key键集中,那么就修改它的就绪集,以前记录在就绪集中的任何就绪信息被保留。
- 如果在步骤二进行时向cancelled-key键集添加了任何键,那么它们将按照步骤1进行处理。
SelectionKey
一个SelectionKey包含两个表示为整数值的操作集。操作集的每一位表示键的通道支持的可选择操作的类别。
- 兴趣集:兴趣集确定在下一次调用
select
方法时,将测试哪些操作类别以准备就绪。 - 就绪集:就绪集指示其通道已为某些操作类别做好准备,这是一种提示,但不保证线程可以在不导致线程阻塞的情况下执行此类类别中的操作。
//这些都是位标志整型常量。因此,如果一个通道需要在同一个选择器中关注多个操作(例如读和写一个socket)),只要在注册时利用位“或”操作符(|)组合这些常量就可以了。
static int OP_ACCEPT
static int OP_CONNECT //连接一旦建立,客户端通道触发该事件
static int OP_READ
static int OP_WRITE
int interestOps()
SelectionKey interestOps(int ops)//修改其兴趣集
boolean isAcceptable()
boolean isConnectable()
boolean isReadable()
boolean isWritable()
SelectableChannel channel()//获取selectionKey对应的通道
void cancel()//撤销注册
Object attach(Object ob)//添加对象附件
Object attachment()//获取对象附件
多路复用同步非阻塞通信示例
server
public class MultiplexingSynchronousNonblockingServer {
private static final Logger logger = Logger.getGlobal();
public static void main(String[] args) throws IOException {
//建立选择器
try (Selector selector=Selector.open()) {
//建立监听通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//配置非阻塞模式
ssc.configureBlocking(false);
//绑定监听端口
ssc.bind(new InetSocketAddress(8888));
//注册通道,并指定它的兴趣集
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//功能见上文
selector.select();
//获取selected-key集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//处理完一个selectionKey就要将其从selected-key集合中删除
iterator.remove();
//判断客户端通道的兴趣集
if (selectionKey.isAcceptable()){
//处理客户端连接,并将其注册
SocketChannel sc = ((ServerSocketChannel) selectionKey.channel()).accept();
logger.info("客户端"+sc.getRemoteAddress()+"已建立了连接");
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(15));
}else if (selectionKey.isReadable()){
//处理客户端写事件
try {
int count = ((SocketChannel) selectionKey.channel()).read((ByteBuffer) selectionKey.attachment());
if (count>0){
print((ByteBuffer) selectionKey.attachment());
}
//正常断开处理
if (count==-1){
logger.info("客户端"+((SocketChannel) selectionKey.channel()).getRemoteAddress()+"已正常断开连接");
selectionKey.cancel();
}
} catch (IOException e) {
//异常断开处理
logger.info("客户端"+((SocketChannel) selectionKey.channel()).getRemoteAddress()+"已异常断开连接");
selectionKey.cancel();
}
}
}
}
}
}
private static void print(ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
logger.info("客户端发送了:" + new String(bytes));
buffer.clear();
}
}
client
public class Client {
private static final Logger logger = Logger.getGlobal();
public static void main(String[] args){
try (SocketChannel sc = SocketChannel.open()) {
sc.connect(new InetSocketAddress("localhost", 8888));
if (sc.isConnected()) {
if (sc.finishConnect()) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
sc.write(ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)));
}
}
}
} catch (IOException e) {
logger.info("服务器无响应");
}
}
}
该示例同样实现了一个线程处理多个连接的需求,并且在不必要时还会阻塞当前线程,在此基础上,还可以通过多线程实现异步资源读写操作,从而提升CPU利用率。
多线程多路复用同步非阻塞通信示例
其中boss只负责资源连接步骤,而资源传输步骤由worker负责。
server
public class Boss {
private static final Logger logger = Logger.getGlobal();
private static class Worker implements Runnable{
private Selector worker;
private final String name;
private final AtomicBoolean isStart=new AtomicBoolean(false);
private final ConcurrentLinkedQueue<Runnable> queue=new ConcurrentLinkedQueue<>();
public Worker(String name) {
this.name = name;
}
public void start(SocketChannel sc) throws IOException {
if (!isStart.getAndSet(true)){
Thread thread = new Thread(this, name);
worker=Selector.open();
thread.start();
}
queue.add(()->{
try {
sc.register(worker, SelectionKey.OP_READ,ByteBuffer.allocate(15));
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
worker.wakeup();
}
@Override
public void run() {
while (true){
try {
worker.select();
if (queue.peek()!=null){
queue.poll().run();
}
Set<SelectionKey> selectionKeys = worker.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isReadable()){
try {
SocketChannel channel = (SocketChannel)selectionKey.channel();
int count = channel.read((ByteBuffer) selectionKey.attachment());
if (count>0){
print((ByteBuffer) selectionKey.attachment());
}
if (count==-1){
logger.info("客户端"+((SocketChannel) selectionKey.channel()).getRemoteAddress()+"已正常断开连接");
selectionKey.cancel();
}
} catch (IOException e) {
logger.info("客户端"+((SocketChannel) selectionKey.channel()).getRemoteAddress()+"已异常断开连接");
selectionKey.cancel();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("boss");
try (Selector boss=Selector.open()) {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8888));
ssc.register(boss, SelectionKey.OP_ACCEPT);
Worker[] workers = new Worker[Runtime.getRuntime().availableProcessors()];
for (int i = 0; i < workers.length; i++) {
workers[i]=new Worker("worker"+i);
}
AtomicInteger index= new AtomicInteger();
while (true) {
boss.select();
Set<SelectionKey> selectionKeys = boss.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()){
SocketChannel sc = ((ServerSocketChannel) selectionKey.channel()).accept();
logger.info("客户端"+sc.getRemoteAddress()+"已建立了连接");
sc.configureBlocking(false);
workers[index.getAndIncrement()% workers.length].start(sc);
}
}
}
}
}
private static void print(ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
logger.info("客户端发送了:" + new String(bytes));
buffer.clear();
}
}
client
public class Client {
private static final Logger logger = Logger.getGlobal();
public static void main(String[] args){
try (SocketChannel sc = SocketChannel.open()) {
sc.connect(new InetSocketAddress("localhost", 8888));
if (sc.isConnected()) {
if (sc.finishConnect()) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
sc.write(ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)));
}
}
}
} catch (IOException e) {
logger.info("服务器无响应");
}
}
}
IO操作的进一步优化
如果使用allocateDirect
方法,那么缓冲区将分配在直接内存中,那么就减少了一次复制操作:
如果使用transferTo
和transferFrom
方法,那么复制操作将直接在内核态内完成,因此就减少了一次操作系统状态的切换(底层使用了Linux2.1提供的sendFile
方法)。
在Linux2.4以后,再使用transferTo
和transferFrom
方法时,IO过程将变成如下所示,此时操作系统状态的切换只有一次,复制过程只有两次,实现了java内存的零拷贝。
Netty架构设计及网络通信模型
Netty是一个异步的、基于事件驱动的网络应用框架,其中异步并不是指异步IO中的异步,而是指Netty使用了多线程将方法调用和结果处理相分离。Netty有五大核心组件:
- BootStarp:引导构建应用程序。
- Channel:数据传输通道。
- ByteBuf:暂存数据的缓冲区。
- ChannelHandler:为通道提供处理逻辑的处理器。
- EventLoop:为通道提供执行处理器的线程,还可以进行任务处理。
- Futture:用于处理异步操作的结果。
ByteBuf
和NIO原生的Bytebuffer
相比,它有如下优点:
- 可以自定义的缓冲区类型
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长
- 读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
ReferenceCounted
ReferenceCounted抽象一类需要显式重分配的引用计数对象。当实例化一个新的ReferenceCounted时,它从引用计数1开始。retain方法增加引用计数,release方法减少引用计数。如果引用计数减少到0,对象将被显式释放,而访问释放的对象通常会导致访问冲突。一般来说,是由最后访问(引用计数)对象的那一方来负责将它释放。
int refCnt()//返回此对象的引用计数。
boolean release()//将引用计数减少1,并在引用计数达到0时释放该对象。
ReferenceCounted retain()//增加计数
ByteBufHolder
content() 返回由这个 ByteBufHolder 所持有的 ByteBuf
copy() 返回这个 ByteBufHolder 的一个深拷贝,包括一个其所包含的 ByteBuf 的非共享拷贝
duplicate() 返回这个 ByteBufHolder 的一个浅拷贝,包括一个其所包含的 ByteBuf 的共享拷贝
类型
类型 | 说明 |
---|---|
堆缓冲区 | 在java堆空间分配的缓冲区。它能在没有使用池化的情况下提供快速的分配和释放。 |
直接缓冲区 | 在直接内存中分配的缓冲区,直接缓冲区避免了在每次调用本地 I/O 操作之前或之后将缓冲区的内容复制到一个中间缓冲区或者从中间缓冲区把内容复制到缓冲区的问题,但它们的分配和释放都较为昂贵。 |
复合缓冲区 | 复合缓冲区是一个缓冲区视图,可以根据需求向该视图内添加不同类型的缓冲区。 |
内部结构
ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从 ByteBuf 读取时,它的 readerIndex 将会被递增已经被读取的字节数。同样地,当你写入 ByteBuf 时,它的writerIndex 也会被递增。
ByteBuf 可以向一个数组一样使用索引进行访问,并且这中访问方式不会改变读写索引的位置:
byte getByte(int index) //返回给定索引处的字节
ByteBuf getBytes(int index, byte[] dst)
ByteBuf getBytes(int index, ByteBuf dst)
ByteBuf setByte(int index, int value) //设定给定索引处的字节值
ByteBuf setBytes(int index, byte[] src)
ByteBuf setBytes(int index, ByteBuf src)
也可以使用读写索引访问ByteBuf :
byte readByte() //返回当前 readerIndex 处的字节,并将 readerIndex 增加 1
ByteBuf readBytes(byte[] dst)
ByteBuf readBytes(ByteBuf dst)
ByteBuf writeByte(int value)//在当前 writerIndex 处写入一个字节值,并将 writerIndex 增加 1
ByteBuf writeBytes(byte[] src)
ByteBuf writeBytes(ByteBuf src)
此后ByteBuf 会被划分为三个区域:
同样的也可以手动管理这些索引:
ByteBuf discardReadBytes()//丢弃可丢弃字节
ByteBuf markReaderIndex()
ByteBuf markWriterIndex()
ByteBuf resetReaderIndex()
ByteBuf resetWriterIndex()
ByteBuf clear()//将该缓冲区的readerIndex和writerIndex设置为0
其他操作
int forEachByte(ByteProcessor processor)//内容检索
ByteBuf duplicate()//回一个共享该缓冲区的整个区域的缓冲区。
ByteBuf readSlice(int length)//返回该缓冲区子区域从当前readerIndex处开始的一个新切片,并将readerIndex增加新切片的大小
ByteBuf slice()//返回可读取与的切片
ByteBuf slice(int index, int length)//返回指定区域切片
boolean isReadable() //如果至少有一个字节可供读取,则返回 true
boolean isWritable() //如果至少有一个字节可被写入,则返回 true
int readableBytes() //返回可被读取的字节数
int writableBytes() 返回可被写入的字节数
int capacity() //返回 ByteBuf 可容纳的字节数。在此之后,它会尝试再次扩展直
到达到 maxCapacity()
int maxCapacity() //返回 ByteBuf 可以容纳的最大字节数
bboolean hasArray() //如果 ByteBuf 由一个字节数组支撑,则返回 true
byte[] array() //如果 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 |
Unpooled
可能某些情况下,不能获取一个到 ByteBufAllocator 的引用。对于这种情况,可以使用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 |
Channel
Channel用于抽象与资源连接的通道。
SocketAddress remoteAddress()
SocketAddress localAddress()
EventLoop eventLoop()//返回分配给 Channel 的 EventLoop
ChannelPipeline pipeline() //返回分配给 Channel 的 ChannelPipeline
ChannelConfig config()//返回通道的配置文件
boolean isOpen()
boolean isActive()//该通道是否是活跃的
boolean isRegistered()
boolean isRegistered()
ByteBufAllocator alloc()//返回一个ByteBufAllocator对象
ChannelFuture closeFuture()
ChannelFuture write(Object msg)//将数据写到远程节点。这个数据将被传递给 ChannelPipeline,并且排队直到它被冲刷
Channel flush()//将之前已写的数据冲刷到底层传输,如一个 Socket
ChannelFuture writeAndFlush(Object msg)//等同于调用 write()并接着调用 flush()
ChannelConfig
每一个Channel被创建时都会分配一个专属的 ChannelConfig,ChannelConfig 包含了该 Channel 的所有配置设置,并且支持热更新。
ChannelHandler
ChannelHandler用于为不同的事件提供处理逻辑,由于事件分为入站时间和出站事件,因此ChannelHandler也分为ChannelInboundHandler和ChannelOutboundHandler。
void handlerAdded(ChannelHandlerContext ctx)//当把 ChannelHandler 添加到 ChannelPipeline 中时被调用
void handlerRemoved(ChannelHandlerContext ctx)//当从 ChannelPipeline 中移除 ChannelHandler 时被调用
ChannelInboundHandler
ChannelInboundHandler用于处理入站数据以及各种状态变化,这些方法将会在数据被接收时或者与其对应的 Channel 状态发生改变时被调用。
void channelRegistered(ChannelHandlerContext ctx)//当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用
void channelUnregistered(ChannelHandlerContext ctx)// 当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用
void channelActive(ChannelHandlerContext ctx)//当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪
void channelInactive(ChannelHandlerContext ctx)//当 Channel 离开活动状态并且不再连接它的远程节点时被调用
void channelRead(ChannelHandlerContext ctx, Object msg)//当从 Channel 读取数据时被调用
void channelReadComplete(ChannelHandlerContext ctx)//当Channel上的一个读操作完成时被调用
void channelWritabilityChanged(ChannelHandlerContext ctx)//当 Channel 的可写状态发生改变时被调用。用户可以确保写操作不会完成得太快(以避免发生 OutOfMemoryError)或者可以在 Channel 变为再次可写时恢复写入。可以通过调用 Channel 的 isWritable()方法来检测Channel 的可写性。与可写性相关的阈值可以通过 Channel.config().setWriteHighWaterMark()和 Channel.config().setWriteLowWaterMark()方法来设置
void userEventTriggered(ChannelHandlerContext ctx, Object evt)// 当 ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用,因为一个 POJO 被传经了 ChannelPipeline
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)//当有异常抛出是被调用
ChannelOutboundHandler
ChannelOutboundHandler用于处理出站数据并且允许拦截所有的操作。它的方法将被 Channel、ChannelPipeline 以及 ChannelHandlerContext 调用。
void bind(ChannelHandlerContext,SocketAddress,ChannelPromise)//当请求将 Channel 绑定到本地地址时被调用
void connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise)//当请求将 Channel 连接到远程节点时被调用
void disconnect(ChannelHandlerContext,ChannelPromise)//当请求将 Channel 从远程节点断开时被调用
void close(ChannelHandlerContext,ChannelPromise)//当请求关闭 Channel 时被调用
void deregister(ChannelHandlerContext,ChannelPromise)//当请求将 Channel 从它的 EventLoop 注销时被调用
void read(ChannelHandlerContext)//当请求从 Channel 读取更多的数据时被调用
void flush(ChannelHandlerContext) //当请求通过 Channel 将入队数据冲刷到远程节点时被调用
void write(ChannelHandlerContext,Object,ChannelPromise)//当请求通过 Channel 将数据写到远程节点时被调用
ChannelHandlerAdapter
有时我们并不会对所有事件都感兴趣,此时可以考虑使用抽象基类 ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter。通过调用 ChannelHandlerContext 上的对应方法,每个都提供了简单地将事件传递给下一个ChannelHandler的方法的实现。随后,可以通过重写我们感兴趣的那些方法来扩展这些类。
boolean isSharable()//如果其对应的实现被标注为 Sharable,那么这个方法将返回 true,表示它可以被添加到多个 ChannelPipeline中
SimpleChannelInboundHandler
当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它将负责显式地释放与池化的 ByteBuf 实例相关的内存。而SimpleChannelInboundHandler会在消息被 channelRead0()方法消费之后自动释放消息。
void channelRead0(ChannelHandlerContext ctx, I msg)
ChannelInitializer
ChannelInitializer的initChannel方法可以将多个 ChannelHandler 添加到一个 ChannelPipeline。一旦 Channel 被注册到 EventLoop ,就会调用initChannel方法。在该方法返回之后,ChannelInitializer 的实例将会从 ChannelPipeline 中移除它自己。
void initChannel(C ch)
ChannelPipeline
每一个Channel被创建时都会分配一个专属的 ChannelPipeline,ChannelPipeline是一个ChannelHandler链的容器。
其实在调用add方法将入站处理器和出站处理器添加到ChannelPipeline时,入站处理器和出站处理器是混合排列的,那么第一个被入站事件看到的处理器将是1,而第一个被出站事件看到的处理器将是3。
添加API
通常 ChannelPipeline 中的每一个 ChannelHandler 都是通过它的 EventLoop来处
理传递给它的事件的。但有时可能需要与那些使用阻塞 API 的遗留代码进行交互。对于这种情况,ChannelPipeline 有一些接受一个 EventExecutorGroup 的 add方法。如果一个事件被传递给一个自定义的 EventExecutorGroup ,它将被包含在这个 EventExecutorGroup 中的某个 EventExecutor 所处理,从而被从该Channel 本身的 EventLoop 中移除。
ChannelPipeline addFirst(ChannelHandler... handlers)
ChannelPipeline addFirst(String name, ChannelHandler handler)
ChannelPipeline addFirst(EventExecutorGroup group, ChannelHandler... handlers)
ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler)
ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler)
ChannelPipeline addBefore(EventExecutorGroup group, String baseName, String name, ChannelHandler handler)
ChannelPipeline addAfter(String baseName, String name, ChannelHandler handler)
ChannelPipeline addAfter(EventExecutorGroup group, String baseName, String name, ChannelHandler handler)
ChannelPipeline addLast(ChannelHandler... handlers)
ChannelPipeline addLast(String name, ChannelHandler handler)
ChannelPipeline addLast(EventExecutorGroup group, ChannelHandler... handlers)
ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler)
修改、删除API
ChannelPipeline remove(ChannelHandler handler)
<T extends ChannelHandler> T remove(Class<T> handlerType)
ChannelHandler remove(String name)
ChannelHandler removeFirst()
ChannelHandler removeLast()
ChannelPipeline replace(ChannelHandler oldHandler, String newName, ChannelHandler newHandler)
<T extends ChannelHandler> T replace(Class<T> oldHandlerType, String newName, ChannelHandler newHandler)
ChannelHandler replace(String oldName, String newName, ChannelHandler newHandler)
访问API
ChannelHandler get(String name)
<T extends ChannelHandler> T get(Class<T> handlerType)
ChannelHandlerContext context(String name)
ChannelHandlerContext context(Class<? extends ChannelHandler> handlerType)
ChannelHandlerContext context(ChannelHandler handler)
ChannelHandlerContext
当ChannelHandler被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,代表ChannelHandler和ChannelPipeline之间的绑定。它的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。ChannelHandlerContext 中的一些方法也存在于 Channel 和 ChannelPipeline ,但是有一点重要的不同。如果调用 Channel 或者 ChannelPipeline 上的这些方法,它们将沿着整个 ChannelPipeline 进行传播。而调用位于 ChannelHandlerContext上的相同方法,则将从当前所关联的ChannelHandler 开始,并且只会传播给位于该ChannelPipeline 中的下一个能够处理该事件的 ChannelHandler,如果是入站事件,那么就会从当前位置向下寻找,如果是出站,那么就会从当前位置向上寻找。
异步结果
Future
Future代表异步操作的结果。
Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener)//将指定的侦听器添加到此future。
Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners)
Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener)
Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners)
boolean cancel(boolean mayInterruptIfRunning)//取消操作,如果取消成功则抛出异常
Throwable cause()//如果操作失败则返回异常原因
V getNow()//不阻塞的返回结果
boolean isCancellable()//如果当前操作可以通过Cannel方法取消时返回true
boolean isSuccess()//当且仅当I/O操作成功完成时返回true。
Future<V> await()//等待异步操作完成,如果操作失败不会抛出异常
Future<V> awaitUninterruptibly()//等待这个异步操作完成,不相应中断
Future<V> sync()//等待异步操作完成。如果任务失败会抛出异常
Future<V> syncUninterruptibly()//同步获取结果并不相应中断
Promise
Promise与Future的区别在于Promise与任务的执行结果解耦,可以设置在不同情况下要执行的操作。
Promise<V> setFailure(Throwable cause)
Promise<V> setSuccess(V result)
boolean setUncancellable()
boolean tryFailure(Throwable cause)
boolean trySuccess(V result)
GenericFutureListener
EventLoop
Netty通过触发事件将Selector从应用程序中抽象出来,事件根据它们与入站或出站数据流的相关性进行分类,由入站数据或者相关的状态更改而触发的事件称为入站事件,入站事件包括:
- 服务器与客户端连接激活或失活
- 数据读取
- 用户事件
- 错误事件
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
- 打开或者关闭到远程节点的连接
- 将数据写到或者冲刷到客户端
在底层,Netty将会为每个 Channel 分配一个 EventLoop,这个EventLoop用以处理该Channel的所有事件并且在该EventLoop的整个生命周期内都不会改变。EventLoop本质是一个Selector和Thread,因此它的工作包括:
- 在Selector注册Channel感兴趣的事件。
- 在Thread内将事件派发给相应的Handler。
EventLoopGroup是一组EventLoop,通过EventLoopGroup中的register方法可以将Channel绑定到其中一个EventLoop。
EventExecutorGroup
EventExecutorGroup负责通过它的next方法提供EventExecutor。除此之外,它还负责处理它们的生命周期,并允许在全局范围内关闭它们。
boolean isShuttingDown()//当且仅当由该事件执行组管理的所有事件执行组被优雅地关闭或关闭时,返回true。
EventExecutor next()//返回一个EventExecutor
Future<?> shutdownGracefully()//优雅关闭
Future<?> terminationFuture()//当由该EventExecutorGroup管理的所有eventexecutor都被终止时通知该Future。
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
EventExecutor
EventExecutor是一个特殊的EventExecutorGroup,它带有一些方便的方法来查看线程是否在事件循环中执行。除此之外,它还扩展了EventExecutorGroup,允许使用通用的方式来访问方法。
<V> Future<V> newFailedFuture(Throwable cause)
<V> ProgressivePromise<V> newProgressivePromise()
<V> Promise<V> newPromise()
<V> Future<V> newSucceededFuture(V result)
EventExecutorGroup parent()
<T> Future<T> submit(Callable<T> task)
Future<?> submit(Runnable task)
<T> Future<T> submit(Runnable task, T result)
EventLoopGroup
特殊的EventExecutorGroup,它允许注册在事件循环期间被处理的通道。
ChannelFuture register(Channel channel)
ChannelFuture register(ChannelPromise promise)
NioEventLoopGroup
用于基于NIO选择器通道的EventLoop。
NioEventLoopGroup()
NioEventLoopGroup(int nThreads)
NioEventLoopGroup(int nThreads, Executor executor)
protected EventLoop newChild(Executor executor, Object... args)//创建一个新的EventExecutor
void rebuildSelectors()//用新创建的选择器替换当前子事件循环的选择器,以解决臭名昭著的epoll 100% CPU bug。
void setIoRatio(int ioRatio)//设置子事件循环中I/O所需时间的百分比。
DefaultEventLoopGroup
用于本地传输的EventLoopGroup。
DefaultEventLoopGroup()
DefaultEventLoopGroup(int nThreads)
DefaultEventLoopGroup(int nThreads, Executor executor)
Bootstrap
BootStrap为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口(服务器),或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程(客户端)。引导一个客户端只需要一个 EventLoopGroup,但是一个
ServerBootstrap 则需要两个,因为服务器需要两组不同的 Channel。第一组将只包含一个 ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接的 Channel。
与 ServerChannel 相关联的 EventLoopGroup 将分配一个负责为传入连接请求创建
Channel 的 EventLoop。一旦连接被接受,第二个 EventLoopGroup 就会给它的 Channel分配一个 EventLoop。
AbstractBootstrap
B group(EventLoopGroup group)//设置用于处理 Channel 所有事件的 EventLoopGroup
B channel(Class<? extends C> channelClass)//channel()方法指定了Channel的现类。如果该实现类没提供默认的构造函数 ① ,可以通过调用channelFactory()方法来指定一个工厂类,它将会被bind()方法调用
B channelFactory(ChannelFactory<? extends C> channelFactory)
B handler(ChannelHandler handler)//设置将被添加到 ChannelPipeline 以接收事件通知的ChannelHandler
<T> B attr(AttributeKey<T> key, T value)
<T> B option(ChannelOption<T> option, T value)//设置 ChannelOption,其将被应用到每个新创建的Channel 的 ChannelConfig。这些选项将会通过bind()或者 connect()方法设置到 Channel,不管哪个先被调用。这个方法在 Channel 已经被创建后再调用将不会有任何的效果。支持的 ChannelOption 取决于使用的 Channel 类型。
AbstractBootstrapConfig<B,C> config()
Bootstrap
Bootstrap remoteAddress(SocketAddress remoteAddress)
ChannelFuture connect()
ServerBootstrap
ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
ServerBootstrap childHandler(ChannelHandler childHandler)
<T> ServerBootstrap childOption(ChannelOption<T> childOption, T value)
<T> ServerBootstrap childAttr(AttributeKey<T> childKey, T value)
B localAddress(SocketAddress localAddress)//指定 Channel 应该绑定到的本地地址。
ChannelFuture bind()
Attribute
有些数据可能会在Netty的生命周期外使用,此时可以使用AttributeMap和 AttributeKey安全地将任何类型的数据项与客户端和服务器 Channel相关联。
ChannelOption
在每个 Channel 创建时都手动配置它可能会变得相当乏味。幸运的是,你不必这样做。相反,你可以使用 option()方法来将 ChannelOption 应用到引导。你所提供的值将会被自动应用到引导所创建的所有 Channel。可用的 ChannelOption 包括了底层连接的详细信息,如keep-alive 或者超时属性以及缓冲区设置。
编码器和解码器
当发送或接收一个消息的时候,就将会发生一次数据转换。入站消息会被解
码,出站消息被编码,Netty提供了大量的encoder和decoder,它们都实现了ChannelInboundHandler和ChannelOutboundHandler。
解码器
ByteToMessageDecoder
将字节解码为消息,由于不知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。
void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)//decode()方法被调用时将会传入一个包含了传入数据的 ByteBuf,以及一个用来添加解码消息的 List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该 List,或者该 ByteBuf 中没有更多可读取的字节时为止。然后,如果该 List 不为空,那么它的内容将会被传递给ChannelPipeline 中的下一个 ChannelInboundHandler
void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)//当Channel的状态变为非活动时,这个方法将会被调用一次。
MessageToMessageDecoder
在两个消息格式之间进行转换。
void decode(ChannelHandlerContext ctx, I msg, List<Object> out)//对于每个需要被解码为另一种格式的入站消息来说,该方法都将会被调用。解码消息随后会被传递给 ChannelPipeline中的下一个 ChannelInboundHandler
编码器
MessageToByteEncoder
来将字节转换为消息。
void encode(ChannelHandlerContext ctx, I msg, ByteBuf out)//被调用时将会传入要被该类编码为 ByteBuf 的(类型为 I 的)出站消息。该 ByteBuf 随后将会被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler
MessageToMessageEncoder
在两个消息格式之间进行转换。
void encode(ChannelHandlerContext ctx, I msg, List<Object> out)//每个通过 write()方法写入的消息都将会被传递给 encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将会被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler
编解码器
ByteToMessageCodec
void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
void encode(ChannelHandlerContext ctx, I msg, ByteBuf out)
MessageToMessageCodec
protected abstract void decode(ChannelHandlerContext ctx, INBOUND_IN msg, List<Object> out)
protected abstract void encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, List<Object> out)
CombinedChannelDuplexHandler
该类充当了 ChannelInboundHandler 和 ChannelOutboundHandler的容器。通过提供分别继承了解码器类和编码器类的类型,我们可以实现一个编解码器,而又不必直接扩展抽象的编解码器类。
消息边界
在网络通信中来自客户端的消息长度是不去确定的,因此在服务器预先创建的缓存长度可能与消息长度不匹配,此时就会出现消息边界问题。
问题分类
服务器在接收消息时会出现以下几种情况:
解决方案
- 方案一:服务端与客户端设定一个消息的最大长度,但会造成内存浪费问题FixedLengthFrameDecoder。
- 方案二:每个消息之间使用分隔符进行分割,服务器根据分隔符不断的调整缓存大小,但服务器需要寻找分隔符,因此此种方式效率较低LineBasedFrameDecoder和DelimiterBasedFrameDecoder。
- 方案三:每个消息分为两部分,第一部分存储第二部分消息的长度,此种方式比较常用lengthFieldBasedFrameDecoder。
常用的编解码器
HttpServerCodec
自定义协议的设计与解析
自定义协议的要素如下:
- 魔数:用来在第一时间判断是否是无效的数据包。
- 版本号:可以支持协议的支持。
- 序列化算法:消息正文到底采用哪种序列化反序列化方式。
- 指令类型:需要进行操作的类型。
- 请求序号:为了双工通信,提供异步能力。
- 对齐填充
- 正文长度
- 消息正文
空闲检测
IdleStateHandler处理器用于检测在指定时间间隔内通道是否还活跃,如果不活跃则触发相应的事件:
- IdleState.READER_IDLE
- IdleState.WRITER_IDLE
- IdleState.ALL_IDLE
触发指定事件后可以自定义处理器进行响应:
@ChannelHandler.Sharable
public class FreeTestHandler extends ChannelDuplexHandler {
//响应特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent){
IdleStateEvent idleStateEvent=(IdleStateEvent) evt;
if (idleStateEvent.state()== IdleState.READER_IDLE){
}else if (idleStateEvent.state()==IdleState.WRITER_IDLE){
}else {
}
}
}
}
客户端可以发送心跳消息来证明自己还活着。