NIO与Netty学习指南

阻塞、非阻塞、同步和异步

当用户线程调用一次读写方法时,操作系统会由用户态切换至内核态,由内核态完成真正的数据读写,读写完成后操作系统再由内核态切换为用户态。数据读写过程又分为两个阶段,分别为等待数据阶段和复制数据阶段。如果在第一阶段用户线程在调用读写方法后立即返回,并通过轮询、多路复用等方式监听是否可以进入第二阶段,那么此过程就是非阻塞的,否则就是阻塞的。进入第二阶段后,如果调用读写方法的线程一直等待数据复制完成后返回,那么此过程就是同步的;如果调用读写方法的线程没有等待数据复制完成就直接返回,而是将等待过程交由另一个用户线程,待复制完成后通知调用读写方法的线程来获取数据,那么此过程就是异步的。
在这里插入图片描述
在此过程中一共进行了三次状态的切换和四次数据的复制。

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方法,那么缓冲区将分配在直接内存中,那么就减少了一次复制操作:
在这里插入图片描述

如果使用transferTotransferFrom方法,那么复制操作将直接在内核态内完成,因此就减少了一次操作系统状态的切换(底层使用了Linux2.1提供的sendFile方法)。
在这里插入图片描述
在Linux2.4以后,再使用transferTotransferFrom方法时,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 {
                
            }
        }
    }
}

客户端可以发送心跳消息来证明自己还活着。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亻乍屯页女子白勺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值