Netty(二)

此笔记是基于B站-黑马老师的视频,强烈推荐此视频

NIO基础

1. 三大组件

  1. Channel、Buffer、Selector
  2. Channel是一个读写数据的双向通道。而传统的BIO中有InputStream和OutputStream,只能读或写数据
  3. Buffer是内存层面的一个缓冲区,Channel将数据读取或写入Buffer中。
  4. 常见的Channel:
  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel
  1. 多线程版本的服务器:每个请求建立一个线程连接,线程太多可能导致内存溢出,线程上下文切换需要时间,只适合少量连接数的场景
  2. 线程池班的服务器:在阻塞模式下,一个线程仅能处理一个Socket连接。如果一个连接没有事件发生,那么该线程只能等待,不能为其他连接服务。仅适合短链接的场景。
  3. Selector版本的设计:selector会配合一个线程来管理多个channel,获取这些channel上发生的事件,这些channel工作在非阻塞模式下,selector的select()阻塞到直到channel发生了读写事件,select()就会返回这些事件交给thread进行处理。

在这里插入图片描述

2. ByteBuffer的使用

/**
 * 使用ByteBuffer读取字节文件
 */
@Test
public void testByteBuffer() {
    // 读取一个文件
    try (FileChannel channel = new FileInputStream("test.txt").getChannel()) {
        while (true) {
            // 创建一个缓冲区,接收文件
            ByteBuffer allocate = ByteBuffer.allocate(10);
            // 将文件读取到缓冲区
            int i = channel.read(allocate);
            // 读取到-1,文件读取完毕
            if(i == -1) {
                break;
            }

            // 切换到读模式
            allocate.flip();
            // 循环判断还有没有数据
            while(allocate.hasRemaining()) {
                System.out.println((char) allocate.get());
            }

            // 切换到写模式
            allocate.clear();
        }
    } catch (IOException e) {
    }
}
  1. ByteBuffer的正确使用:
    ① 向ByteBuffer写入数据,channel.read(buffer)
    ② 使用flip()切换到读模式
    ③ 从ByteBuffer中读取数据,buffer.get()
    ④ 调用clear()或者compact()切换到写模式

  2. rewind()重头开始读取。

  3. buffer.mark()标记当前position的位置,和reset()一起使用,回到mark的位置进行读取。

@Test
public void ByteBufferTest1() {
    ByteBuffer buffer = ByteBuffer.allocate(10);
    buffer.put(new byte[]{'a', 'b', 'c', 'd'});

    // 切换到读模式
    buffer.flip();

//        System.out.println((char) buffer.get());
//        // 重置position到开头,重新进行读取
//        buffer.rewind();
//        ByteBufferUtils.debugAll(buffer);
//        System.out.println((char) buffer.get());

    // 使用mark标记,和reset方法
    System.out.println((char) buffer.get());
    System.out.println((char) buffer.get());
    buffer.mark();
    System.out.println((char) buffer.get());
    System.out.println((char) buffer.get());
    // position跳转到mark标记时的位置
    buffer.reset();
    System.out.println((char) buffer.get());
    System.out.println((char) buffer.get());
}

3. ByteBuffer的分配

@Test
public void ByteBuffer3() {
    ByteBuffer buffer = ByteBuffer.allocate(16);
    ByteBuffer directBuffer = ByteBuffer.allocateDirect(16);
    /**
     * class java.nio.HeapByteBuffer
     * 分配的堆内存,读写效率低,收到GC的影响
     */
    System.out.println(buffer.getClass());
    /**
     * class java.nio.DirectByteBuffer
     * 分配的直接内存,读写效率高(少一次拷贝),不受GC影响
     */
    System.out.println(directBuffer.getClass());
}

4. ByteBuffer与字符串之间的转化

@Test
public void testByteBufferToString() {
    // 1. 直接通过字符串的字节数组传递
    ByteBuffer buffer1 = ByteBuffer.allocate(16);
    buffer1.put("hello".getBytes());
    ByteBufferUtils.debugAll(buffer1);
    buffer1.put("中国".getBytes());
    ByteBufferUtils.debugAll(buffer1);

    // 要切换为写模式
    buffer1.flip();
    // 将byteBuffer解码成字符串
    String str = StandardCharsets.UTF_8.decode(buffer1).toString();
    System.out.println(str);

    // 2. 使用 StandardCharsets.UTF_8.encode, 会自动切换到读模式
    ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello中国");
    ByteBufferUtils.debugAll(buffer2);

    String str2 = StandardCharsets.UTF_8.decode(buffer2).toString();
    System.out.println(str2);
}

5. ByteBuffer处理粘包、半包

/**
 * 处理粘包、半包
 */
@Test
public void testTcp() {
    ByteBuffer buffer = ByteBuffer.allocate(60);
    buffer.put("Hello,World\nI'm Hanmeimei\n中国".getBytes());
    split(buffer);
    buffer.put("你好\n".getBytes());
    split(buffer);
}

/**
 * 处理粘包、半包
 * @param buffer
 */
public void split(ByteBuffer buffer) {
    // 切换到读模式
    buffer.flip();
    // 遍历读取到的数据
    for(int i = 0; i < buffer.limit(); i++) {
        // get(index),不会移动position位置
        if(buffer.get(i) == '\n') {
            // 计算当前读取到的一条信息的长度
            int length = i + 1 - buffer.position();
            ByteBuffer buf = ByteBuffer.allocate(length);
            for(int j = 0; j < length; j++) {
                buf.put(buffer.get());
            }
            ByteBufferUtils.debugAll(buf);
            buf.flip();
            String str = StandardCharsets.UTF_8.decode(buf).toString();
            System.out.println(str);
        }
    }

    // 切换到写模式,并且留下没有读取的数据,解决半包
    buffer.compact();
}
  1. FileChannel的零拷贝
@Test
public void transferTo() {
    try (
            FileChannel from = new FileInputStream("test.txt").getChannel();
            FileChannel to = new FileOutputStream("data.txt").getChannel();
    ) {
        // 使用操作系统的零拷贝,最大传输2G
        from.transferTo(0, from.size(), to);
    } catch (IOException e) {
        e.printStackTrace();
    };
}

/**
 * 传输2G以上的大文件
 */
@Test
public void transferTo2() {
    try (
            FileChannel from = new FileInputStream("data.txt").getChannel();
            FileChannel to = new FileOutputStream("to.txt").getChannel();
    ) {
        // 总共多少字节
        long size = from.size();
        // 还剩多少字节没传输
        long index = size;
        while(index > 0) {
            System.out.println("position: " + (size - index) + " size: " + index);
            // 返回的是传输了多少字节
            index -= from.transferTo(size - index, index, to);
        }
    } catch (IOException e) {
    }
}

6. 使用Files遍历文件夹

public class TestFilesWalkFileTree {
    public static void main(String[] args) throws IOException {
        // 遍历目录下的所有目录以及文件
//        walkFileTree();
        // 过滤出jdk8目录下的jar文件
        filterJar();
    }

    private static void filterJar() throws IOException {
        AtomicInteger fileCount = new AtomicInteger();
        Files.walkFileTree(Paths.get("D:\\java1\\jdk1.8"), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if(file.toString().endsWith("jar")) {
                    System.out.println(file);
                    fileCount.incrementAndGet();
                }
                return super.visitFile(file, attrs);
            }
        });

        System.out.println(fileCount);
    }

    private static void walkFileTree() throws IOException {
        AtomicInteger dirCount = new AtomicInteger();
        AtomicInteger fileCount = new AtomicInteger();
        // 遍历文件夹下的所有目录和文件
        Files.walkFileTree(Paths.get("D:\\java1\\jdk1.8"), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                System.out.println("dir: " + dir);
                // 统计目录数量
                dirCount.incrementAndGet();
                return super.preVisitDirectory(dir, attrs);
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                System.out.println("file: " + file);
                fileCount.incrementAndGet();
                return super.visitFile(file, attrs);
            }
        });

        System.out.println("目录数量:" + dirCount);
        System.out.println("文件数量:" + fileCount);
    }
}

7. 使用Files删除多级目录

/**
 * 删除多级目录
 */
public class TestFilesWalkFileTree2 {
    public static void main(String[] args) throws IOException {
        Files.walkFileTree(Paths.get("F:\\logs"), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                // 打开文件之前做的操作
                return super.preVisitDirectory(dir, attrs);
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                // 打开文件的时候,删除文件
                Files.delete(file);
                return super.visitFile(file, attrs);
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                // 打开文件之后的操作
                // 删除文件夹
                Files.delete(dir);
                return super.postVisitDirectory(dir, exc);
            }
        });
    }
}

8. 复制文件夹

/**
 * 复制文件夹
 */
public class TestFilesWalkFileTree2 {
    public static void main(String[] args) throws IOException {
        String source = "F:\\web\\logs";
        String target = "F:\\web\\logsaaa";

        Files.walk(Paths.get(source)).forEach(path -> {
            // 创建新的目录
            String newPath = path.toString().replace(source, target);

            try {
                // 是不是目录
                if(Files.isDirectory(path)) {
                    Files.createDirectory(Paths.get(newPath));
                }

                // 是文件
                if(Files.isRegularFile(path)) {
                    // 复制文件
                    Files.copy(path, Paths.get(newPath));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
}

9. 阻塞模式-单线程

  1. 阻塞模式,ServerSocketChannel.accept()方法是阻塞等待客户端连接,SocketChannel.read()阻塞等待客户端的写入。
  2. 阻塞模式在多客户端连接的情况下,使用一个单线程处理,那么可能线程阻塞在accpet()方法,就无法处理read()方法。
@Slf4j
public class Service {
    public static void main(String[] args) throws IOException {
        // 创建一个服务端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定一个端口号
        serverSocketChannel.bind(new InetSocketAddress(8080));

        ByteBuffer buffer = ByteBuffer.allocate(16);
        List<SocketChannel> socketChannelList = new ArrayList<>();
        while(true) {
            // 获取一个连接
            log.debug("connecting ...");
            /*
            accept()是一个阻塞方法,线程会阻塞等待客户端连接
             */
            SocketChannel socketChannel = serverSocketChannel.accept();
            log.debug("connected ... {}", socketChannel);
            socketChannelList.add(socketChannel);

            // 遍历所有连接
            for(SocketChannel channel : socketChannelList) {
                log.debug("before read... {}", channel);
                /*
                read()是一个阻塞方法,等待客户端输入数据
                 */
                channel.read(buffer);
                // 切换读模式
                buffer.flip();
                ByteBufferUtils.debugAll(buffer);

                buffer.clear();
                log.debug("after read... {}", channel);
            }
        }
    }
}

public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(8080));
        // debug运行调试
        System.out.println("waiting ...");
    }
}

10. 非阻塞模式-单线程

  1. ServerSocketChannelSocketChannel开始非阻塞模式,都使用configureBlocking(false)
  2. ServerSocketChannl的accept()方法和SocketChannel的read()都不会阻塞。
  3. 在没有客户端连接之前accept()方法接收到的SocketChannel为null,read()读取到的字节数为0
@Slf4j
public class Service {
    public static void main(String[] args) throws IOException {
        // 创建一个服务端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置成非阻塞模式,就不会在accept()阻塞
        serverSocketChannel.configureBlocking(false);
        // 绑定一个端口号
        serverSocketChannel.bind(new InetSocketAddress(8080));

        ByteBuffer buffer = ByteBuffer.allocate(16);
        List<SocketChannel> socketChannelList = new ArrayList<>();
        while(true) {
            /**
             * 不会阻塞在这里,等待客户端连接,没有连接之前socketChannel都是nul
             */
            SocketChannel socketChannel = serverSocketChannel.accept();
            if(socketChannel != null) {
                // 设置为非阻塞模式,不会阻塞在read()方法
                socketChannel.configureBlocking(false);
                log.debug("connected ... {}", socketChannel);
                socketChannelList.add(socketChannel);
            }

            // 遍历所有连接
            for(SocketChannel channel : socketChannelList) {
                /**
                 * read为0说明没有读取到数据
                 */
                int read = channel.read(buffer);
                if(read > 0) {
                    // 切换读模式
                    buffer.flip();
                    ByteBufferUtils.debugAll(buffer);

                    buffer.clear();
                    log.debug("after read... {}", channel);
                }
            }
        }
    }
}

11. Selector

  1. SelectionKey的几个事件:
  • accept : 有连接请求时触发。
  • connect : 客户端,连接建立后触发。
  • read : 可读事件。
  • write : 可写事件。
  1. Selector是工作在非阻塞模式下的,所有Channel都必须要设置configureBlocking(false)
  2. Selector的select()方法,会阻塞等待事件发生,但是如果有事件没有处理,就不会阻塞,所有事件一定要处理或者cancel()
  3. 处理完事件之后,一定要移除SelectionKey。如果不移除,可能下一次发生了其他事件,该key上绑定的事件就为空,出现异常。
@Slf4j
public class SelectorTest {
    public static void main(String[] args) throws IOException {
        // 创建一个Selector
        Selector selector = Selector.open();
        // 创建一个服务器端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));
        /*
         将服务器端的channel注册到selector上
         第二个参数可以选择感兴趣的事件
         返回一个SelectionKey
         */
        SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
        // 给key注册一个感兴趣的事件,连接事件accept
        serverKey.interestOps(SelectionKey.OP_ACCEPT);

        log.debug("{}", serverKey);

        while(true) {
            /*
             阻塞等待事件发生
             如果有事件未处理,那么select不会阻塞,可以调用cancel()方法
             */
            log.debug("blocking ...");
            selector.select();

            log.debug("have an event");

            // 获取所有发生事件的key
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while(iterator.hasNext()) {
                // 获取发生事件的SelectionKey
                SelectionKey key = iterator.next();
                /*
                 处理完key的事件一定要从selectionKey集合上删除
                 不然下次其他key发生了事件,以前的key还在,但是没有真正的发生监听的事件
                 可能就会报错
                 */
                iterator.remove();

                log.debug("event selectionKey {}", key);

                // 区分事件类型
                if (key.isAcceptable()) {
                    // 客户端连接事件
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    // 获取连接服务端的channel
                    SocketChannel socketChannel = channel.accept();
                    log.debug("{}", socketChannel);

                    // 设置客户端channel非阻塞
                    socketChannel.configureBlocking(false);
                    // 将客户端channel注册到select中
                    SelectionKey clientKey = socketChannel.register(selector, 0, null);
                    // 关心读事件
                    clientKey.interestOps(SelectionKey.OP_READ);
                } else if(key.isReadable()) {
                    // 可读事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    channel.read(buffer);
                    buffer.flip();

                    ByteBufferUtils.debugAll(buffer);
                }

                // 不处理事件
//                key.cancel();
            }
        }
    }
}

12. 处理客户端断开连接

  1. 不管是客户端强制退出,还是正常退出(SocketChannel.close()),都会触发一次可读事件
  2. 客户端退出之后,要将客户端对应的key从SelectionKey中移除。key.cancel(),不然就会一直监听到可读事件。
  3. 客户端正常退出时,SocketChannel.read()方法会返回-1
@Slf4j
public class SelectorTest {
    public static void main(String[] args) throws IOException {
        // 创建一个Selector
        Selector selector = Selector.open();
        // 创建一个服务器端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));
        /*
         将服务器端的channel注册到selector上
         第二个参数可以选择感兴趣的事件
         返回一个SelectionKey
         */
        SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
        // 给key注册一个感兴趣的事件,连接事件accept
        serverKey.interestOps(SelectionKey.OP_ACCEPT);

        log.debug("{}", serverKey);

        while(true) {
            /*
             阻塞等待事件发生
             如果有事件未处理,那么select不会阻塞,可以调用cancel()方法
             */
            log.debug("blocking ...");
            selector.select();

            log.debug("have an event");

            // 获取所有发生事件的key
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while(iterator.hasNext()) {
                // 获取发生事件的SelectionKey
                SelectionKey key = iterator.next();
                /*
                 处理完key的事件一定要从selectionKey集合上删除
                 不然下次其他key发生了事件,以前的key还在,但是没有真正的发生监听的事件
                 可能就会报错
                 */
                iterator.remove();

                log.debug("event selectionKey {}", key);

                // 区分事件类型
                if (key.isAcceptable()) {
                    // 客户端连接事件
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    // 获取连接服务端的channel
                    SocketChannel socketChannel = channel.accept();
                    log.debug("{}", socketChannel);

                    // 设置客户端channel非阻塞
                    socketChannel.configureBlocking(false);
                    // 将客户端channel注册到select中
                    SelectionKey clientKey = socketChannel.register(selector, 0, null);
                    // 关心读事件
                    clientKey.interestOps(SelectionKey.OP_READ);
                } else if(key.isReadable()) {
                    // 可读事件
                    // 不管是客户端正常断开连接,还是强制退出连接,都会触发一次可读事件
                    try {
                        SocketChannel channel = (SocketChannel) key.channel();
                        channel.close();
                        ByteBuffer buffer = ByteBuffer.allocate(16);

                        /**
                         * 如果客户端是正常退出,read()方法返回-1
                         */
                        int read = channel.read(buffer);
                        if(read == -1) {
                            // 正常退出,断开连接
                            key.cancel();
                        } else {
                            buffer.flip();
                            ByteBufferUtils.debugAll(buffer);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        // 捕捉到客户端异常断开的异常,将key 取消
                        key.cancel();
                    }
                }
            }
        }
    }
}

13. 处理消息边界

  1. SocketChannel.read(ByteBuffer)如果ByteBuffer分配的字节数较少,消息的长度大于buffer的容量,那么会触发好几次可读事件

  2. 常见处理消息边界的方法
    ① 服务器端固定消息长度,如果客户端传输的数据较小,可能浪费空间。
    ② 使用分隔符拆分,要每个字节进行遍历,效率较低。
    ③ Http2.0使用的TLV格式,使用几个字节表示传递消息的长度。

  3. 在Channel注册到Selector时,register方法的第三个参数,是添加一个附件,可以为每个Channel添加一个独立的ByteBuffer

  4. 使用SelectionKey的attachment()方法获取附件

  5. SelectionKey的attach()添加一个附件

@Slf4j
public class SelectorTest {
    public static void main(String[] args) throws IOException {
        // 创建一个Selector
        Selector selector = Selector.open();
        // 创建一个服务器端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));
        /*
         将服务器端的channel注册到selector上
         第二个参数可以选择感兴趣的事件
         返回一个SelectionKey
         */
        SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
        // 给key注册一个感兴趣的事件,连接事件accept
        serverKey.interestOps(SelectionKey.OP_ACCEPT);

        log.debug("{}", serverKey);

        while (true) {
            /*
             阻塞等待事件发生
             如果有事件未处理,那么select不会阻塞,可以调用cancel()方法
             */
            log.debug("blocking ...");
            selector.select();

            log.debug("have an event");

            // 获取所有发生事件的key
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                // 获取发生事件的SelectionKey
                SelectionKey key = iterator.next();
                /*
                 处理完key的事件一定要从selectionKey集合上删除
                 不然下次其他key发生了事件,以前的key还在,但是没有真正的发生监听的事件
                 可能就会报错
                 */
                iterator.remove();

                log.debug("event selectionKey {}", key);

                // 区分事件类型
                if (key.isAcceptable()) {
                    // 客户端连接事件
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    // 获取连接服务端的channel
                    SocketChannel socketChannel = channel.accept();
                    log.debug("{}", socketChannel);

                    // 设置客户端channel非阻塞
                    socketChannel.configureBlocking(false);

                    ByteBuffer buffer = ByteBuffer.allocate(10);
                    /*
                     将客户端channel注册到select中,将buffer注册到key中
                     */
                    SelectionKey clientKey = socketChannel.register(selector, 0, buffer);
                    // 关心读事件
                    clientKey.interestOps(SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 可读事件
                    // 不管是客户端正常断开连接,还是强制退出连接,都会触发一次可读事件
                    try {
                        SocketChannel channel = (SocketChannel) key.channel();

                        // 取出key中绑定的buffer
                        ByteBuffer buffer = (ByteBuffer) key.attachment();

                        /**
                         * 如果客户端是正常退出,read()方法返回-1
                         */
                        int read = channel.read(buffer);
                        if (read == -1) {
                            // 正常退出,断开连接
                            key.cancel();
                        } else {
                            // 处理客户端传递过来的消息
                            split(buffer);
                            // 判断是否解析出一条完整的消息
                            if (buffer.position() == buffer.limit()) {
                            /*
                             说明一条消息还没解析完,ByteBuffer就已经不够用了
                             扩容
                             */
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
                                // 复制老buffer的数据
                                buffer.flip();
                                newBuffer.put(buffer);
                                // 绑定到key上
                                key.attach(newBuffer);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        // 捕捉到客户端异常断开的异常,将key 取消
                        key.cancel();
                    }
                }
            }
        }
    }

    /**
     * 处理粘包、半包
     *
     * @param buffer
     */
    public static void split(ByteBuffer buffer) {
        // 切换到读模式
        buffer.flip();
        // 遍历读取到的数据
        for (int i = 0; i < buffer.limit(); i++) {
            // get(index),不会移动position位置
            if (buffer.get(i) == '\n') {
                // 计算当前读取到的一条信息的长度
                int length = i + 1 - buffer.position();
                ByteBuffer buf = ByteBuffer.allocate(length);
                for (int j = 0; j < length; j++) {
                    buf.put(buffer.get());
                }
                ByteBufferUtils.debugAll(buf);
                buf.flip();
                String str = StandardCharsets.UTF_8.decode(buf).toString();
                System.out.println(str);
            }
        }

        // 切换到写模式,并且留下没有读取的数据,解决半包
        buffer.compact();
    }
}

14. 写事件-写入内容太多

  1. 当写入大量数据时,有可能一次写不完。SocketChannel.write()返回一个int整型,代表实际写入的字节数据。
  2. buffer.hasRemaining():buffer中是否还有数据没写出。
public class WriteServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while(true) {
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while(iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 发送大量数据给客户端
                    StringBuilder builder = new StringBuilder();
                    for(int i = 0; i < 300000; i++) {
                        builder.append("a");
                    }
                    ByteBuffer buffer = Charset.defaultCharset().encode(builder.toString());
                    /*
                     数据量大可能一次写不完,返回的是实际写入的字节数
                     hasRemaining还有字节没发送就继续读取
                     */
                    while(buffer.hasRemaining()) {
                        int write = socketChannel.write(buffer);
                        System.out.println(write);
                    }
                }
            }
        }
    }
}

15. 处理可写事件

public class WriteServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while(true) {
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while(iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    SelectionKey socketChannelKey = socketChannel.register(selector, 0, null);

                    // 发送大量数据给客户端
                    StringBuilder builder = new StringBuilder();
                    for(int i = 0; i < 300000; i++) {
                        builder.append("a");
                    }
                    ByteBuffer buffer = Charset.defaultCharset().encode(builder.toString());
                    /*
                     数据量大可能一次写不完,返回的是实际写入的字节数
                     hasRemaining还有字节没发送就继续读取
                     */
//                    while(buffer.hasRemaining()) {
//                        int write = socketChannel.write(buffer);
//                        System.out.println(write);
//                    }

                    if(buffer.hasRemaining()) {
                        // 如果buffer中还有数据,注册一个可写事件
                        socketChannelKey.interestOps(socketChannelKey.interestOps() + SelectionKey.OP_WRITE);
                        // 绑定一个buffer
                        socketChannelKey.attach(buffer);
                    }
                } else if(key.isWritable()) {
                    // 如果读取到了write事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    // 向客户端写入
                    int write = channel.write(buffer);
                    System.out.println(write);

                    // 如果buffer中没有了数据,就移除可写事件
                    if(!buffer.hasRemaining()) {
                        key.attach(null);
                        key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
                    }
                }
            }
        }
    }
}

16. 多线程worker优化

  1. selector.select()方法会阻塞等待事件发生,channel.register()如果在select之后,那么select阻塞做了,就不会处理register事件。
  2. 可以使用selector.wakeup()唤醒select()的阻塞。
  3. 两个线程之间传递数据可以使用一个队列。
@Slf4j
public class MultiThreadServer {
    public static void main(String[] args) throws IOException {
        // 使用一个boss线程,专门处理accept连接
        Thread.currentThread().setName("boss");
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));

        Selector selector = Selector.open();
        SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
        serverKey.interestOps(SelectionKey.OP_ACCEPT);

        // 创建一个worker,监听读写事件
        Worker worker = new Worker("work-0");
        while(true) {
            selector.select();

            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                if (key.isAcceptable()) {
                    SocketChannel channel = serverSocketChannel.accept();
                    channel.configureBlocking(false);
                    log.debug("connect... , {}", channel.getRemoteAddress());

                    // 将channel注册到worker中
                    log.debug("before register... , {}", channel.getRemoteAddress());
                    worker.register(channel);
                    
                    log.debug("after register... , {}", channel.getRemoteAddress());
                }
            }
        }
    }

    /**
     * 使用一个worker线程,专门处理可读可写事件
     */
    static class Worker implements Runnable{
        private Thread thread;
        private Selector selector;
        private String name;
        private volatile boolean start = false;
        // 使用一个队列传递数据
        private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();

        public Worker(String name) {
            this.name = name;
        }

        /**
         * 初始化方法
         */
        public void register(SocketChannel channel) throws IOException {
            // 保证线程只初始化一次
            if(!start) {
                thread = new Thread(this, name);
                selector = Selector.open();
                thread.start();
            }

            /*
            如果selector.select()方法先执行,那么就会阻塞做,register就不能注册到seletor中
             */
            queue.add(() -> {
                try {
                    channel.register(selector, SelectionKey.OP_READ, null);
                } catch (ClosedChannelException e) {
                    e.printStackTrace();
                }
            });

            /**
             * 唤醒selector的阻塞
             */
            selector.wakeup();
        }

        @Override
        public void run() {
            while(true) {
                try {
                    // worker线程监听
                    selector.select();
                    // 获取队列中的register事件
                    Runnable poll = queue.poll();
                    if(poll != null) {
                        poll.run();
                    }
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = keys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        iterator.remove();

                        SocketChannel channel = (SocketChannel) key.channel();
                        channel.configureBlocking(false);
                        ByteBuffer buffer = ByteBuffer.allocate(16);
                        channel.read(buffer);
                        buffer.flip();
                        ByteBufferUtils.debugAll(buffer);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

17. 多个Worker

@Slf4j
public class MultiThreadServer {
    public static void main(String[] args) throws IOException {
        // 使用一个boss线程,专门处理accept连接
        Thread.currentThread().setName("boss");
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));

        Selector selector = Selector.open();
        SelectionKey serverKey = serverSocketChannel.register(selector, 0, null);
        serverKey.interestOps(SelectionKey.OP_ACCEPT);

        // 多个worker
        Worker[] workers = new Worker[2];
        for(int i = 0; i < workers.length; i++) {
            workers[i] = new Worker("work-" + i);
        }

        AtomicInteger index = new AtomicInteger();
        while(true) {
            selector.select();

            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                if (key.isAcceptable()) {
                    SocketChannel channel = serverSocketChannel.accept();
                    channel.configureBlocking(false);
                    log.debug("connect... , {}", channel.getRemoteAddress());

                    // 将channel注册到worker中
                    log.debug("before register... , {}", channel.getRemoteAddress());
                    // 轮询注册到多个worker中
                    workers[index.incrementAndGet() % workers.length].register(channel);
                    log.debug("after register... , {}", channel.getRemoteAddress());
                }
            }
        }
    }

    /**
     * 使用一个worker线程,专门处理可读可写事件
     */
    static class Worker implements Runnable{
        private Thread thread;
        private Selector selector;
        private String name;
        private volatile boolean start = false;
        // 使用一个队列传递数据
        private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();

        public Worker(String name) {
            this.name = name;
        }

        /**
         * 初始化方法
         */
        public void register(SocketChannel channel) throws IOException {
            // 保证线程只初始化一次
            if(!start) {
                thread = new Thread(this, name);
                selector = Selector.open();
                thread.start();
            }

            /*
            如果selector.select()方法先执行,那么就会阻塞做,register就不能注册到seletor中
             */
            queue.add(() -> {
                try {
                    channel.register(selector, SelectionKey.OP_READ, null);
                } catch (ClosedChannelException e) {
                    e.printStackTrace();
                }
            });

            /**
             * 唤醒selector的阻塞
             */
            selector.wakeup();
        }

        @Override
        public void run() {
            while(true) {
                try {
                    // worker线程监听
                    selector.select();
                    // 获取队列中的register事件
                    Runnable poll = queue.poll();
                    if(poll != null) {
                        poll.run();
                    }
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = keys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        iterator.remove();

                        SocketChannel channel = (SocketChannel) key.channel();
                        channel.configureBlocking(false);
                        ByteBuffer buffer = ByteBuffer.allocate(16);
                        log.debug("read ...{}", channel);
                        channel.read(buffer);
                        buffer.flip();
                        ByteBufferUtils.debugAll(buffer);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

18. IO模型

在这里插入图片描述

  1. 阻塞IO:Java程序切换到Linux内核空间调用操作系统的read读取方法,如果操作系统没有返回,线程就会阻塞
  2. 非阻塞IO:Java程序在操作系统的等待数据阶段,是不阻塞的,如果读取到的字节数为0,也会立即返回。但是在操作系统复制数据阶段(从网卡将数据复制出来),线程还是需要阻塞等待数据
  3. 多路复用:在阻塞模式中,如果线程阻塞在accept中,那么另一个channel的read读取事件,就只能在不阻塞后在处理。多路复用使用selector监听多个事件。
  4. 异步阻塞:异步就是通过另外一个线程处理事情,然后调用一个回调函数获取结果。异步的本质是没有阻塞的
  5. 异步非阻塞

19. 零拷贝

在这里插入图片描述

  1. 内部工作流程:
    在这里插入图片描述

  2. java本身是不具备IO读写能力的,调用read方法后,java程序要从用户态切换到核心态,调用操作系统的Kernel的读能力,将数据读到内核缓冲区,这期间,用户线程会阻塞,操作系统使用DMA实现文件,期间不会使用CPU.

在这里插入图片描述

  1. NIO的ByteBuffer.allocateDirect(10)DirectByteBuffer使用的是操作系统内存,少一次拷贝。

在这里插入图片描述

  1. java中的FileChannel.transferTo进一步优化,底层采用Linux2.1后提供的sendFile方法。
  2. Java调用transferTo方法后,Java程序要从用户态切换到核心态,使用DMA将数据读入内核缓冲区,不会使用CPU

在这里插入图片描述

  1. Linux2.4后进一步优化。

20. 异步IO-AIO

@Slf4j
public class AioFileChannel {
    public static void main(String[] args) {
        /*
         获取一个异步IO的文件读取类
         参数1:文件路径
         参数2:操作类型(读写)
         */
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("F:\\IdeaProject\\github\\my-spring-boot-demo\\netty\\test.txt"), StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(16);
            /**
             * 读取文件
             * 参数1:接收文件的buffer
             * 参数2:读取的起始位置
             * 参数3:附件,如果没读取完,接着读取
             * 参数4:异步读取之后的回调函数,回调是一个守护线程
             */
            log.debug("read start...");
            channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                // 读取成功
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    log.debug("read success... {}", result);
                    attachment.flip();
                    ByteBufferUtils.debugAll(attachment);
                }
                // 读取异常
                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {

                }
            });

            log.debug("read end...");
            // aio读取回调是一个守护线程,主线程不能停止
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Netty HelloWorld

  1. 服务器端
/**
 * Hello World 服务器端
 */
public class HelloServer {
    public static void main(String[] args) {
        // 启动服务器,负责组装netty组件
        new ServerBootstrap()
                // 加入一个轮询事件组
                .group(new NioEventLoopGroup())
                // 服务器端ServerSocketChannel
                .channel(NioServerSocketChannel.class)
                // worker负责读写,执行read、write那些操作
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    // 数据读写的通道
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 解码handler:将ByteBuf转化为字符串
                        nioSocketChannel.pipeline().addLast(new StringDecoder());
                        // 自定义handler
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                // 触发了读事件
                                System.out.println(msg);
                            }
                        });
                    }
                })
                .bind(8888);
    }
}
  1. 客户端
/**
 * Hello World 客户端
 */
public class HelloClient {
    public static void main(String[] args) throws InterruptedException {
        // 客户端启动器
        new Bootstrap()
                // 监听轮询分组
                .group(new NioEventLoopGroup())
                // 客户端的channel实现
                .channel(NioSocketChannel.class)
                // 处理器
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 添加一个编码处理
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                // 连接服务器
                .connect(new InetSocketAddress("localhost", 8888))
                // 阻塞方法,直到连接建立
                .sync()
                // 获取传输通道
                .channel()
                // 向服务器发送数据
                .writeAndFlush("hello, world");
    }
}
  1. Netty中数据的传输使用ByteBuf,是对JDK中的ByteBuffer做了进一步封装
  2. 服务器端启动后,NioEventLoopGroup中有一个select会监听accept事件,等待客户端连接,EventLoop就相当于Selector监听事件。
    在这里插入图片描述
  3. pipeline(流水线),负责发布事件(读、读取完成…),传播给每个handler,handler对感兴趣的事件进行处理。
  4. eventLoop理解为处理数据的工人。
  5. 工人可以管理多个channel的io操作,eventLoop会绑定一个channel,负责到底。
  6. eventLoop有任务队列。任务分为普通任务和定时任务。
  7. eventLoop底层使用了一个单线程的线程池

Netty组件

1. EventLoop

  1. EventLoop本质上是一个单线程执行器(同时维护了一个Selector),里面有run方法,处理channel的io事件
  2. EventLoop继承关系复杂:
  • 一条线继承了JDK线程池中的ScheduleExecutorService(执行定时任务),因此包含线程池的所有方法。
  • 另一条线继承Netty自己的OrderedEventExecutor。方法inEventLoop(Thread thread)判断是否属于此EventLoop。parent()方法,查看自己属于那个EventLoopGroup

2. EventLoopGroup

  1. EventLoopGroup就是一组EventLoop,Channel会调用EventLoopGroup中的register()方法来绑定其中一个EventLoop。后续,Channel发生了io事件,都有此EventLoop进行处理(为了保证线程安全)。

  2. 实现类:

  • NioEventLoopGroup:能够处理IO事件、普通任务、定时任务。创建的默认线程数,max(1, cpu核心数*2)
  • DefaultEventLoopGroup:处理普通任务、定时任务。

1. 执行普通任务、定时任务

  1. 从EventLoopGroup中可以获得EventLoop对象。EventLoop继承了线程池ScheduleExecutorService定时任务执行对象,可以执行普通任务和定时任务。
/**
 * 测试EventLoop
 */
@Slf4j
public class TestEventLoop {
    public static void main(String[] args) {
        // 获取线程的核心数
        // System.out.println(NettyRuntime.availableProcessors());

        // 新建一个事件循环组
        // 默认创建线程的数量:max(1, cpu核心数*2)
        // private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
        EventLoopGroup group = new NioEventLoopGroup(2);

        // 获取事件循环对象EventLoop:指定2个对象,轮询获取
        System.out.println(group.next());
        System.out.println(group.next());
        System.out.println(group.next());
        System.out.println(group.next());

        // 执行一个普通任务
//        group.next().submit(() -> {
//            try {
//                Thread.sleep(2000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            log.debug("submit");
//        });

        // 执行一个定时任务,已一定频率执行
        group.next().scheduleAtFixedRate(() -> {
            log.debug("执行");
        }, 0, 1, TimeUnit.SECONDS);

        log.debug("main线程");
    }
}

2. 执行IO任务

  1. 服务器端
@Slf4j
public class EventLoopServer {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                // 强转为ByteBuf对象
                                ByteBuf buf = (ByteBuf) msg;
                                // ByteBuf转化为String,指定字符集
                                log.debug(buf.toString(Charset.defaultCharset()));
                            }
                        });
                    }
                })
                .bind(8888);
    }
}
  1. 客户端
@Slf4j
public class EventLoopClient {
    public static void main(String[] args) throws InterruptedException {
        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect(new InetSocketAddress(8888))
                .sync()
                .channel();

        log.debug("channel: {}", channel);

		// 断点停在这里,可以随便发数据,也可以启动多个客户端
        System.out.println();

    }
}
  1. DEBUG模式下,向服务器端发送数据。
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3. 对EventLoopGroup进行细分

  1. 如果NioEventLoopGroup执行一个时间较长的任务,那会就会影响到其他Channel的处理。
  2. 可以新建一个DefaultEventLoopGroup,对时间长的任务进行单独处理,而不影响NioEventLoopGroup对其他Channel的处理。
@Slf4j
public class EventLoopServer {
    public static void main(String[] args) {
        // 创建一个EventLoop
        EventLoopGroup group = new DefaultEventLoopGroup();

        new ServerBootstrap()
                // 创建两个事件轮询组,第一个只负责监听Accept事件,第二个负责读写事件
                .group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                // 强转为ByteBuf对象
                                ByteBuf buf = (ByteBuf) msg;
                                // ByteBuf转化为String,指定字符集
                                log.debug(buf.toString(Charset.defaultCharset()));

                                // 传递给下面的handler链继续处理
                                ctx.fireChannelRead(msg);
                            }
                        })
                                // 指定group进行处理
                                .addLast(group, new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                log.debug(buf.toString(Charset.defaultCharset()));
                            }
                        });
                    }
                })
                .bind(8888);
    }
}

在这里插入图片描述

4. EventLoop之间的切换源码分析

  1. 在EventLoop之间切换就是切换线程,因为一个EventLoop绑定一个线程。
  2. 线程之间切换,数据是如何传输的
  3. 主要的代码在io.netty.channel.AbstractChannelHandlerContext类中。
// 参数1:AbstractChannelHandlerContext 通道处理上下文,可以获取通道channel
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);

	// 使用通道处理上下文,获得通道对应的EventLoop
    EventExecutor executor = next.executor();
    // 判断是不是同一个EventLoop,也就是判断是不是同一个线程处理
    if (executor.inEventLoop()) {
    	// 如果是,就直接进行处理
        next.invokeChannelRead(m);
    } else {
    	// 如果不是EventLoop是间接继承了线程池
    	// ScheduledExecutorService,创建一个线程执行
        executor.execute(new Runnable() {
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }

}

3. Channel

  1. close():关闭Channel。
  2. closeFuture():处理Channel的关闭。
  • sync():同步等待。(当前线程等待(阻塞)EventLoop的线程将客户端和服务器建立连接之后,唤醒当前线程继续运行)
  • addListener():异步等待。(在EventLoop中的线程建立完连接之后,会回调加入的方法)
  1. pipeline():添加处理器handler
  2. write():将数据写入,但是可能不会立即将数据通过网络发出去,有一个缓冲机制。可以调用flush()将缓冲区的数据发送。
  3. writeAndFlush():将数据写入,并刷出

1. sync和addListener

  1. 演示sync()代码:
@Slf4j
public class EventLoopClient {
    public static void main(String[] args) throws InterruptedException {
        // Future和Promise类型都是和异步方法配套使用
        // 调用connect得到ChannelFuture对象
        ChannelFuture channelFuture = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                // connect是异步非阻塞的,连接操作是由NioEventLoop中的线程去连接客户端
                // main线程继续向下执行
                .connect(new InetSocketAddress(8888));

        // main线程会等待nio线程将客户端和服务器之间建立连接
        channelFuture.sync();

        // 获取连接的channel对象:如果不调用sync()方法,可能另一个线程还没有建立连接
        Channel channel = channelFuture.channel();

        log.debug("channel: {}", channel);
        // 发送数据
        channel.writeAndFlush("123");

        System.out.println();

    }
}
  1. 演示addListen()代码:
@Slf4j
public class EventLoopClient {
    public static void main(String[] args) throws InterruptedException {
        // Future和Promise类型都是和异步方法配套使用
        // 调用connect得到ChannelFuture对象
        ChannelFuture channelFuture = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                // connect是异步非阻塞的,连接操作是由NioEventLoop中的线程去连接客户端
                // main线程继续向下执行
                .connect(new InetSocketAddress(8888));

        // 加入异步回调
        channelFuture.addListener(new ChannelFutureListener() {
            // EventLoop中的线程建立好与服务器之间的连接后,会回调此方法
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                // 获取channel
                Channel channel = channelFuture.channel();
                log.debug("channel {}", channel);

                channel.writeAndFlush("13245600");
            }
        });

//        // main线程会等待nio线程将客户端和服务器之间建立连接
        channelFuture.sync();

        // 获取连接的channel对象:如果不调用sync()方法,可能另一个线程还没有建立连接
        Channel channel = channelFuture.channel();

        log.debug("channel: {}", channel);
        // 发送数据
        channel.writeAndFlush("123");

        System.out.println();

    }
}

2. 使用ChannelFuture正确的关闭Channel

  1. channel.close()也是一个异步操作,由EventLoop里面的线程关闭Channel通道。
  2. 如果正确的在channel关闭之后,做一些操作。
  3. 可以使用上面的sync()等待关闭完之后,也可以使用addListener(),将关闭后的任务,交给EventLoop的线程。
/**
 * 从控制台输入内容,发送到服务器
 */
@Slf4j
public class CloseFutureClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        ChannelFuture channelFuture = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 设置打印日记,可以查看netty内部的打印
                        nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        // 设置编码
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect(new InetSocketAddress(8888));

        Channel channel = channelFuture.sync().channel();

        // 新建一个线程处理控制台输入
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            // 循环从控制台输入
            while(true) {
                String line = scanner.nextLine();
                if("ex".equals(line)) {
                    // 断开连接:close也是一个异步执行
                    channel.close();
                    break;
                } else {
                    channel.writeAndFlush(line);
                }
            }
        }, "线程1").start();

        // 正确处理,channel.close():方式一

        // 获取一个关闭channel对象
        ChannelFuture closeFuture = channel.closeFuture();
        log.debug("channelFuture : {}", closeFuture);
        /*
        // 阻塞等待channel关闭
        closeFuture.sync();

        // 下面就是正确执行,channel关闭之后的流程
        log.debug("channel 关闭成功,退出连接");
        */

        // 正确处理,channel.close():方式二

        closeFuture.addListener(new ChannelFutureListener() {
            // 这个方法是close()的线程回调执行
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                log.debug("channel 关闭成功,退出连接");

                // 优雅的关闭EventLoopGroup,事件轮询组
                // 会把没执行完的任务执行完
                group.shutdownGracefully();
            }
        });
    }
}

4. Future和Promise

  1. 才进行异步处理时,经常使用这两个接口。
  2. Netty中的Future继承子JDK中的Future,Promise对Netty中的Future进行了扩展。
  3. JDK中的Future只能同步等任务结束,才能得到结果。
  4. Netty中的Future可以同步等待任务结束得到结果,也可以异步获取结果,但是都要任务结束。
  5. Netty中的Promise有Future的功能,而且脱离任务存在,只作为两个线程之间传递结果的容器。
    在这里插入图片描述

1. JDK中的Future演示:

/**
 * jdk中的Future的测试
 */
@Slf4j
public class JDKFutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 新建一个线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        Future<Integer> future = threadPool.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("执行计算");
                Thread.sleep(1000);
                return 50 - 10;
            }
        });

        // future或阻塞等待,计算结果
        log.debug("计算结果为:{}", future.get());
    }
}

2. Netty中的Future演示:

/**
 * 测试netty的Future
 */
@Slf4j
public class NettyFutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 新建一个事件循环组
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        // 获取的一个EventLoop,相当于获得一个线程
        EventLoop eventLoop = eventLoopGroup.next();

        // 提交一个任务
        // 这里的Future是netty包下的Future
        Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("执行计算");
                Thread.sleep(1000);
                return 40 - 20;
            }
        });

        // main线程等待结果
        // log.debug("结果:{}", future.get());

        // 异步获取结果:由执行计算的线程执行
        future.addListener(new GenericFutureListener<Future<? super Integer>>() {
            @Override
            public void operationComplete(Future<? super Integer> future) throws Exception {
                log.debug("异步获取结果:{}", future.getNow());
            }
        });
    }
}

3. Promise的演示

/**
 * Netty中Promise和Future比较相似,Future是线程执行完计算返回
 * Promise可以主动创建,并且可以返回错误的信息
 */
@Slf4j
public class NettyPromise {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        EventLoop eventLoop = eventLoopGroup.next();

        // 创建一个Promise对象
        DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);

        // 新建一个线程执行任务
        new Thread(() -> {
            try {
                log.debug("开始计算");
                Thread.sleep(1000);
                // int i = 1 / 0;
                // 使用promise填充计算结果
                promise.setSuccess(80);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 出现错误就填充一个异常对象
                promise.setFailure(e);
            }
        }).start();

        // 同步获取
        log.debug("计算结果:{}", promise.get());

        // 异步获取获取
//        promise.addListener();
    }
}

5. Handle和Pipeline

  1. ChannelHandle用来处理Channel上的各种事件,分为入站(数据读取操作)、出站(数据写入操作)两种。
  2. 入站(ChannelInboundHandler)和出站(ChannelOutboundHandler)之间有一个明显的区别:若数据是从用户应用程序到远程主机则是“出站(outbound)”,相反若数据时从远程主机到用户应用程序则是“入站(inbound)”
  3. 所有的ChannelHandler连成一串,就是Pipeline
  4. 入站处理器通常是ChannelboundHandlerAdapter的子类,主要用来读取客户端数据,写回结果
  5. 出战处理器通常是ChannelOutboundHandlerAdapter的子类,主要对写数据加工
  6. 入站处理器,是顺序执行。出战处理器,是逆序执行。
  7. 管道Channel调用write写出数据,出战处理器才会执行。
  8. 处理器默认有一个headtail,形成一条处理器链,调用处理器管道的writeAndFlush()方法,整个出站处理器链调用从tail开始,如果是当前处理器链上的ChannelHandlerContext调用了writeAndFlush()方法,那么就是向前执行出站处理器。
  9. 代码演示:
@Slf4j
public class PipelineTest {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                // 添加一个ChannelInboundHandlerAdapter入站处理器的实现类
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 拿到一个Pipeline
                        ChannelPipeline pipeline = nioSocketChannel.pipeline();
                        // 添加一些处理器,本来有一个 head -> tail
                        pipeline.addLast("handler1", new ChannelInboundHandlerAdapter() {
                            // 监听读取事件
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("handler1");
                                ByteBuf byteBuf = (ByteBuf) msg;
                                String str = byteBuf.toString(Charset.defaultCharset());

                                // 必须调用数据才会传递到下一个责任链
                                super.channelRead(ctx, str);
                            }
                        });

                        pipeline.addLast("handler2", new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("handler2");

                                String str = (String) msg;

                                log.debug("2。。。" + str);
                                super.channelRead(ctx, msg);
                            }
                        });

                        pipeline.addLast("handler3", new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("handler3");
                                super.channelRead(ctx, msg);

                                // 只有写入数据,下面的出战处理器才会被调用,会从tail开始向前寻找出站处理器链
                                // ctx.alloc().buffer() 分配一个ByteBuf
                                // nioSocketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("nihao".getBytes()));
                                
                                // 只会从当前处理器,向前寻找出站处理器链
                                ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("nihao".getBytes()));
                            }
                        });

                        // 添加一些出战处理器:出战处理器的调用顺序是相反的
                        pipeline.addLast("outHandler1", new ChannelOutboundHandlerAdapter() {
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("outHandler1");
                                super.write(ctx, msg, promise);
                            }
                        });

                        pipeline.addLast("outHandler2", new ChannelOutboundHandlerAdapter() {
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("outHandler2");
                                super.write(ctx, msg, promise);
                            }
                        });

                        pipeline.addLast("outHandler3", new ChannelOutboundHandlerAdapter() {
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("outHandler3");
                                super.write(ctx, msg, promise);
                            }
                        });
                    }
                })
                .bind(8888);
    }
}
  1. 使用一个netty提供的,测试Handler的处理工具类EmbeddedChannel
// 测试netty中的工具类,EmbeddedChannel,可以用来调试handler处理器
@Slf4j
public class EmbeddedChannelTest {
    public static void main(String[] args) {
        // 新建两个入站处理器
        ChannelInboundHandlerAdapter inboundHandler1 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.info("in handler1");
                super.channelRead(ctx, msg);
            }
        };

        ChannelInboundHandlerAdapter inboundHandler2 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.info("in handler2");
                super.channelRead(ctx, msg);
            }
        };

        // 新建两个出站处理器
        ChannelOutboundHandlerAdapter outboundHandler1 = new ChannelOutboundHandlerAdapter() {
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.debug("out handler1");
                super.write(ctx, msg, promise);
            }
        };

        ChannelOutboundHandlerAdapter outboundHandler2 = new ChannelOutboundHandlerAdapter() {
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.debug("out handler2");
                super.write(ctx, msg, promise);
            }
        };

        // 创建一个netty提供的测试Handler的工具类
        EmbeddedChannel channel = new EmbeddedChannel(inboundHandler1, inboundHandler2, outboundHandler1, outboundHandler2);

        // 模拟入站操作,查看处理链的执行流程
        channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes()));
        // 模拟出站操作
        channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world".getBytes()));
    }
}

6. ByteBuf

  1. ByteBufAllocator.DEFALUT.buffer()ByteBufAllocator.DEFALUT.directBuffer():分配的是直接内存
  2. ByteBufAllocator.DEFALUT.heapBuffer():分配的堆内存
  3. ByteBuf默认字节数是256。可以自动扩容
  4. 默认是开启池化功能的。(类似于线程池、连接池)
public class ByteBufTest {
    public static void main(String[] args) {
        // 创建一个默认的ByteBuf:默认字节为256,可以自动扩容
        // 是直接内存的,默认开启池化功能
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
        System.out.println(byteBuf.getClass());

        ByteBuf heapBuffer = ByteBufAllocator.DEFAULT.heapBuffer();
        System.out.println(heapBuffer.getClass());

        // 也可以创建一个指定容量的ByteBuf
//        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);

        // 写入数据
        byteBuf.writeBytes("nihao".getBytes());

        // 可以查看byteBuf的写索引、读索引、和容量
        // System.out.println(byteBuf);

        // 使用log工具方法,查看byteBuf内部
        log(byteBuf);
    }

    private static void log(ByteBuf buffer) {
        int length = buffer.readableBytes();
        int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
        StringBuilder buf = new StringBuilder(rows * 80 * 2)
                .append("read index:").append(buffer.readerIndex())
                .append(" write index:").append(buffer.writerIndex())
                .append(" capacity:").append(buffer.capacity())
                .append(NEWLINE);
        appendPrettyHexDump(buf, buffer);
        System.out.println(buf.toString());
    }
}

1. ByteBuf的组成

在这里插入图片描述

2. 写入方法

writeBoolean(boolean value)写入 boolean 值用一字节 01|00 代表 true|false
writeByte(int value)写入 byte 值
writeShort(int value)写入 short 值
writeInt(int value)写入 int 值Big Endian,即 0x250,写入后 00 00 02 50
writeIntLE(int value)写入 int 值Little Endian,即 0x250,写入后 50 02 00 00
writeLong(long value)写入 long 值
writeChar(int value)写入 char 值
writeFloat(float value)写入 float 值
writeDouble(double value)写入 double 值
writeBytes(ByteBuf src)写入 netty 的 ByteBuf
writeBytes(byte[] src)写入 byte[]
writeBytes(ByteBuffer src)写入 nio 的 ByteBuffer
int writeCharSequence(CharSequence sequence, Charset charset)写入字符串CharSequence是String、StringBuilder、StringBuffer的父接口
  1. 先写入4个字节
byteBuf.writeBytes(new byte[]{1,2,3,4});
log(byteBuf);
  1. ByteBuf的容量情况
read index:0 write index:4 capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04                                     |....            |
+--------+-------------------------------------------------+----------------+
  1. 在写入一个int,int是4个字节。
byteBuf.writeInt(5);
log(byteBuf);
  1. ByteBuf的容量情况
read index:0 write index:8 capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05                         |........        |
+--------+-------------------------------------------------+----------------+
  1. 当在写入1个int时,容量不够(初始容量是10),就会触发扩容
  2. 扩容规则:
  • 如果写入数据大小没有超过512,就选择下一个16的整数,例如写入后是12,就选择16。
  • 如果写入后的大小超过512 = 2^9,就选择下一个2^n,例如写入后是513,那么扩容后就是2^10 = 1024
  • 扩容超过最大容量(max capatity)就会报错,最大容量是Integer.MAX_VALUE

3. 读取

  1. 读取一个字节
byteBuf.readByte();
  1. 读取int,在读取之间先做一个标记mark
byteBuf.markReaderIndex();
byteBuf.readInt();
  1. 想要重复读取,就重置到标记位置。
byteBuf.resetReaderIndex();
  1. 如果使用get方法,不会改变读取指针(read index)。

4. 释放ByteBuf的内存

  1. Netty中有堆外内存的ByteBuf实现,堆外内存最好是手动释放

  2. UnpooledHeapByteBuf使用的是JVM的内存,只需要等GC回收。

  3. UnpooledDirectByteBuf使用的是直接内存,需要调用特殊方法进行回收。

  4. PooledByteBuf和它的子类使用了池化的机制,需要更复杂的规则来回收。

  5. Netty使用了引用计数器来控制回收内存,每个ByteBuf都实现了ReferenceCounted接口:

  • 每个ByteBuf对象的初始计数为1
  • 调用release方法计数器减1,如果计数为0,ByteBuf内存被回收。
  • 调用retain方法计数加1,表示调用者没使用完之前(ByteBuf可以沿着handler应用链向下传递),其他handler即使调用release也不会被回收。
  • 当计数器为0时,底层内存会被回收,这是即使ByteBuf对象还在,其各个方法都无法正常使用。
  1. 因为pipeline的存在,一般会将ByteBuf向下传递给下一个ChannelHandler。
  2. 谁是最后使用者,谁就负责释放(release)ByteBuf的内存。

5. slice(切片)

  1. slice是零拷贝的应用之一。
  2. 可以将原始的ByteBuf切片成多个ByteBuf,切片后的ByteBuf没有发生内存复制,还是使用了原始ByteBuf的内存,切片后的ByteBuf维护独立的read、write指针。
public class SliceTest {
    public static void main(String[] args) {
        // 新建一个ByteBuf
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
        // 加入数据
        buffer.writeBytes(new byte[] {1,2,3,4,5,6,7,8,9,10});
        log(buffer);

        // 使用slice进行切片
        ByteBuf b1 = buffer.slice(0, 5);
        ByteBuf b2 = buffer.slice(5, 5);

        log(b1);
        log(b2);

        System.out.println("---------------------------------");

        // 切片后的ByteBuf和原ByteBuf共用底层内存
        b1.setByte(0, 2);

        log(buffer);
        log(b1);
    }
}
  1. 切片后的ByteBuf的最大容量就是切片的容量。

7. 服务器和客户端双向通信

/**
 * 完成一个双向通信,实现回声服务器,客户端发什么,服务器端就回复什么
 */
public class EchoService {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 添加链式处理
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                if(msg instanceof ByteBuf) {
                                    // 显示客户端发送来的消息
                                    ByteBuf buf = (ByteBuf) msg;
                                    System.out.println(buf.toString(Charset.defaultCharset()));

                                    // 给客户端写回数据,获得一个ByteBuf
                                    ByteBuf writeBuffer = ctx.alloc().buffer();
                                    writeBuffer.writeBytes(buf);
                                    ctx.writeAndFlush(writeBuffer);
                                }
                            }
                        });
                    }
                })
                .bind(8888);
    }
}
/**
 * 双向通信客户端,客户端写入什么数据,服务器端就返回什么
 */
public class EchoClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        Channel channel = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 添加一个写出编码器
                        nioSocketChannel.pipeline().addLast(new StringEncoder());

                        // 获取服务器端的读取事件
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                System.out.println(buf.toString(Charset.defaultCharset()));
                            }
                        });
                    }
                })
                .connect("127.0.0.1", 8888)
                .sync()
                .channel();

        // 释放连接
        channel.closeFuture().addListener(future -> {
            group.shutdownGracefully();
        });

        // 新建一个线程从控制台写数据
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while(true) {
                String str = scanner.nextLine();
                if(!"ex".equals(str)) {
                    channel.writeAndFlush(str);
                } else {
                    // 关闭客户端
                    channel.close();
                    System.out.println("退出!");
                    break;
                }
            }
        }).start();
    }
}

Netty进阶

1. Netty中的粘包、半包现象

/**
 * 粘包、半包演示
 */
@Slf4j
public class HelloWorldService {
    public static void main(String[] args) {
        // 创建一个只处理连接的线程
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        // 创建处理读写数据的线程
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss, worker);

            // 修改服务器端接收缓冲区,一次接收10字节,演示半包现象
            serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);

            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel socketChannel) throws Exception {
                    // 添加一个输出日志打印
                    socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        // 当有客户端连接到服务器是,触发active事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("connected{}", ctx.channel());
                            super.channelActive(ctx);
                        }

                        // 当客户端断开连接时,触发此方法
                        @Override
                        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("disconnect{}", ctx.channel());
                            super.channelInactive(ctx);
                        }
                    });
                }
            });

            // 绑定端口
            ChannelFuture channelFuture = serverBootstrap.bind(8888);
            log.debug("{} binging", channelFuture.channel());

            // 异步处理建立连接
            channelFuture.sync();
            log.debug("{} bound", channelFuture.channel());

            // 调用关闭连接时,同步阻塞,等待关闭
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.debug("server error", e);
        } finally {
            // 关闭连接
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            log.debug("server stop");
        }
    }
}
/**
 * 粘包、半包演示
 */
@Slf4j
public class HelloWorldClient {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(worker);
            bootstrap.channel(NioSocketChannel.class);

            bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel socketChannel) throws Exception {
                    log.debug("connected...");
                    socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        // 客户端连接到服务器,执行此方法
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");

                            ByteBuf buffer = ctx.alloc().buffer();
                            // 客户端每次发送16字节,发送10次
                            for (int i = 0; i < 10; i++) {
                                // ByteBuf buffer = ctx.alloc().buffer();
                                buffer.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p'});

                                // 发送10次16字节的数据,演示粘包现象
                                // ctx.writeAndFlush(buffer);
                            }

                            // 一次发送160字节,演示半包现象
                            ctx.writeAndFlush(buffer);
                        }
                    });
                }
            });

            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888);

            // 异步等待连接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.debug("connect... fail", e);
        } finally {
            // 关闭连接
            worker.shutdownGracefully();
            log.debug("exit...");
        }
    }
}

2. 解决黏包、半包现象:短连接

  1. 黏包、半包现象的本质是TCP消息没有边界。消息发送的大小可能会受到应用层ByteBufTCP层滑动窗口等的影响。
  2. 可以采用短连接,解决黏包,但是短连接不能解决半包。短链接就是发送一次数据就断开。
/**
 * 短连接,只能解决粘包,不能解决半包的现象
 */
@Slf4j
public class HelloServer {
    public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        // 设置TCP缓冲区的大小
//        serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
        // 设置当前netty的缓冲区(ByteBuf)大小,最小是16
        serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16));
        serverBootstrap.group(new NioEventLoopGroup());
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
            }
        });

        serverBootstrap.bind(8080);
    }
}
public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        // 发送10次数据
        send();
    }
}

public static void send() {
    NioEventLoopGroup worker = new NioEventLoopGroup();
    try {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(worker);
        bootstrap.channel(NioSocketChannel.class);

        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel socketChannel) throws Exception {
                log.debug("connected...");
                socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                    // 客户端连接到服务器,执行此方法
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        log.debug("sending...");

                        ByteBuf buffer = ctx.alloc().buffer();
                        buffer.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p'});
                        ctx.writeAndFlush(buffer);

                        // 使用短连接,发送完数据就断开连接,解决黏包现象
                        ctx.close();
                    }
                });
            }
        });

        ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888);

        // 异步等待连接
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("connect... fail", e);
    } finally {
        // 关闭连接
        worker.shutdownGracefully();
        log.debug("exit...");
    }
}

3. 解决黏包、半包现象:定长解码器

  1. 让所有数据包的长度固定。(假设长度为10字节)服务器端加入FixedLengthFrameDecoder(10)定长解码器。
  2. 服务器端:
// 加入一个黏包、半包的定长处理器,每次处理10字节
socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 添加一个输出日志打印
socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));

4. 解决黏包、半包现象:固定分隔符

  1. 使用换行符处理器对消息进行分割。LineBasedFrameDecoder(1024),构造参数含义是:超过多少字节没有找到固定分隔符就报错。
  2. 也可以自定义固定分隔符DelimiterBasedFrameDecoder
// 处理黏包、半包,已一个分隔符进行区分,使用换行符解码器
// LineBasedFrameDecoder:构造参数是 在多少字节还没有遇到分隔符就报错
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
// 添加一个输出日志打印
socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));

5. 解决黏包、半包现象:预设数据的长度

  1. 使用LengthFieldBasedFrameDecoder,将发送的数据分为 数据的长度 + 数据
  2. 在LengthFieldBasedFrameDecoder中5个参数:
  • 参数1:maxFrameLength,一次最大处理长度
  • 参数2:lengthFieldOffset,长度的偏移量,从第几个字节开始解析
  • 参数3:lengthFieldLength,表示前几个字节是代表数据长度的
  • 参数4:lengthAdjustment,读取数据的偏移量(如果在代表数据长度和数据之间,加入了其他,例如版本号,就从偏移量开始读取数据)
  • 参数5:initialBytesToStrip,解析后的结果需要剥离几个字节(比如,解析之后不想要前面几个代表数据的字节,只保留数据)
/**
 * 使用 内容长度 + 内容 的方式,解决黏包、半包问题
 */
public class LengthFieldDecoderTest {
    public static void main(String[] args) {
        // 使用EmbeddedChannel进行测试
        EmbeddedChannel channel = new EmbeddedChannel(
                // 加入一个预设长度的解析器:
                // 参数1:一次最大处理长度
                // 参数2:长度的偏移量,从第几个字节开始解析
                // 参数3:表示前几个字节是代表数据长度的
                // 参数4:读取数据的偏移量(如果在代表数据长度和数据之间,加入了其他,例如版本号,就从偏移量开始读取数据)
                // 参数5:解析后的结果需要剥离几个字节(比如,解析之后不想要前面几个代表数据的字节,只保留数据)
                new LengthFieldBasedFrameDecoder(1024, 0, 4, 1, 4),
                // 打印日志处理器
                new LoggingHandler(LogLevel.DEBUG)
        );

        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        String str1 = "Hello, World";
        String str2 = "Hello, nihao";

        fillData(buffer, str1);
        fillData(buffer, str2);

        // 发送数据
        channel.writeInbound(buffer);
    }

    /**
     * 填充数据
     * @param buffer
     * @param data
     */
    private static void fillData(ByteBuf buffer, String data) {
        byte[] bytes = data.getBytes();
        // 获取发送数据的字节长度
        int len = bytes.length;

        // 发送的数据:前四个字节(int) 是发送数据的长度,后面是数据
        // 前四个字节,表示数据的长度
        buffer.writeInt(len);
        // 在数据长度和数据之间,加入一个版本号
        buffer.writeByte(1);
        // 后面是数据
        buffer.writeBytes(bytes);
    }
}

协议设计与解析

1. 使用redis协议向redis服务器发送数据

  1. redis有自己的通信协议。我们可以利用redis的通信协议,向redis服务器发送数据。
  2. 代码演示:
/**
 * 遵循redis的协议,与redis服务器通信
 */
public class RedisAgreementClient {
    // 定义一个回车换行符 \n,13代表回车,10代表换行
    private static final byte[] LINE = {13, 10};

    public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        bootstrap.group(worker);
        bootstrap.channel(NioSocketChannel.class);
        ChannelFuture channelFuture = null;
        try {
            channelFuture = bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    // 添加日志处理
                    nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        // 与服务器建立连接后
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 向redis服务器发送 set name zhangsan
                            set(ctx);
                        }

                        // 读取redis服务器发送回来的数据
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf buf = (ByteBuf) msg;
                            System.out.println(buf.toString(Charset.defaultCharset()));
                        }
                    });
                }
            })
                    // 连接redis客户端
                    .connect("192.168.122.1", 6379)
                    .sync();

            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 关闭客户端
            worker.shutdownGracefully();
        }
    }

    /**
     * 向redis服务器发送数据
     * @param ctx
     */
    public static void set(ChannelHandlerContext ctx) {
        ByteBuf buffer = ctx.alloc().buffer();
        // 使用redis的协议,想redis服务器set数据

        // 表示有三个参数 set name zhangsan
        buffer.writeBytes("*3".getBytes());
        // 加入一个回车换行
        buffer.writeBytes(LINE);
        // 表示第一个参数 set 有3个字节
        buffer.writeBytes("$3".getBytes());
        buffer.writeBytes(LINE);
        // 写入3字节
        buffer.writeBytes("set".getBytes());
        buffer.writeBytes(LINE);
        // 表示第二个参数 name 有4个字节
        buffer.writeBytes("$4".getBytes());
        buffer.writeBytes(LINE);
        // 写入4字节
        buffer.writeBytes("name".getBytes());
        buffer.writeBytes(LINE);
        // 表示第三个参数 zhangsan 有8个字节
        buffer.writeBytes("$8".getBytes());
        buffer.writeBytes(LINE);
        // 写入8字节数据
        buffer.writeBytes("zhangsan".getBytes());
        buffer.writeBytes(LINE);

        Channel channel = ctx.channel();
        // 写到redis服务器
        channel.writeAndFlush(buffer);
    }
}

2. 解析HTTP协议

  1. netty中解析HTTP的处理器是HttpServerCodec,同时实现了入站和出站处理器。
  2. SimpleChannelInboundHandler可以只关心我们想要的handler处理过后的类型。
  3. 代码演示:
/**
 * 解析HTTP协议,服务器
 */
@Slf4j
public class HttpAgreementService {

    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss, worker);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    // 加入一个HTTP请求的handler编解码器:既是入站处理器(处理器请求),也是出站处理器(处理响应)
                    nioSocketChannel.pipeline().addLast(new HttpServerCodec());

                    // 如果我们只想关心处理器传过来的是我们想要的类型
                    // 可以使用SimpleChannelInboundHandler,只关心HttpRequest类型
                    nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) throws Exception {
                            // 获取uri
                            String uri = httpRequest.uri();
                            log.debug(uri);

                            // 返回响应:设置协议版本号、响应状态码
                            DefaultFullHttpResponse response = new DefaultFullHttpResponse(httpRequest.protocolVersion(), HttpResponseStatus.OK);

                            // 写回响应体
                            byte[] bytes = "<h1>Hello World</h1>".getBytes();
                            // 设置响应体的长度
//                            response.headers().setInt(CONTENT_LENGTH, bytes.length);
                            response.content().writeBytes(bytes);

                            // 写到通道,经过ttp的编解码处理器,也是一个出站处理器
                            channelHandlerContext.writeAndFlush(bytes);
                        }
                    });

                    // 自定义handler,查看http编解码器传过来的
//                    nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//                        @Override
//                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//                            // 查看Http编解码器传过来的是什么对象
//                            log.debug("{}", msg.getClass());
//
//                            // Http编解码器将请求封装成了两个对象
//                            // 请求头 + 请求协议:DefaultHttpRequest
//                            // 请求体:LastHttpContent(get请求请求体为空)
//                        }
//                    });
                }
            });

            // 异步连接
            ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
            // 异步释放连接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.debug("server error ", e);
        } finally {
            // 关闭连接
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
  1. 使用游览器访问8888端口,就可以对http协议解析。

3. 自定义协议

  1. 自定义协议的要素:
  • 魔数,用来在第一时间判定是否是无效数据包
  • 版本号,可以支持协议的升级
  • 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
  • 指令类型,是登录、注册、单聊、群聊… 跟业务相关
  • 请求序号,为了双工通信,提供异步能力
  • 正文长度
  • 消息正文
  1. 自定义一个协议Message的编解码消息处理器
/**
 * 自定义协议 编解码的协议
 * 将ByteBuf转化为业务的自定义的Message类
 * 既能做入站也能做出站
 */
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {

    // 出站的时候将message编码成ByteBuf
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
        // 自定义协议
        // 4字节 定义魔数
        byteBuf.writeBytes(new byte[]{1,2,3,4});
        // 1字节 的协议版本
        byteBuf.writeByte(1);
        // 1字节表示,正文的编码类型 0表示java的序列化,1表示json
        byteBuf.writeByte(0);
        // 1字节的消息类型,自定义的
        byteBuf.writeByte(message.getMessageType());
        // 4字节的请求序号
        byteBuf.writeInt(message.getSequenceId());

        // 为了严谨,使得固定的字节数是2的整数倍,在加一个字节,对齐填充
        byteBuf.writeByte(0xff);

        // 将Message对象(序列化)转化为字节数组
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(message);

        byte[] bytes = bos.toByteArray();
        // 4字节 正文的长度
        byteBuf.writeInt(bytes.length);

        // 写入正文
        byteBuf.writeBytes(bytes);
    }

    // 入站解码,将ByteBuf解码成Message
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        // 对自定义的协议进行解码
        // 读取魔数 4字节
        int magicNum = byteBuf.readInt();
        // 读取版本号 1字节
        byte version = byteBuf.readByte();
        // 序列化算法的类型 1字节
        byte serializerType = byteBuf.readByte();
        // 消息类型 1字节
        byte messageType = byteBuf.readByte();
        // 请求序列号 4字节
        int sequenceId = byteBuf.readInt();
        // 对齐填充,无意义 1字节
        byteBuf.readByte();

        // 读取内容长度 4字节
        int length = byteBuf.readInt();
        byte[] bytes = new byte[length];
        // 读取内容到字节数组中,我们使用的是jdk序列化
        byteBuf.readBytes(bytes, 0, length);
        // 反序列成对象
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();

        log.debug("{},{},{},{},{},{}", magicNum, version, serializerType, messageType, sequenceId, length);
        log.debug("{}", message);

        // 解析出来一条消息,给下一个handler用
        list.add(message);
    }
}
  1. 测试
/**
 * 自定义协议编解码测试
 */
public class MessageCodecTest {
    public static void main(String[] args) throws Exception {
        EmbeddedChannel channel = new EmbeddedChannel(
                new LoggingHandler(),
                // 处理半包黏包的处理器
                new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0),
                new MessageCodec()
        );

        // 创建一个登录信息
        LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123", "张三");
        channel.writeOutbound(message);

        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        new MessageCodec().encode(null, message, buffer);

        // 演示黏包和半包,将buf切分成
        // 切前面100个字节
        ByteBuf s1 = buffer.slice(0, 100);
        // 切100字节到最后
        ByteBuf s2 = buffer.slice(100, buffer.readableBytes() - 100);

        // 将引用计数器+1
        s1.retain();

        // 入站
        channel.writeInbound(s1);
        channel.writeInbound(s2);
    }
}

在这里插入图片描述

4. @Sharable

  1. @Sharable注解只是一个标记,如果一个handler上标记了注解,证明已经充分考虑了线程安全性
  2. 比如LoggingHandler只是记录日志,可以被多个Pipele使用,所以可以标记为Sharable。
  3. 但是LengthFieldBasedFrameDecoder基于长度的解码器,解决粘包、半包的handler,如果没有解析到一条完整的消息,就会先把解析的消息保存下来,就不是线程安全的。
  4. 所以编解码的ByteToMessageCodec处理器,不能加上Sharable。在构造器上进行了限制。
  5. 要使用MessageToMessageCodec就可以加上Sharable注解。

5. 聊天服务器

@Slf4j
public class ChatService {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        NioEventLoopGroup boss = new NioEventLoopGroup();

        // 线程安全的handler加了Sharable注解,多个channel可以共用一个
        LoggingHandler LOGGING_HANDLER = new LoggingHandler();
        // 编解码自定义协议
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss, worker);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);
                }
            });

            ChannelFuture future = serverBootstrap.bind(8080).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.error("服务器异常", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

6. 聊天客户端和CountDownLatch

  1. CountDownLatch可以使用多个线程之间通信
// 为0时,会唤醒await
CountDownLatch down = new CountDownLatch(1);

// 线程1等待
down.await();

// 线程2conutDown唤醒await的线程1
down.countDown();
@Slf4j
public class ChatClient {
    public static void main(String[] args) {
        NioEventLoopGroup group = new NioEventLoopGroup();

        // 线程安全的handler加了Sharable注解,多个channel可以共用一个
        LoggingHandler LOGGING_HANDLER = new LoggingHandler();
        // 编解码自定义协议
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();

        // CountDownLatch减为0时,唤醒await()线程
        CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);
        // 判断是否登录成功
        AtomicBoolean LOGIN_SUCCESS = new AtomicBoolean();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
//                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);

                    ch.pipeline().addLast(new SimpleChannelInboundHandler<LoginResponseMessage>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, LoginResponseMessage response) throws Exception {
                            log.debug("{}", response);
                            // 读取登录返回信息
                            boolean success = response.isSuccess();
                            LOGIN_SUCCESS.set(success);
                            // CountDownLatch减为0,就会唤醒await
                            WAIT_FOR_LOGIN.countDown();
                        }
                    });
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 建立连接之后,触发active事件
                            new Thread(() -> {
                                // 进行登录
                                Scanner scanner = new Scanner(System.in);
                                System.out.println("请输入用户名:");
                                String username = scanner.nextLine();
                                System.out.println("请输入密码:");
                                String password = scanner.nextLine();

                                LoginRequestMessage message = new LoginRequestMessage(username, password);
                                ctx.writeAndFlush(message);

                                try {
                                    // 发送登录信息之后,等待服务器端是否登录成功
                                    WAIT_FOR_LOGIN.await();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }

                                // 判断是否登录成功
                                boolean success = LOGIN_SUCCESS.get();
                                if(!success) {
                                    // 登录失败
                                    System.out.println("登录失败");
                                    ctx.channel().close();
                                    return;
                                }

                                System.out.println("登录成功");

                                // 登录成功
                                while(true) {
                                    System.out.println("==================================");
                                    System.out.println("send [username] [content]");
                                    System.out.println("gsend [group name] [content]");
                                    System.out.println("gcreate [group name] [m1,m2,m3...]");
                                    System.out.println("gmembers [group name]");
                                    System.out.println("gjoin [group name]");
                                    System.out.println("gquit [group name]");
                                    System.out.println("quit");
                                    System.out.println("==================================");

                                    Scanner sca = new Scanner(System.in);
                                    String msg = sca.nextLine();
                                }
                            }, "system-in").start();
                            super.channelActive(ctx);
                        }
                    });
                }
            });

            ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.error("client error", e);
        } finally {
            group.shutdownGracefully();
        }
    }
}

7. 服务器端检测客户端连接断开

  1. 客户端断开连接,channl.close();
  2. 服务器端写handler进行处理,判断是正常断开还是异常断开,但是不管那种断开,都会触发channelInactive()事件。
@Slf4j
@ChannelHandler.Sharable
public class QuitChatMessageHandler extends ChannelInboundHandlerAdapter {
    /**
     * 正常断开 或 异常断开,都会触发channelInactive事件
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        SessionFactory.getSession().unbind(ctx.channel());
        log.debug("断开连接:{}", ctx.channel());
    }

    // 异常断开
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//        SessionFactory.getSession().unbind(ctx.channel());
        log.debug("异常断开连接:{}", ctx.channel());
    }
}

8. 连接假死,心跳检测

  1. netty提供IdelStateHandler检测读空闲事件,写空闲事件
  2. 服务器端,检测读空闲事件,客户端没有发送数据就断开链接,避免占用服务器资源。
// 加入空闲检测:8秒还没有读取到数据,就会触发IdleState.READ_IDLE事件
ch.pipeline().addLast(new IdleStateHandler(8, 0, 0));
ch.pipeline().addLast(new ChannelDuplexHandler() {
    // 用来触发特殊事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 触发读空闲事件
        IdleStateEvent event = (IdleStateEvent) evt;
        if (event.state() == IdleState.READER_IDLE) {
            System.out.println("8秒钟没有收到读事件, 可能网络故障了,断开连接");
            ctx.channel().close();
        }
    }
});
  1. 客户端代码,发送心跳,证明客户端正常。
// 发送心跳数据:5秒还没写入数据
ch.pipeline().addLast(new IdleStateHandler(0, 5, 0));
// ChannelDuplexHandler同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
    // 用来触发特殊事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        IdleStateEvent event = (IdleStateEvent) evt;
        if(IdleState.WRITER_IDLE == event.state()) {
            // 触发了空闲事件
//                                System.out.println("5秒没有写入数据了,发送一个心跳包");
            // 发送一个心跳包
            ctx.writeAndFlush(new PingMessage());
        }
    }
});

参数调优

1. CONNECT_TIMEOUT_MILLS

  1. 客户端连接服务器超时时间。
  2. 在指定时间内客户端没有连接到服务器,就会报出ConnectTimeOutException异常。
  3. 但是如果确定服务器端没有开启,可能没到指定的连接超时时间就会报错,报java.net的ConnectException
public class TestConnectTimeOut {
    public static void main(String[] args) throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();

        /**
         * 设置连接超时的参数
         * 如果在服务端设置:option()设置ServerSocketChannel的参数,childOption()设置客户端的参数
         */
        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(new NioEventLoopGroup());
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new LoggingHandler());
            }
        });

        ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
        future.channel().closeFuture().sync();
    }
}

2. SO_BACKLOG

  1. TCP三次握手
    在这里插入图片描述

  2. 在Linux2.2之前,backlog参数的大小包括两个队列(半连接队列,全连接队列)的大小。(全连接队列就代表了这个服务器的连接客户端上限,如果全连接队列满了,server将发送一个拒绝连接的错误信息到client

  3. Linux2.2之后,linux有两个系统配置文件,配置这两个参数的大小。

  4. 半连接队列(sync queue)大小通过/proc/sys/net/ipv4/tcp_max_syn_backlog指定。在synccookies启用的情况下,逻辑上没有最大限制,这个设置便被忽略。

  5. 全连接队列(accept queue)通过/proc/sys/net/core/somaxconn指定,在客户端连接时,内核会根据传入的backlog参数,与系统配置中的参数,取二者比较小的值

  6. Linux系统通过ss -lnt查看全连接队列的大小。

在这里插入图片描述

  1. ServerSocket在构造函数中执行
    在这里插入图片描述

  2. netty中服务器端通过option()设置参数。

  3. 演示全连接队列满,测试服务器端:(服务器端要用debug模式启动)

/**
 * 测试全连接队列backlog参数
 * windows环境下,没有Linux系统的全连接配置文件,所以客户端设置多大,就是多大
 */
@Slf4j
public class TestBacklogServer {
    public static void main(String[] args) {
        ServerBootstrap bootstrap = new ServerBootstrap();
        /**
         * 通过SO_BACKLOG设置全连接队列的大小
         * 但是不好进行演示,连接建立后,加入全连接队列
         * 调用accept()之后,就会从队列中移除。
         * 所以Netty要在NioEventLoop的accept处打一个断点
         * 建立三次连接之后,就保持在全连接队列中
         */
        bootstrap.option(ChannelOption.SO_BACKLOG, 2);
        bootstrap.group(new NioEventLoopGroup());
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new LoggingHandler());
            }
        });

        bootstrap.bind(8080);
    }
}
  1. 测试客户端
/**
 * 测试全连接队列客户端
 */
@Slf4j
public class TestBacklogClient {
    public static void main(String[] args) throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(new NioEventLoopGroup());
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new LoggingHandler());
            }
        });

        ChannelFuture future = bootstrap.connect("localhost", 8080);
        future.sync().channel().closeFuture();
    }
}
  1. NioEventLoop中的accept()处打上断点,如果调用了accept()方法,那么就会从全连接队列中移除,无法演示。
    在这里插入图片描述

  2. 第3个客户端连接上,就会抛出连接异常ConnectException

3. ALLOCATOR

  1. 决定ByteBuf池化还是非池化直接内存还是堆内存

  2. netty默认是池化的直接内存ByteBuf
    在这里插入图片描述

  3. 如何设置是否池化,或者使用直接内存。跟踪源码实现。

  4. ByteBufUtil中进行分配。先读取配置信息,如果没有配置,就判断是否是安卓的操作系统,是就使用非池化,否则就是池化

  5. 配置非池化和只用堆内存:-Dio.netty.allocator.type=unpooled -Dio.netty.noPreferDirect=true

String allocType = SystemPropertyUtil.get(
                "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();

ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
    alloc = UnpooledByteBufAllocator.DEFAULT;
    logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
    alloc = PooledByteBufAllocator.DEFAULT;
    logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
    alloc = PooledByteBufAllocator.DEFAULT;
    logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}

DEFAULT_ALLOCATOR = alloc;

4. RCVBUF_ALLOCATOR

  1. 控制Netty的接收缓冲区的大小,负责对ByteBuf的大小进行分配。
  2. ByteBufAllocator决定了缓冲区是池化还是非池化,但是大小的分配由RecvByteBufAllocator决定。并且IO操作的ByteBuf,强制使用直接内存。因为直接内存少一次在jvm虚拟机的内存复制,效率较高
    在这里插入图片描述

RPC

1. RPC请求、响应消息

  1. RPC请求需要知道的几个东西:
调用的接口的全限定类名 String interfaceName
调用接口中的方法 String methodName
方法的返回值类型 Class<?> returnType
方法的请求参数类型 Class[] paramterTypes
方法参数的数组 Objectp[] paramterValue
/**
 * @author yihang
 */
@Getter
@ToString(callSuper = true)
public class RpcRequestMessage extends Message {

    /**
     * 调用的接口全限定名,服务端根据它找到实现
     */
    private String interfaceName;
    /**
     * 调用接口中的方法名
     */
    private String methodName;
    /**
     * 方法返回类型
     */
    private Class<?> returnType;
    /**
     * 方法参数类型数组
     */
    private Class[] parameterTypes;
    /**
     * 方法参数值数组
     */
    private Object[] parameterValue;

    public RpcRequestMessage(int sequenceId, String interfaceName, String methodName, Class<?> returnType, Class[] parameterTypes, Object[] parameterValue) {
        super.setSequenceId(sequenceId);
        this.interfaceName = interfaceName;
        this.methodName = methodName;
        this.returnType = returnType;
        this.parameterTypes = parameterTypes;
        this.parameterValue = parameterValue;
    }

    @Override
    public int getMessageType() {
        return RPC_MESSAGE_TYPE_REQUEST;
    }
}
  1. 响应消息
返回值 returnValue
异常值 Exception
/**
 * @author yihang
 */
@Data
@ToString(callSuper = true)
public class RpcResponseMessage extends Message {
    /**
     * 返回值
     */
    private Object returnValue;
    /**
     * 异常值
     */
    private Exception exceptionValue;

    @Override
    public int getMessageType() {
        return RPC_MESSAGE_TYPE_RESPONSE;
    }
}

2. RPC调用

  1. 创建一个需要远程调用的接口和实现类。
public interface HelloService {
    String sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String name) {
        return "你好" + name;
    }
}
  1. 服务器端,使用反射调用方法。
@Slf4j
@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage msg) throws Exception {
        RpcResponseMessage response = new RpcResponseMessage();
        try {
            // 通过反射进行远程调用
            HelloService service = (HelloService) ServicesFactory.getService(Class.forName(msg.getInterfaceName()));
            Method method = service.getClass().getMethod(msg.getMethodName(), msg.getParameterTypes());
            Object invoke = method.invoke(service, msg.getParameterValue());

            response.setReturnValue(invoke);

            ctx.writeAndFlush(response);
        } catch (Exception e) {
            log.error("rpc error", e);
            response.setExceptionValue(e);
        }
    }
}
  1. 客户端发起远程调用
ChannelFuture future = bootstrap.connect("localhost", 8080);
Channel channel = future.sync().channel();

// 连接建立之后,发起远程调用
RpcRequestMessage request = new RpcRequestMessage(Message.RPC_MESSAGE_TYPE_REQUEST,
        "netty2.rpc.service.HelloService",
        "sayHello",
        String.class,
        new Class[]{String.class},
        new Object[]{"张三"});

ChannelFuture writeFuture = channel.writeAndFlush(request);

3. Gson转化Class问题

  1. Gson序列化Class的时候,序列化失败,但是客户端没有打印异常信息,因为WriteAndFlush是另一个线程调用,监控改线程返回的Promise判断错误信息。
ChannelFuture future = bootstrap.connect("localhost", 8080);
Channel channel = future.sync().channel();

// 连接建立之后,发起远程调用
RpcRequestMessage request = new RpcRequestMessage(Message.RPC_MESSAGE_TYPE_REQUEST,
        "netty2.rpc.service.HelloService",
        "sayHello",
        String.class,
        new Class[]{String.class},
        new Object[]{"张三"});

ChannelFuture writeFuture = channel.writeAndFlush(request);
writeFuture.addListener(promise -> {
    // 如果数据发送失败,打印错误信息
    if (!promise.isSuccess()) {
        Throwable cause = promise.cause();
        log.debug("发送数据错误,", cause);
    }
});
channel.closeFuture().sync();
  1. 自定义Gson对Class的序列化、反序列化
public class TestJson {
    public static void main(String[] args) {
        Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCoder()).create();
        System.out.println(gson.toJson(String.class));
    }

    /**
     * Gson将Class类进行json序列化的时候,会报错
     * 自定义Class的序列化、反序列化
     */
    static class ClassCoder implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>> {

        @Override
        public Class<?> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
            try {
                String str = jsonElement.getAsString();
                // 将全限定类名转为Class类
                return Class.forName(str);
            } catch (ClassNotFoundException e) {
                throw new JsonParseException(e);
            }
        }

        @Override
        public JsonElement serialize(Class<?> aClass, Type type, JsonSerializationContext jsonSerializationContext) {
            // 将Class类 -> 转为全限定类名
            return new JsonPrimitive(aClass.getName());
        }
    }
}

4. 使用动态代理改造RPC

  1. 问题分析:每次调用RPC都需要传入很多参数,封装对象,很不友好。
// 客户端每次远程调用,都要封装一个对象,不友好
channel.writeAndFlush(new RpcRequestMessage(Message.RPC_MESSAGE_TYPE_REQUEST,
        "netty2.rpc.service.HelloService",
        "sayHello",
        String.class,
        new Class[]{String.class},
        new Object[]{"张三"}));
  1. 改造成,调用远程方法,就像调用本地方法一样
// 设置成直接调用方法,就可以实现远程调用
HelloService service = null;
service.sayHello("张三");
  1. 使用动态代理对方法进行封装
@Slf4j
public class RpcClientManager {
    private static volatile Channel channel = null;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // getProxyService获取一个代理类
        HelloService service = getProxyService(HelloService.class);
        service.sayHello("张三");
        service.sayHello("李四");
    }

    /**
     * 为一个接口,创建一个代理对象,自动封装成RpcRequestMessage对象
     * @param clazz
     * @param <T>
     * @return
     */
    private static <T> T getProxyService(Class<T> clazz) {
        ClassLoader classLoader = clazz.getClassLoader();
        Class<?>[] interfaces = new Class[]{clazz};
        Object o = Proxy.newProxyInstance(classLoader, interfaces, (proxy, method, args) -> {
            // 封装请求消息对象
            RpcRequestMessage requestMessage = new RpcRequestMessage(SequenceIdGenerator.nextId(),
                    clazz.getName(),
                    method.getName(),
                    method.getReturnType(),
                    method.getParameterTypes(),
                    args);

            // 获取channel,发送数据
            getChannel().writeAndFlush(requestMessage);

            // 暂时返回一个null
            return null;
        });

        return (T) o;
    }

    /**
     * 获取Channel对象
     * @return
     */
    public static Channel getChannel() {
        // Channel只能加载一次,所以使用单例的双重检查
        if(channel == null) {
            synchronized (lock) {
                if(channel == null) {
                    initChannel();
                    return channel;
                }
            }
        }

        return channel;
    }

    /**
     * 初始化channel
     */
    private static void initChannel() {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group);
            bootstrap.channel(NioSocketChannel.class);

            ProcotolFrameDecoder procotolFrameDecoder = new ProcotolFrameDecoder();
            LoggingHandler loggingHandler = new LoggingHandler();
            MessageCodecSharable messageCodecSharable = new MessageCodecSharable();
            RpcResponseMessageHandler rpcResponseHandler = new RpcResponseMessageHandler();

            bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    // 粘包半包处理
                    ch.pipeline().addLast(procotolFrameDecoder);
                    ch.pipeline().addLast(loggingHandler);
                    // 协议编码
                    ch.pipeline().addLast(messageCodecSharable);
                    ch.pipeline().addLast(rpcResponseHandler);
                }
            });

            channel = bootstrap.connect("localhost", 8080).sync().channel();
            // 如果调用了channel的close操作,异步关闭eventLoop
            channel.closeFuture().addListener(future -> {
                group.shutdownGracefully();
            });
        } catch (Exception e) {
            log.error("客户端出错,", e);
        }
    }
}

5. 异步接收结果

  1. 调用sayHello方法是在主线程,而返回远程调用结果是在nio线程。两个线程之间的通信可以使用Promise
  2. 在RpcResponseMessageHandler加入一个填充Promise消息的map
@ChannelHandler.Sharable
@Slf4j
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
    // 响应填充结果的Promise的Map
    public static final Map<Integer, Promise<Object>> MAP = new ConcurrentHashMap<>();

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {
        // 判断远程调用结果
        Object returnValue = msg.getReturnValue();
        Exception exceptionValue = msg.getExceptionValue();

        // 取出响应消息对应的Promise,填充结果
        Promise<Object> promise = MAP.remove(msg.getSequenceId());
        if(exceptionValue != null) {
            promise.setFailure(exceptionValue);
        } else {
            promise.setSuccess(returnValue);
        }

        log.debug("{}", msg);
    }
}
  1. 调用方的Promise阻塞,等待nio线程返回数据。
@Slf4j
public class RpcClientManager {
    private static volatile Channel channel = null;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // getProxyService获取一个代理类
        HelloService service = getProxyService(HelloService.class);
        System.out.println(service.sayHello("张三"));
        System.out.println(service.sayHello("李四"));
    }

    /**
     * 为一个接口,创建一个代理对象,自动封装成RpcRequestMessage对象
     * @param clazz
     * @param <T>
     * @return
     */
    private static <T> T getProxyService(Class<T> clazz) {
        ClassLoader classLoader = clazz.getClassLoader();
        Class<?>[] interfaces = new Class[]{clazz};
        Object o = Proxy.newProxyInstance(classLoader, interfaces, (proxy, method, args) -> {
            // 封装请求消息对象
            int sequenceId = SequenceIdGenerator.nextId();
            RpcRequestMessage requestMessage = new RpcRequestMessage(sequenceId,
                    clazz.getName(),
                    method.getName(),
                    method.getReturnType(),
                    method.getParameterTypes(),
                    args);

            // 获取channel,发送数据
            getChannel().writeAndFlush(requestMessage);

            // 创建一个Promise,用于接收结果
            DefaultPromise<Object> promise = new DefaultPromise<>(getChannel().eventLoop());
            RpcResponseMessageHandler.MAP.put(sequenceId, promise);

            // 等待填充结果
            promise.await();
            if (promise.isSuccess()) {
                return promise.getNow();
            } else {
                throw new RuntimeException(promise.cause());
            }
        });

        return (T) o;
    }

    /**
     * 获取Channel对象
     * @return
     */
    public static Channel getChannel() {
        // Channel只能加载一次,所以使用单例的双重检查
        if(channel == null) {
            synchronized (lock) {
                if(channel == null) {
                    initChannel();
                    return channel;
                }
            }
        }

        return channel;
    }

    /**
     * 初始化channel
     */
    private static void initChannel() {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group);
            bootstrap.channel(NioSocketChannel.class);

            ProcotolFrameDecoder procotolFrameDecoder = new ProcotolFrameDecoder();
            LoggingHandler loggingHandler = new LoggingHandler();
            MessageCodecSharable messageCodecSharable = new MessageCodecSharable();
            RpcResponseMessageHandler rpcResponseHandler = new RpcResponseMessageHandler();

            bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    // 粘包半包处理
                    ch.pipeline().addLast(procotolFrameDecoder);
                    ch.pipeline().addLast(loggingHandler);
                    // 协议编码
                    ch.pipeline().addLast(messageCodecSharable);
                    ch.pipeline().addLast(rpcResponseHandler);
                }
            });

            channel = bootstrap.connect("localhost", 8080).sync().channel();
            // 如果调用了channel的close操作,异步关闭eventLoop
            channel.closeFuture().addListener(future -> {
                group.shutdownGracefully();
            });
        } catch (Exception e) {
            log.error("客户端出错,", e);
        }
    }
}

源码分析

1. Netty中bind()分析

  1. 连接主要做的几件事:会创建一个NioServerSocketChannel,作为一个附件传递到ServerSocketChannel,客户端的连接事件,都由NioServerSocketChannel建立连接。
创建一个NioServerSocketChannel
NioServerSockerChannel nioSsc = new NioServerSocketChannel();
创建一个服务器
ServerSocketChannel ssc = new ServerSocketChannel();
创建一个多路复用器(这一步是在NioEventLoop中完成)
Selection selection = Selection.open();
服务器注册到多路复用器
SelectionKey key = ssc.register(selection, 0, nioSsc);
绑定端口
ssc.bind(8080, backlog);
监听客户端的连接事件
key.interestOps(SelectlionKey.OP_ACCEPT);
  1. 源码分析
    在这里插入图片描述
private ChannelFuture doBind(final SocketAddress localAddress) {
		// init初始化和注册
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        if (regFuture.isDone()) {
        	// 注册完成、成功
            // At this point we know that the registration was complete and successful.
            ChannelPromise promise = channel.newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
        	// 未注册成功,再次注册
            // Registration future is almost always fulfilled already, but just in case it's not.
            final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
            // 使用另一个线程进行注册和发起bind
            regFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    Throwable cause = future.cause();
                    if (cause != null) {
                        // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                        // IllegalStateException once we try to access the EventLoop of the Channel.
                        promise.setFailure(cause);
                    } else {
                        // Registration was successful, so set the correct executor to use.
                        // See https://github.com/netty/netty/issues/2586
                        promise.registered();

                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

在这里插入图片描述

在这里插入图片描述

protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
            	// ServerSocketChannel注册一个多路复用器,将NioServerSocketChannel作为附件,进行关联,selectionKey上发生的accept事件,由附件进行连接操作。
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
  1. 完整流程

在这里插入图片描述

2. NioEventLoop

  1. NioEventLoop里面有一个单线程,一个Select,一个任务队列
  2. 继承了线程池的定时任务,可以执行定时任务。单线程如果有多个任务,所有需要任务队列。
  3. NioEventLoop底层使用的SelectKey进行了重写,原生的SelectKey是基于Set的,Set是基于Map的,遍历,需要比较hash,有冲突需要遍历链表。Netty重写了一个基于数组的,替换了原生SelectKey中的参数,提高了遍历效率。
private SelectorTuple openSelector() {
        final Selector unwrappedSelector;
        try {
        	// 创建一个select
            unwrappedSelector = provider.openSelector();
        } catch (IOException e) {
            throw new ChannelException("failed to open a new selector", e);
        }

        if (DISABLE_KEY_SET_OPTIMIZATION) {
            return new SelectorTuple(unwrappedSelector);
        }

        Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    return Class.forName(
                            "sun.nio.ch.SelectorImpl",
                            false,
                            PlatformDependent.getSystemClassLoader());
                } catch (Throwable cause) {
                    return cause;
                }
            }
        });

        if (!(maybeSelectorImplClass instanceof Class) ||
            // ensure the current selector implementation is what we can instrument.
            !((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) {
            if (maybeSelectorImplClass instanceof Throwable) {
                Throwable t = (Throwable) maybeSelectorImplClass;
                logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t);
            }
            return new SelectorTuple(unwrappedSelector);
        }

        final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
        // 准备一个数组实现的SelectKey
        final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

        Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                	// 获取原来的selectKey参数,原来的SelectKey是基于Set的,Set底层是基于Map的,遍历,
                	// 要比较hash,有冲突还要遍历链表,效率较低,Netty实现了一个基于数组的,遍历效率高。
                    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

                    if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
                        // Let us try to use sun.misc.Unsafe to replace the SelectionKeySet.
                        // This allows us to also do this in Java9+ without any extra flags.
                        long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField);
                        long publicSelectedKeysFieldOffset =
                                PlatformDependent.objectFieldOffset(publicSelectedKeysField);

                        if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) {
                            PlatformDependent.putObject(
                                    unwrappedSelector, selectedKeysFieldOffset, selectedKeySet);
                            PlatformDependent.putObject(
                                    unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet);
                            return null;
                        }
                        // We could not retrieve the offset, lets try reflection as last-resort.
                    }

                    Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }
                    cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }

					// 替换原生SelectKey的两个参数
                    selectedKeysField.set(unwrappedSelector, selectedKeySet);
                    publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                    return null;
                } catch (NoSuchFieldException e) {
                    return e;
                } catch (IllegalAccessException e) {
                    return e;
                }
            }
        });

        if (maybeException instanceof Exception) {
            selectedKeys = null;
            Exception e = (Exception) maybeException;
            logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e);
            return new SelectorTuple(unwrappedSelector);
        }
        selectedKeys = selectedKeySet;
        logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
        return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
    }
  1. select()空轮询,JDK在Linux系统下的select()会出现空轮询bug,select方法不会阻塞住,程序会一直在for循环中运行,导致CPU100%。
  2. Netty如何进行解决:
int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);

SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;

private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

            for (;;) {
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }

                // If a task was submitted when wakenUp value was true, the task didn't get a chance to call
                // Selector#wakeup. So we need to check task queue again before executing select operation.
                // If we don't, the task might be pended until select operation was timed out.
                // It might be pended until idle timeout if IdleStateHandler existed in pipeline.
                if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

				// 可能出现空轮询bug的地方
                int selectedKeys = selector.select(timeoutMillis);
                // 通过一个计数,如果计数快速增加超过了一个阈值,说明出现了空轮询bug
                selectCnt ++;

                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    // - Selected something,
                    // - waken up by user, or
                    // - the task queue has a pending task.
                    // - a scheduled task is ready for processing
                    break;
                }
                if (Thread.interrupted()) {
                    // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
                    // As this is most likely a bug in the handler of the user or it's client library we will
                    // also log it.
                    //
                    // See https://github.com/netty/netty/issues/2426
                    if (logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely because " +
                                "Thread.currentThread().interrupt() was called. Use " +
                                "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                    }
                    selectCnt = 1;
                    break;
                }

                long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                    // timeoutMillis elapsed without anything selected.
                    selectCnt = 1;
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    // 如果selectCnt >= 阈值(默认512),说明出现了空轮询bug
                    // The code exists in an extra method to ensure the method is not too big to inline as this
                    // branch is not very likely to get hit very frequently.
                    // 重建一个select,替换掉以前的,并把以前的状态复制给新的
                    selector = selectRebuildSelector(selectCnt);
                    selectCnt = 1;
                    break;
                }

                currentTimeNanos = time;
            }

            if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                            selectCnt - 1, selector);
                }
            }
        } catch (CancelledKeyException e) {
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                        selector, e);
            }
            // Harmless exception - log anyway
        }
    }
  1. ioRatio参数是控制处理IO事件所占用时间的比例。NioEventLoop是一个单线程,它可以处理io事件,也可以处理普通的任务,如果普通任务执行时间较长,就会影响io事件的处理。
// 默认50%
private volatile int ioRatio = 50;

final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
    try {
    	// 处理SelectKey事件,io事件
        processSelectedKeys();
    } finally {
    	// 如果将ioRatio设置成100%,反而会将普通任务执行完毕,在执行io任务
        // Ensure we always run tasks.
        runAllTasks();
    }
} else {
    final long ioStartTime = System.nanoTime();
    try {
    	// 处理SelectKey事件,io事件
        processSelectedKeys();
    } finally {
        // Ensure we always run tasks.
        // io运行的时间
        final long ioTime = System.nanoTime() - ioStartTime;
        // 运行一个普通任务的时间,如果没有执行完,下一次从任务队列中取。
        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
    }
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值