Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >《跟闪电侠学Netty》阅读笔记 - 开篇入门Netty

《跟闪电侠学Netty》阅读笔记 - 开篇入门Netty

作者头像
阿东
发布于 2023-09-02 09:25:44
发布于 2023-09-02 09:25:44
56500
代码可运行
举报
运行总次数:0
代码可运行

引言

《跟闪电侠学Netty》 并不是个人接触的第一本Netty书籍,但个人更推荐读者把它作为作为第一本Netty入门的书籍。

《Netty In Action》 不同,这本书直接从Netty入门程序代码开始引入Netty框架,前半部分教你如何用Netty搭建简易的通讯系统,整体难度比较低,后半部分直接从服务端源码、客户端源码、ChannelPipeline开始介绍,和前半部分割裂较为严重。

相较于入门的程序,源码分析毫无疑问是比较有干货的部分,但是和前面入门程序相比有点学完了99乘法表就让你去做微积分的卷子一样,如果Netty使用生疏源码部分讲解肯定是十分难懂的,所以更建议只看前半截。

个人比较推荐这本书吃透Netty编写的简单通讯“项目”之后,直接去看《Netty In Action》做一个更为系统的深入和基础巩固。等《Netty In Action》看明白之后,再回过头来看《跟闪电侠学Netty》的源码分析部分。

抛开源码分析部分,这本书是“我奶奶都能学会”的优秀入门书籍,用代码实战加讲解方式学起来轻松印象深刻。

开篇入门部分先不引入项目,这里先对于过去JDK的网络IO模型作为引子介绍为什么我们需要用Netty,学习Netty带来的好处等。

思维导图

https://www.mubucm.com/doc/3eZpDZQHKMB

《跟闪电侠学Netty》阅读笔记 - 实战入门篇(一).png

Netty 依赖版本(4.1.6.Final)

本书使用的Netty版本为 4.1.6,为了避免后面阅读源码的时候产生误解,建议以此版本为基准。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.6.Final</version>
    </dependency>

JDK 原生编程模型

到目前为止,JDK一共实现了三种网络IO编程模型:BIO、NIO和AIO。三种模型不仅产生的间隔时间跨度大,并且由三组完全不同编程风格的开发人员设计API,不同编程模型和设计思路之间的切换十分复杂,开发者的学习成本也比较大。

针对这些问题,我们直接了解Netty如何统一这些模型以及如何降低并发编程的开发难度,这里先对过去的JDK网络IO编程模型做一个了解。

洗衣机案例理解阻塞非阻塞,同步异步概念

在了解JDK的网络IO模型之前,必须先了解的阻塞非阻塞同步异步的概念。

同步和异步指的是任务之间是否需要等待其它任务完成或者等待某个事件的发生。如果一个任务必须等待另一个任务完成才能继续执行,那么这两个任务就是同步的;如果一个任务可以直接继续执行而无需等待另一个任务的完成,那么这两个任务就是异步的。

阻塞和非阻塞指的是任务在等待结果时是否会一直占用CPU资源。如果一个任务在等待结果时会一直占用CPU资源,那么这个任务就是阻塞的;如果一个任务在等待结果时不会占用CPU资源,那么这个任务就是非阻塞的。

这里给一个生活中洗衣服的例子帮助完全没有了解过这些概念的读者加深印象,这个例子来源于某个网课,个人觉得十分贴切和易懂就拿过来用了。

同步阻塞

理解:

洗衣服丢到洗衣机,全程看着洗衣机洗完,洗好之后晾衣服。

类比 :

  • 请求接口
  • 等待接口返回结果,中间不能做其他事情。
  • 拿到结果处理数据

分析: 同步:全程看着洗衣机洗完。 阻塞:等待洗衣机洗好衣服之后跑过去晾衣服。

同步非阻塞

理解:

把衣服丢到洗衣机洗,然后回客厅做其他事情,定时看看洗衣机是不是洗完了,洗好后再去晾衣服。(等待期间你可以做其他事情,比如用电脑刷剧看视频)。

这种模式类似日常生活洗衣机洗衣服。

类比:

  • 请求接口。
  • 等待期间切换到其他任务,但是需要定期观察接口是否有回送数据。
  • 拿到结果处理数据。

分析:

和阻塞方式的最大区别是不需要一直盯着洗衣机,期间可以抽空干其他的事情。

同步:等待洗衣机洗完这个事情没有本质变化,洗好衣服之后还是要跑过去晾衣服。 非阻塞:拿到衣服之前可以干别的事情,只不过需要每次隔一段时间查看能不能拿到洗好的衣服。

异步阻塞

理解:

把衣服丢到洗衣机洗,然后看着洗衣机洗完,洗好后再去晾衣服(没这个情况,几乎没这个说法,可以忽略)。

类比:

  • 请求接口,不需要关心结果。
  • 客户端可以抽空干其他事情,但是非得等待接口返回结果
  • 拿到服务端的处理结果

分析:

难以描述,几乎不存在这种说法。

异步非阻塞

理解:

把衣服丢到洗衣机洗,然后回客厅做其他事情,洗衣机洗好后会自动去晾衣服,晾完成后放个音乐告诉你洗好衣服并晾好了

类比 :

  • 请求接口,此时客户端可以继续执行代码。
  • 服务端准备并且处理数据,在处理完成之后在合适的时间通知客户端
  • 客户端收到服务端处理完成的结果。

分析: 异步:洗衣机自己不仅把衣服洗好了还帮我们把衣服晾好了。 非阻塞:拿到“衣服”结果之前可以干别的事情。

注意异步非阻塞情况下,“我们”对待洗衣服这件事情的“态度”完全变了。

BIO 编程模型

BIO叫做阻塞IO模型,在阻塞IO模型中两个任务之间需要等待响应结果,应用进程需要等待内核把整个数据准备好之后才能开始进行处理。

BIO是入门网络编程的第一个程序,从JDK1.0开始便存在了,存在于java.net包当中。下面的程序也是入门Tomcat源码的基础程序。

image.png

Java实现代码

在BIO的实现代码中,服务端通过accept一直阻塞等待直到有客户端连接。首先是服务端代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static void main(String[] args) throws IOException {  
    ServerSocket serverSocket = new ServerSocket(8000);  
  
    // 接受连接  
    new Thread(() -> {  
        while (true) {  
            // 1. 阻塞获取连接  
            try {  
                Socket socket = serverSocket.accept();  
  
                // 2. 为每一个新连接使用一个新线程  
                new Thread(() -> {  
                    try {  
                        int len;  
                        byte[] data = new byte[1024];  
                        InputStream inputStream = socket.getInputStream();  
                        // 字节流读取数据  
                        while ((-1 != (len = inputStream.read()))) {  
                            System.err.println(new String(data, 0, len));  
                        }  
                    } catch (IOException ioException) {  
                        ioException.printStackTrace();  
                    }  
                }).start();  
            } catch (IOException e) {  
                e.printStackTrace();  
  
            }  
        }  
    }).start();  
}

较为核心的部分是serverSocket.accept()这一串代码,会导致服务端阻塞等待客户端的连接请求,即使没有连接也会一直阻塞。

服务端启动之后会监听8000端口,等待客户端连接,此时需要一直占用CPU资源,获取到客户端连接之将会开辟一个新的线程单独为客户端提供服务。

image.png

然后是客户端代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static void main(String[] args) {  
    new Thread(()->{  
        try {  
            Socket socket = new Socket("127.0.0.1", 8000);  
            while (true){  
                socket.getOutputStream().write((new Date() + ":"+ "hellow world").getBytes(StandardCharsets.ISO_8859_1));  
                try {  
                    Thread.sleep(2000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        } catch (IOException ioException) {  
            ioException.printStackTrace();  
        }  
    }).start();  
}

客户端的核心代码如下,通过建立Socket和服务端建立连接。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Socket socket = new Socket("127.0.0.1", 8000);
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Connected to the target VM, address: '127.0.0.1:5540', transport: 'socket'

客户端启动之后会间隔两秒发送数据给服务端,服务端收到请求之后打印客户端传递的内容。

image.png

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Connected to the target VM, address: '127.0.0.1:5548', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:5548', transport: 'socket'

Process finished with exit code 130

优缺点分析

传统的IO模型有如下优缺点:

  • 优点
    • 实现简单 。
    • 客户端较少情况下运行良好。
  • 缺点
    • 每次连接都需要一个单独的线程。
    • 单机单核心线程上下文切换代价巨大 。
    • 数据读写只能以字节流为单位。
    • while(true) 死循环非常浪费CPU资源 。
    • API 晦涩难懂,对于编程人员需要考虑非常多的内容。

结论:

在传统的IO模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个while死循环,那么1w个连接对应1w个线程,继而1w个while死循环。

单机是不可能完成同时支撑1W个线程的,但是在客户端连接数量较少的时候,这种方式效率很高并且实现非常简单。

NIO 编程模型

NIO 编程模型是 JDK1.4 出现的全新API,它实现的是同步非阻塞IO编程模型。以下面的模型为例,第二阶段依然需要等待结果之后主动处理数据,主要的区别在第一阶段(红线部分)轮询的时候可以干别的事情,只需多次调用检查是否有数据可以开始读取。

image.png

Java 实现代码

NIO编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个指定线程

概念上理解NIO并不难,但是要写出JDK的NIO编程模板代码却不容易。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static void main(String[] args) throws IOException {  
 Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        new Thread(() -> {
            try {
                // 对应IO编程中服务端启动
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(8000));
                listenerChannel.configureBlocking(false);
                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

                while (true) {
                    // 监测是否有新的连接,这里的1指的是阻塞的时间为1ms
                    if (serverSelector.select(1) > 0) {
                        Set<SelectionKey> set = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isAcceptable()) {
                                try {
                                    // (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                } finally {
                                    keyIterator.remove();
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }

        }).start();


        new Thread(() -> {
            try {
                while (true) {
                    // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为1ms
                    if (clientSelector.select(1) > 0) {
                        Set<SelectionKey> set = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isReadable()) {
                                try {
                                    SocketChannel clientChannel = (SocketChannel) key.channel();
                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                    // (3) 读取数据以块为单位批量读取
                                    clientChannel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                            .toString());
                                } finally {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }
        }).start();
}

上面的代码不需要过多纠结,NIO的代码模板确实非常复杂,我们可以把上面的两个线程看作是两个传送带,第一条传送带只负责接收外部的连接请求,收到请求数据之后直接丢给第二条传送带处理。第二条传送带收到任务之后进行解析和处理,最后把结果返回即可。

书中并没有给NIO的客户端案例,但是有意思的是Netty的客户端启动连接代码可以完美衔接JDK的NIO Server服务端,从这一点上可以发现Netty的NIO编程模型实际上就是对于JDK NIO模型的改良和优化。

PS:后续篇章的源码阅读可以看到Netty和JDK的API的关系密不可分。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static void main(String[] args) throws InterruptedException {  
    Bootstrap bootstrap = new Bootstrap();  
    NioEventLoopGroup eventExecutors = new NioEventLoopGroup();  
    // 引导器引导启动  
    bootstrap.group(eventExecutors)  
            .channel(NioSocketChannel.class)  
            .handler(new ChannelInitializer<Channel>() {  
                @Override  
                protected void initChannel(Channel channel) throws Exception {  
                    channel.pipeline().addLast(new StringEncoder());  
                }  
            });  
  
    // 建立通道  
    Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();  
  
    while (true){  
        channel.writeAndFlush(new Date() + " Hello world");  
        Thread.sleep(2000);  
    }  
}

Netty无论是客户端启动还是服务端启动都会打印一堆日志,下面是客户端启动日志。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
14:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.cacheTrimIntervalMillis: 0
14:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.useCacheForAllThreads: false
14:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxCachedByteBuffersPerChunk: 1023
14:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.allocator.type: pooled
14:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.threadLocalDirectBufferSize: 0
14:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.maxThreadLocalCharBufferSize: 16384
14:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 4096
14:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
14:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.chunkSize: 32
14:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.blocking: false
14:42:24.060 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkAccessible: true
14:42:24.060 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkBounds: true
14:42:24.060 [nioEventLoopGroup-2-1] DEBUG i.n.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@310af49
Disconnected from the target VM, address: '127.0.0.1:13875', transport: 'socket'

Process finished with exit code 130

客户端连接之后会间隔2S向服务端推送当前时间。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Connected to the target VM, address: '127.0.0.1:13714', transport: 'socket'
Tue Apr 11 14:42:24 CST 2023 Hello world
Tue Apr 11 14:42:26 CST 2023 Hello world
Tue Apr 11 14:42:28 CST 2023 Hello world

JDK的NIO针对BIO的改良点

NIO模型工作上有了“分工”的细节,即两个Selector,一个负责接受新连接,另一个负责处理连接传递的数据。

对比BIO模型一个连接就分配一个线程的策略,NIO模型的策略是让所有的连接注册过程变为由一个Selector完成,Selector会定期轮询检查哪个客户端连接可以接入,如果可以接入就注册到当前的Selector,后续遇到数据读取只需要轮询一个Selector就行了。

线程资源受限问题通过Selector将每个客户端的while(true) 转为只有一个 while(true) 死循环得以解决,它的“副作线程用”是线程的减少直接带来了切换效率的提升。不仅如此NIO还提供了面向Buffer的缓存 ByteBuffer,提高读写效率,移动指针任意读写。

JDK的NIO编程模型缺点

看起来无非就是代码复杂了一点,其实NIO模型看起来也“还不错”?

NO!NO!NO!JDK的NIO实际上还有很多其他问题:

    1. API复杂难用,需要理解非常多的底层概念 。(尤其是臭名昭著的 ByteBuffer)
    1. JDK没有线程模型,用户需要自己设计底层NIO模型。
    1. 自定义协议也要拆包 。
    1. JDK的NIO是由于Epoll实现的,底层存在空轮询的BUG
    1. 自行实现NIO模型会存在很多问题。
    1. 编程人员的编程水平层次不齐,个人定制的NIO模型难以通用,替换性也很差。

基于以上种种问题,Netty 统统都有解决方案。

简单介绍AIO

JDK的AIO不是很成熟,AIO底层依然因为Epoll的遗留问题存在臭名昭著的空轮询BUG,这里并不推荐读者使用JDK的AIO进行编程。

image.png

Java AIO 的核心在于两个关键类:AsynchronousSocketChannelAsynchronousServerSocketChannel

AsynchronousSocketChannel 实现异步套接字通信,可以让我们在不同的客户端连接之间切换,而无需创建新的线程或线程池。

AsynchronousServerSocketChannel 则用于异步地监听客户端的连接请求。

Java 实现代码

这里用ChatGPT生成了一段JDK的AIO代码,为了更好理解顺带让它把注释一块生成了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class AIOServer {  
  
  
    public static void main(String[] args) throws IOException {  
        // 创建一个 ExecutorService,用于处理异步操作的线程池  
        ExecutorService executor = Executors.newFixedThreadPool(10);  
        // 创建一个 AsynchronousChannelGroup,将线程池与该 Channel 组关联  
        AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(executor);  
  
        // 创建 AsynchronousServerSocketChannel,并绑定到指定地址和端口  
        final AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup);  
        InetSocketAddress address = new InetSocketAddress("localhost", 12345);  
        serverSocketChannel.bind(address);  
  
        System.out.println("Server started on port " + address.getPort());  
  
        // 调用 accept 方法接收客户端连接,同时传入一个 CompletionHandler 处理连接结果  
        serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {  
            // 当连接成功时会调用 completed 方法,传入客户端的 SocketChannel 实例作为参数  
            @Override  
            public void completed(AsynchronousSocketChannel clientSocketChannel, Object attachment) {  
                // 继续接受下一个客户端连接,并处理当前客户端的请求  
                serverSocketChannel.accept(null, this);  
                handleClient(clientSocketChannel);  
            }  
  
            // 当连接失败时会调用 failed 方法,传入异常信息作为参数  
            @Override  
            public void failed(Throwable exc, Object attachment) {  
                System.out.println("Error accepting connection: " + exc.getMessage());  
            }  
        });  
  
        // 在主线程中等待,防止程序退出  
        while (true) {  
            try {  
                Thread.sleep(Long.MAX_VALUE);  
            } catch (InterruptedException e) {  
                break;  
            }  
        }  
    }  
  
    private static void handleClient(AsynchronousSocketChannel clientSocketChannel) {  
        ByteBuffer buffer = ByteBuffer.allocate(1024);  
        // 读取客户端发送的数据,同时传入一个 CompletionHandler 处理读取结果  
        clientSocketChannel.read(buffer, null, new CompletionHandler<Integer, Object>() {  
            // 当读取成功时会调用 completed 方法,传入读取到的字节数和附件对象(此处不需要)  
            @Override  
            public void completed(Integer bytesRead, Object attachment) {  
                if (bytesRead > 0) {  
                    // 将 Buffer 翻转,以便进行读取操作  
                    buffer.flip();  
                    byte[] data = new byte[bytesRead];  
                    buffer.get(data, 0, bytesRead);  
                    String message = new String(data);  
                    System.out.println("Received message: " + message);  
                    // 向客户端发送数据  
                    clientSocketChannel.write(ByteBuffer.wrap(("Hello, " + message).getBytes()));  
                    buffer.clear();  
                    // 继续读取下一批数据,并传入当前的 CompletionHandler 以处理读取结果  
                    clientSocketChannel.read(buffer, null, this);  
                } else {  
                    try {  
                        // 当客户端关闭连接时,关闭该 SocketChannel                        clientSocketChannel.close();  
                    } catch (IOException e) {  
                        System.out.println("Error closing client socket channel: " + e.getMessage());  
                    }  
                }  
            }  
  
            // 当读取失败时会调用 failed 方法,传入异常信息和附件对象(此处不需要)  
            @Override  
            public void failed(Throwable exc, Object attachment) {  
                System.out.println("Error reading from client socket channel: " + exc.getMessage());  
            }  
        });  
    }  
  
  
}

AIO 编程模型优缺点

优点

并发性高、CPU利用率高、线程利用率高 。

缺点

不适合轻量级数据传输,因为进程之间频繁的通信在追错、管理,资源消耗上不是很可观。

适用场景

对并发有需求的重量级数据传输。

从上面的代码也可以看出,AIO的API和NIO又是截然不同的写法,为了不继续增加学习成本,这里点到为止,不再深入AIO编程模型的部分了,让我们继续回到Netty,了解Netty的编程模型。

使用Netty 带来的好处

  • Netty不需要了解过多概念
  • 底层IO模型随意切换
  • 自带粘包拆包的问题处理
  • 解决了空轮询问题
  • 自带协议栈,支持通用协议切换
  • 社区活跃,各种问题都有解决方案
  • RPC、消息中间件实践,健壮性极强

网络IO通信框架过程

一个网络IO通信框架从客户端发出请求到接受到结果,基本包含了下面这8个操作:

    1. 解析指令
    1. 构建指令对象
    1. 编码
    1. 等待响应
    1. 解码
    1. 翻译指令对象
    1. 解析指令
    1. 执行

下面来看看Netty的编程模型。

Netty 启动模板代码(重要)

经过上面一长串的铺垫,现在来到整体Netty的代码部分:

服务端

首先是服务端代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        NioEventLoopGroup boos = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        serverBootstrap
                .group(boos, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel ch) {
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println(msg);
                            }
                        });
                    }
                })
                .bind(8000);
    }

初学Netty的时候可能没有NIO的经验,所以我们简单做个类比:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();

可以直接看作

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();

其中boss负责处理连接,worker负责读取请求和处理数据。两者的工作模式也是类似的,boss就像是老板负责“接单”,worker 打工仔负责接收单子的内容然后开始打工干活。

客户端

客户端的启动代码如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

public static void main(String[] args) throws InterruptedException {  
    Bootstrap bootstrap = new Bootstrap();  
    NioEventLoopGroup eventExecutors = new NioEventLoopGroup();  
    // 引导器引导启动  
    bootstrap.group(eventExecutors)  
            .channel(NioSocketChannel.class)  
            .handler(new ChannelInitializer<Channel>() {  
                @Override  
                protected void initChannel(Channel channel) throws Exception {  
                    channel.pipeline().addLast(new StringEncoder());  
                }  
            });  
  
    // 建立通道  
    Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();  
  
    while (true){  
        channel.writeAndFlush(new Date() + " Hello world");  
        Thread.sleep(2000);  
    }  
  
}

客户端的代码中的NioEventLoopGroup实际对应了main函数单独开启的线程。上面的代码可以完美的替代调JDK的NIO、AIO、BIO 的API,学习成本大大降低,Netty为使用者做了大量的“准备”工作,提供了很多"开箱即用"的功能,非常方便。

Netty的服务端和客户端的入门程序代码是分析源码的开始,这部分代码需要有较深的印象。

问题

摘录部分Netty入门级别的八股。

Linux网络编程中的五种I/O模型

关键点:

不同的角度理解IO模型的概念会有变化。注意本部分站在用户程序和内核的网络IO交互的角度理解的。

权威:

  • RFC标准
  • 书籍 《UNIX Network Programming》(中文名《UNIX网络编程-卷一》)第六章。

下面部分总结自:《UNIX Network Programming》(中文名《UNIX网络编程-卷一》)

1)阻塞式I/O

注意原书中阻塞式I/O给出的例子是UDP而不是TCP的例子。recvfrom 函数可以看作是系统调用,在阻塞I/O模型中,recvfrom 的系统调用要等待内核把数据从内核态拷贝到用户的缓冲池或者发生错误的时候(比如信号中断)才进行返回。recvfrom 收到数据之后再执行数据处理。

image.png

2)非阻塞式I/O

recvfrom 的系统调用会在设置非阻塞的时候,会要求内核在无数据的时候返回错误,所以前面三次都是错误调用,在第四次调用之后此时recvfrom轮询到数据,于是开始正常的等待内核把数据复制到用户进程缓存。

此处轮询的定义为:对于描述符进行recvfrom循环调用,会增加CPU的开销。注意非阻塞的轮询不一定要比阻塞等待要强,有时候甚至会有无意义的开销反而不如阻塞。

image.png

3)I/O复用(select,poll,epoll...)

I/O多路复用是阻塞在select,epoll这样的系统调用,没有阻塞在真正的I/O系统调用如recvfrom。进程受阻于select,等待可能多个套接口中的任一个变为可读

IO多路复用最大的区别是使用两个系统调用(select和recvfrom)。Blocking IO(BIO)只调用了一个系统调用(recvfrom)。

select/epoll 核心是可以同时处理多个 connection,但是这并不一定提升效率,连接数不高的话性能不一定比多线程+阻塞IO好。但是连接数比较庞大之后会有显著的差距。

多路复用模型中,每一个socket都需要设置为non-blocking,否则是无法进行elect的。

listenerChannel.configureBlocking(false);这个设置的意义就在于此。

image.png

4)信号驱动式I/O(SIGIO)

信号驱动的优势是等待数据报到之前进程不被阻塞,主循环可以继续执行,等待信号到来即可,注意这里有可能是数据已经准备好被处理,或者数据复制完成可以准备读取。

信号驱动IO 也是同步模型,虽然可以通过信号的方式减少交互,但是系统调用过程当中依然需要进行等待,内核也依然是通知何时开启一个IO操作,和前面介绍的IO模型对比发现优势并不明显。

image.png

5)异步I/O(POSIX的aio_系列函数)

image.png

核心: Future-Listener机制

  • IO操作分为两步
    • 发起IO请求,等待数据准备(Waiting for the data to be ready)
    • 实际的IO操作,将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

前四种IO模型都是同步IO操作,主要的区别在于第一阶段处理方式,而他们的第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用或者select() 函数。 异步I/O模型内在这两个阶段都要(自行)处理。

阻塞IO和非阻塞IO区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO

异步IO模型非常像是我们日常点外卖,我们时不时看看配送进度就是在“轮询”,当外卖员把外卖送到指定位置打电话通知我们去拿即可。

交互几个核心点

再次强调是针对用户程序和内核的网络IO交互角度理解的。

  • 阻塞非阻塞说的是线程的状态(重要)
  • 同步和异步说的是消息的通知机制(重要) - 同步需要主动读写数据,异步是不需要主动读写数据 - 同步IO和异步IO是针对用户应用程序和内核的交互

为什么Netty使用NIO而不是AIO?

  1. Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化
  2. Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱,把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来
  3. AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多
  4. Linux上AIO不够成熟,处理回调结果速度跟不到处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)。

结论

Netty整体架构是reactor模型,采用epoll机制,所以往深的说,还是IO多路复用模式,所以也可说netty是同步非阻塞模型(看的层次不一样),只不过实际是异步IO。

Netty 应用场景了解么?

  • 作为 RPC 框架的网络通信工具。分布式系统之间的服务器通信可以使用Netty完成,虽然是Java编写的框架,但是性能非常接近 C 和C++ 执行效率。
  • 消息队列:比如大名鼎鼎的RocketMq底层完全依赖Netty,编程人员不需要很强的并发编程功底也可以快速上手和维护代码。
  • 实现一个即时通讯系统:正好和本书应用场景重合了。

介绍Netty

简短介绍

Netty是一个高性能异步NIO编程模型的网络编程框架。它提供了简单易用的API,可以快速地开发各种网络应用程序,如客户端、服务器和协议实现等。同时,Netty还具有良好的可扩展性和灵活性,支持多种传输协议和编解码器。

稍微复杂一点

Netty是由JBOSS提供的一个java开源框架, 是业界最流行的NIO框架,整合了多种协议( 包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,精心设计的框架,在多个大型商业项目中得到充分验证。 1)API使用简单 2)成熟、稳定 3)社区活跃 有很多种NIO框架 如mina 4)经过大规模的验证(互联网、大数据、网络游戏、电信通信行业)。

总结

  • 开篇简单介绍了JDK的BIO、NIO和AIO,三者不仅出现时间跨度大,三个团队编写,和JDK的IO编程一样晦涩难懂和不好用,开发人员需要花大量事件学习底层细节。
  • 用洗衣机的例子,理解网络编程模型的重要概念:同步、非同步、阻塞、非阻塞。从入门的角度来看,同步和异步可以认为是否是由客户端主动获取数据,而阻塞和非阻塞则是客户端是否需要拿到结果进行处理,两者是相辅相成的。
  • Netty 编程模型统一了JDK的编程模型,降低了学习成本,同时效率比原生JDK更高,并且解决了NIO 中的空轮询问题。
  • Netty 底层实际上和JDK的网络编程模型密切相关,从案例代码可以看到Netty的客户端API代码可以直接往NIO的Server发送数据。
  • 补充书中没有介绍的AIO编程模型,用ChatGPT 生成的代码简单易懂。
  • 最后补充有关Netty的问题。

写在最后

开篇部分补充了书中没介绍的一些网络编程模型的基本概念,以及在最后关联了些相关书籍的知识点和,最后顺带归纳了一些八股问题,当然最为重要的部分是熟悉Netty的入门程序代码。

开篇入门篇到此就结束了,如果内容描述有误,欢迎评论或者私信留言。

参考

  • 《跟闪电侠学Netty》开篇:Netty是什么? - 简书 (jianshu.com)
  • 网课专栏:《高并发系列之百万连接Netty实战课程》

Netty 书籍推荐

  • 《Netty权威指南》
  • 《Netty进阶之路》
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 懒时小窝 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
《跟闪电侠学Netty》阅读笔记 - Netty入门程序解析
上一节《跟闪电侠学Netty》阅读笔记 - 开篇入门Netty 中介绍了Netty的入门程序,本节如标题所言将会一步步分析入门程序的代码含义。
阿东
2023/06/27
8000
《跟闪电侠学Netty》阅读笔记 - Netty入门程序解析
Netty4学习笔记 --- Netty入门
互联网行业: 在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。
挽风
2021/04/13
6660
Netty4学习笔记 --- Netty入门
分布式专题|肝了这篇,再也不怕面试官问BIO、NIO、AIO了,我先肝了,你随意
IO模型指的是在网络数据传输过程中,使用什么通道去发送和接收数据,我们常见的有BIO、NIO、AIO(NIO2.0),我接下来会对这些进行详细的介绍
AI码师
2020/12/13
3960
分布式专题|肝了这篇,再也不怕面试官问BIO、NIO、AIO了,我先肝了,你随意
Java面试常考的 BIO,NIO,AIO 总结
熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也是你学习 Netty 的基础。
Java技术江湖
2019/09/25
8060
Java面试常考的 BIO,NIO,AIO 总结
高性能通讯框架——Netty
编程复杂,缓冲区Buffer要考虑读写指针切换。而Netty把它封装之后,进行优化并提供了一个易于操作的使用模式和接口,因此Netty就被广泛使用于通信框架。
IT大咖说
2021/04/08
7390
史诗级最强教科书式“NIO与Netty编程”
java.nio全称java non-blocking IO,是指JDK1.4开始提供的新API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,也被称为NIO(既New IO),新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写,新增类满足NIO的功能。 NIO和BIO有着相同的目的和作用,但是它们的实现方式完全不同,BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多。另外,NIO是非阻塞式的,这一点跟BIO也很不相同,使用它可以提供非阻塞式的高伸缩性网络。 NIO主要有三大核心部分 :Channel(通道),Buffer(缓冲区),Selector(选择器)。传统的BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如 :连接打开,数据到达)。因此使用单个线程就可以监听多个数据管道。
海仔
2019/08/26
9400
史诗级最强教科书式“NIO与Netty编程”
Netty权威指南_算法笔记上机指南pdf
fd:file descriptor,文件描述符。linux内核将所有外部设备都看作一个文件来操作,对文件的读写会调用内核提供的命令,返回一个文件描述符。对一个socket的读写也会有相应的socket fd。描述符就是一个指向内核中结构体的数字。
全栈程序员站长
2022/09/20
1.4K0
Netty权威指南_算法笔记上机指南pdf
Netty整理 顶
洗衣机洗衣服(无论阻塞式IO还是非阻塞式IO,都是同步IO模型) 同步阻塞:你把衣服丢到洗衣机洗,然后看着洗衣机洗完,洗好后再去晾衣服(你就干等,啥都不做,阻塞在那边)
算法之名
2019/09/12
4370
Netty整理
                                                                            顶
Netty入门之WebSocket初体验
BIO即同步阻塞模式一请求一应答的通信模型,该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是JAVA虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
端碗吹水
2020/09/23
1K0
Netty入门之WebSocket初体验
一篇文章搞定Netty入门
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
用户1212940
2019/11/13
4220
Netty[Netty从入门到精通]
应用层:Http协议、电子文件传输、文件服务器等 表示层:解决我们不同系统之间语法的通讯 会话层:建立与应用程序之间的通讯 传输层:提供了端口号和接口协议TPC/UDP
高大北
2022/09/02
1.1K0
Netty[Netty从入门到精通]
Netty之旅:你想要的NIO知识点,这里都有!
这段时间也一直在学习Netty相关知识,因为涉及知识点比较多,也走了不少弯路。目前网上关于Netty学习资料玲琅满目,不知如何下手,其实大家都是一样的,学习方法和技巧都是总结出来的,我们在没有找到很好的方法之前不如按部就班先从基础开始,一般从总分总的渐进方式,既观森林,又见草木。
一枝花算不算浪漫
2020/08/10
5520
【Netty】NIO编程的利器
今天换换口味,由于本人工作中马上要用到Netty这个东西,所以这几天也是开始学习,此学习过程应该会是一个完整的系列,初步的目标是先会用,之后有机会再深入。鉴于笔者之前也从未使用过Netty,所以有什么疏漏错误的,希望大家指正,先行感谢!
周三不加班
2019/09/04
4140
【Netty】NIO编程的利器
NIO (New I/O)
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
崔笑颜
2020/06/08
8110
netty入门
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。 “快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。
周杰伦本人
2022/10/25
2910
netty入门
Netty序章之BIO NIO AIO演变
Netty是一个提供异步事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络服务器和客户端程序。Netty简化了网络程序的开发,是很多框架和公司都在使用的技术。更是面试的加分项。Netty并非横空出世,它是在BIO,NIO,AIO演变中的产物,是一种NIO框架。而BIO,NIO,AIO更是笔试中要考,面试中要问的技术。也是一个很好的加分项,加分就是加工资,你还在等什么?本章带你细细品味三者的不同! 流程图:
用户1212940
2022/04/13
5430
Netty序章之BIO NIO AIO演变
45 张图深度解析 Netty 架构与原理
接下来我们会学习一个 Netty 系列教程,Netty 系列由「架构与原理」,「源码」,「架构」三部分组成,今天我们先来看看第一部分:Netty 架构与原理初探,大纲如下:
kunge
2020/11/27
25.6K4
Netty之美--I/O模型
「同步(synchronous)」是指当一个任务A的执行需要依赖于另外一个任务B的执行结果时,任务A必须等待任务B执行完成,才可以继续执行;
早安嵩骏
2020/08/11
6220
1 Netty 网络高并发框架
纯手打,总结! Netty是什么? Netty是当前非常流行的网络通讯开源框架,高并发和高可靠,底层就可以用Netty支撑。 Netty 官网:https://netty.io/ 学习视频:https
收心
2022/01/19
6910
1 Netty 网络高并发框架
Netty网络编程第六卷
1)获取请求数据,客户端与服务器建立连接发出请求,服务器接受请求(1-3); 2)构建响应,当服务器接收完请求,并在用户空间处理客户端的请求,直到构建响应完成(4); 3)返回数据,服务器将已构建好的响应再通过内核空间的网络 I/O 发还给客户端(5-7)。
大忽悠爱学习
2022/05/06
3820
Netty网络编程第六卷
相关推荐
《跟闪电侠学Netty》阅读笔记 - Netty入门程序解析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验