# netty学习笔记 **Repository Path**: june-woo/netty-study-notes ## Basic Information - **Project Name**: netty学习笔记 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-04-27 - **Last Updated**: 2022-04-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Netty ## I/O 模型 I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。 Java 共支持 3 种网络编程模型 I/O 模式:BIO、NIO、AIO。 > `Java BIO`:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。【简单示意图】 | BIO | | ------------------------------------------------------------ | | ![image-20220425200508607](netty.assets/image-20220425200508607.png) | > `Java NIO`:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 `I/O` 请求就进行处理。【简单示意图】 | NIO | | ------------------------------------------------------------ | | ![image-20220425201027608](netty.assets/image-20220425201027608.png) | > `Java AIO(NIO.2)`:异步非阻塞,`AIO` 引入异步通道的概念,采用了 `Proactor` 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 ## BIO、NIO、AIO 使用场景分析 BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序简单易理解。 NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。 AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。 ## Java BIO编程 Java BIO 就是传统的 Java I/O 编程,其相关的类和接口在 java.io。 BIO(BlockingI/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。【后有应用实例】 BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。 ### 对 BIO 编程流程的梳理 1 服务器端启动一个 ServerSocket。 2 客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。 3 客户端发出请求后,先咨询服务器是否有线程响应,==如果没有则会等待,或者被拒绝。== 4 如果有响应,客户端线程会等待请求结束后,再继续执行。 ## Java NIO 基本介绍 Java NIO 全称 Java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 NewIO),是同步非阻塞的。 NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。【基本案例】 NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器) 。 NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。 Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。【后面有案例说明】 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。 HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1 大了好几个数量级。 ### Selector、Channel 和 Buffer 关系图(简单版) image-20220427195225332 1. 每个 `Channel` 都会对应一个 `Buffer`。 2. `Selector` 对应一个线程,一个线程对应多个 `Channel`(连接)。 3. 该图反应了有三个 `Channel` 注册到该 `Selector` //程序 4. 程序切换到哪个 `Channel` 是由事件决定的,`Event` 就是一个重要的概念。 5. `Selector` 会根据不同的事件,在各个通道上切换。 6. `Buffer` 就是一个内存块,底层是有一个数组。 7. 数据的读取写入是通过 `Buffer`,这个和 `BIO`是不同的,`BIO` 中要么是输入流,或者是输出流,不能双向,但是 `NIO` 的 `Buffer` 是可以读也可以写,需要 `flip` 方法切换 `Channel` 是双向的,可以返回底层操作系统的情况,比如 `Linux`,底层的操作系统通道就是双向的。 ### 缓冲区(Buffer) ### 基本介绍 缓冲区(`Buffer`):缓冲区本质上是一个**可以读写数据的内存块**,可以理解成是一个**容器对象(含数组)**,该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。`Channel` 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 `Buffer` ### Buffer 类及其子类 1. 在 `NIO` 中,`Buffer` 是一个顶层父类,它是一个抽象类,类的层级关系图: ![image-20220427195831800](netty.assets/image-20220427195831800.png) 2. `Buffer` 类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息: ``` // Invariants: mark <= position <= limit <= capacity private int mark = -1; //标记 private int position = 0;//位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备 private int limit;//表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作.这个值是可以修改的 private int capacity;//容量,即可以容纳最大的数据量,在缓冲区创建时被设定并且不能被改变 ``` 3. `Buffer` 类相关方法一览 ![image-20220427201017459](netty.assets/image-20220427201017459.png) #### ByteBuffer 从前面可以看出对于 `Java` 中的基本数据类型(`boolean` 除外),都有一个 `Buffer` 类型与之相对应,最常用的自然是 `ByteBuffer` 类(二进制数据),该类的主要方法如下: ![image-20220427201118263](netty.assets/image-20220427201118263.png) ### 通道(Channel) 1. `NIO` 的通道类似于流,但有些区别如下: - 通道可以同时进行读写,而流只能读或者只能写 - 通道可以实现异步读写数据 - 通道可以从缓冲读数据,也可以写数据到缓冲: 2. `BIO` 中的 `Stream` 是单向的,例如 `FileInputStream` 对象只能进行读取数据的操作,而 `NIO` 中的通道(`Channel`)是双向的,可以读操作,也可以写操作。 3. `Channel` 在 `NIO` 中是一个接口 `public interface Channel extends Closeable{}` 4. 常用的 `Channel` 类有:**`FileChannel`、`DatagramChannel`、`ServerSocketChannel` 和 `SocketChannel`**。【`ServerSocketChanne` 类似 `ServerSocket`、`SocketChannel` 类似 `Socket`】 5. `FileChannel` 用于文件的数据读写,`DatagramChannel` 用于 `UDP` 的数据读写,`ServerSocketChannel` 和 `SocketChannel` 用于 `TCP` 的数据读写。 #### FileChannel 类 `FileChannel` 主要用来对本地文件进行 `IO` 操作,常见的方法有 - `public int read(ByteBuffer dst)`,从通道读取数据并放到缓冲区中 - `public int write(ByteBuffer src)`,把缓冲区的数据写到通道中 - `public long transferFrom(ReadableByteChannel src, long position, long count)`,从目标通道中复制数据到当前通道 - `public long transferTo(long position, long count, WritableByteChannel target)`,把数据从当前通道复制给目标通道 ##### 应用实例1 - 本地文件写数据 实例要求: 1. 使用前面学习后的 `ByteBuffer`(缓冲)和 `FileChannel`(通道),将 “hello,尚硅谷” 写入到 `file01.txt` 中 2. 文件不存在就创建 3. 代码演示 ``` JAVA package com.atguigu.nio; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class NIOFileChannel01 { public static void main(String[] args) throws Exception { String str = "hello,尚硅谷"; //创建一个输出流 -> channel FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt"); //通过 fileOutputStream 获取对应的 FileChannel //这个 fileChannel 真实类型是 FileChannelImpl FileChannel fileChannel = fileOutputStream.getChannel(); //创建一个缓冲区 ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //将 str 放入 byteBuffer byteBuffer.put(str.getBytes()); //对 byteBuffer 进行 flip byteBuffer.flip(); //将 byteBuffer 数据写入到 fileChannel fileChannel.write(byteBuffer); fileOutputStream.close(); } } ``` ##### 应用实例2 - 本地文件读数据 实例要求: 1. 使用前面学习后的 `ByteBuffer`(缓冲)和 `FileChannel`(通道),将 `file01.txt` 中的数据读入到程序,并显示在控制台屏幕 2. 假定文件已经存在 3. 代码演示 ``` JAVA package com.atguigu.nio; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class NIOFileChannel02 { public static void main(String[] args) throws Exception { //创建文件的输入流 File file = new File("d:\\file01.txt"); FileInputStream fileInputStream = new FileInputStream(file); //通过 fileInputStream 获取对应的 FileChannel -> 实际类型 FileChannelImpl FileChannel fileChannel = fileInputStream.getChannel(); //创建缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length()); //将通道的数据读入到 Buffer fileChannel.read(byteBuffer); //将 byteBuffer 的字节数据转成 String System.out.println(new String(byteBuffer.array())); fileInputStream.close(); } } ``` ##### 应用实例3 - 使用一个 Buffer 完成文件读取、写入 实例要求: 1. 使用 `FileChannel`(通道)和方法 `read、write`,完成文件的拷贝 2. 拷贝一个文本文件 `1.txt`,放在项目下即可 3. 代码演示 ``` package com.atguigu.nio; import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class NIOFileChannel03 { public static void main(String[] args) throws Exception { FileInputStream fileInputStream = new FileInputStream("1.txt"); FileChannel fileChannel01 = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("2.txt"); FileChannel fileChannel02 = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { //循环读取 //这里有一个重要的操作,一定不要忘了 /* public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; } */ byteBuffer.clear(); //清空 buffer int read = fileChannel01.read(byteBuffer); System.out.println("read = " + read); if (read == -1) { //表示读完 break; } //将 buffer 中的数据写入到 fileChannel02--2.txt byteBuffer.flip(); fileChannel02.write(byteBuffer); } //关闭相关的流 fileInputStream.close(); fileOutputStream.close(); } } ``` ##### 应用实例4 - 拷贝文件 transferFrom 方法 1. 实例要求: 2. 使用 `FileChannel`(通道)和方法 `transferFrom`,完成文件的拷贝 3. 拷贝一张图片 4. 代码演示 ``` JAVA package com.atguigu.nio; import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.channels.FileChannel; public class NIOFileChannel04 { public static void main(String[] args) throws Exception { //创建相关流 FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg"); FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg"); //获取各个流对应的 FileChannel FileChannel sourceCh = fileInputStream.getChannel(); FileChannel destCh = fileOutputStream.getChannel(); //使用 transferForm 完成拷贝 destCh.transferFrom(sourceCh, 0, sourceCh.size()); //关闭相关通道和流 sourceCh.close(); destCh.close(); fileInputStream.close(); fileOutputStream.close(); } } ``` ## Selector(选择器) ### 基本介绍 1. `Java` 的 `NIO`,用非阻塞的 `IO` 方式。可以用一个线程,处理多个的客户端连接,就会使用到 `Selector`(选择器)。 2. `Selector` 能够检测多个注册的通道上是否有事件发生(注意:多个 `Channel` 以事件的方式可以注册到同一个 `Selector`),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。 3. 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。 4. 避免了多线程之间的上下文切换导致的开销。 说明如下: 1. `Netty` 的 `IO` 线程 `NioEventLoop` 聚合了 `Selector`(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。 2. 当线程从某客户端 `Socket` 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。 3. 线程通常将非阻塞 `IO` 的空闲时间用于在其他通道上执行 `IO` 操作,所以单独的线程可以管理多个输入和输出通道。 4. 由于读写操作都是非阻塞的,这就可以充分提升 `IO` 线程的运行效率,避免由于频繁 `I/O` 阻塞导致的线程挂起。 5. 一个 `I/O` 线程可以并发处理 `N` 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 `I/O` 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。 ### 注意事项 1. `NIO` 中的 `ServerSocketChannel` 功能类似 `ServerSocket`、`SocketChannel` 功能类似 `Socket`。 2. `Selector`相关方法说明 - `selector.select();` //阻塞 - `selector.select(1000);` //阻塞 1000 毫秒,在 1000 毫秒后返回 - `selector.wakeup();` //唤醒 selector - `selector.selectNow();` //不阻塞,立马返还 ## IO过程简述: 1. 用户进程调用 read 方法,向cpu发出 I/O 请求 2. cpu向磁盘发起IO请求给磁盘控制器,**之后立马返回**。返回之后cpu可以切换到其它进程执行其他任务 3. 磁盘控制器收到指令后,于是就开始进行磁盘IO,磁盘IO完成后会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个**中断** 4. CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据读进内核的页缓存【这个过程是可以用DMA进行优化的】。 5. 接着将数据从内核页缓存拷贝到用户进程空间【这个过程想要优化,只能用到我们上面说的异步IO】 6. 最后read()调用返回。 > 注意: > > 1. 这里很多博客画的图是错的,讲的也是错的。使用中断减少CPU开销时,在进行磁盘IO期间,CPU可以执行其他的进程不必等待磁盘IO。【因为这是《操作系统导论》里的原话】 image-20220427202719760 ### 利用DMA进行更高效的数据传送 1. 用户进程调用 read 方法,向cpu发出 I/O 请求 2. cpu将IO请求交给DMA控制器,**之后自己立马返回去执行其他进程的任务** 3. DMA向磁盘发起IO请求 4. 磁盘控制器收到指令后,于是就开始进行磁盘IO,磁盘IO完成后会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个**中断**。 5. DMA收到中断后,把磁盘控制器的缓冲区的数据读进内核的页缓存,接着抛出一个中断 6. 操作系统收到中断后,调度cpu回来执行之前的进程:将数据从内核页缓存拷贝到用户进程空间【这一步还是只能用异步IO来优化】 7. 最后read()调用返回。 image-20220427202855309 ## 零拷贝 - 传统文件IO 场景:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。 image-20220427202138455 1. 很明显发生了4次拷贝 - 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝是通过 DMA 的。 - 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是应用程序就可以使用这部分数据了,这个拷贝是由 CPU 完成的。 - 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然由 CPU 完成的。 - 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到协议栈里,这个过程又是由 DMA 完成的。 2. 发生了4次用户上下文切换,因为发生了两个系统调用read和write。一个系统调用对应两次上下文切换,所以上下文切换次数在一般情况下只可能是偶数。 > 想要优化文件传输的性能就两个方向 > > 1. 减少上下文切换次数 > 2. 减少数据拷贝次数 > > 因为这两个是最耗时 ## 零拷贝之mmap `read()` 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,为了减少这一步开销,我们可以用 `mmap()` 替换 `read()` 系统调用函数。`mmap()` 系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间共享缓冲区,就不需要再进行任何的数据拷贝操作。 image-20220427202257914 总的来说mmap减少了一次数据拷贝,总共4次上下文切换,3次数据拷贝 ## 零拷贝之sendfile Linux2.1` 版本提供了 `sendFile` 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 `SocketBuffer image-20220427202403559 总的来说有2次上下文切换,3次数据拷贝。 ## sendfile再优化 `Linux在2.4` 版本中,做了一些修改,避免了从内核缓冲区拷贝到 `Socketbuffer` 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝 image-20220427202439716 ## 文件传输总结 ### 小文件传输 前文一直提到了内核里的页缓存(PageCache),这个页缓存的作用就是用来提升小文件传输的效率 原因: 1. 读写磁盘相比读写内存的速度慢太多了,这个有点基础的人应该都知道,所以我们应该想办法把读写磁盘换成读写内存。于是,我们通过 DMA 把磁盘里的数据拷贝到内存里,这样就可以用读内存替换读磁盘。读磁盘数据的时候,优先在 PageCache 找,如果数据存在则可以直接返回;如果没有,则从磁盘中读取,然后缓存 PageCache 中。这点不是很类似redis和mysql的关系吗,所以说操作系统里的一些设计理念和平时工作应用息息相关,毕竟操作系统可是无数大牛的结晶 2. 程序运行的时候,具有局部性原理,也就是说刚被访问的数据在短时间内再次被访问的概率很高,通常称为热点数据,于是我们用 PageCache 来缓存这些热点数据,当空间不足时有对应的缓存淘汰策略。 #### 大文件传输 Q:PageCache可以用来大文件传输吗? A:不能 Q:为什么呢? A: 1. 假设你要几G的数据要传输,用户访问这些大文件的时候,内核会把它们载入 PageCache 中, PageCache 空间很快被这些大文件占满。 2. PageCache 由于长时间被大文件占据,其他热点小文件可能就无法使用到 PageCache,就频繁读写磁盘,效率低下 3. 而PageCache 中的大文件数据,没有享受到缓存带来的好处,反而却耗费 DMA 多拷贝到 PageCache 一次 4. 这前前后后加起来,效率低了很多,所以PageCache不适合小文件传输 而想不用到内核缓冲区,我们就想到了**直接IO**这个东西,直接IO不经过内核缓存,同时经过上面的讲述,我们也可以知道异步IO效率是最高的。所以大文件传输最好的解决办法应该是:**异步IO+直接IO** ## Netty工作模式简单版 ![image-20220426222603406](netty.assets/image-20220426222603406.png) **对上图说明** 1. `BossGroup` 线程维护 `Selector`,只关注 `Accecpt` 2. 当接收到 `Accept` 事件,获取到对应的 `SocketChannel`,封装成 `NIOScoketChannel` 并注册到 `Worker` 线程(事件循环),并进行维护 3. 当 `Worker` 线程监听到 `Selector` 中通道发生自己感兴趣的事件后,就进行处理(就由 `handler`),注意 `handler` 已经加入到通道 ## 工作原理示意图2 - 进阶版 ![image-20220426222827498](netty.assets/image-20220426222827498.png) `BossGroup`有点像主`Reactor` 可以有多个,`WorkerGroup`则像`SubReactor`一样可以有多个。 ## 工作原理示意图3 - 详细版 ![image-20220426223111348](netty.assets/image-20220426223111348.png) 1. `Netty` 抽象出两组线程池 ,`BossGroup` 专门负责接收客户端的连接,`WorkerGroup` 专门负责网络的读写 2. `BossGroup` 和 `WorkerGroup` 类型都是 `NioEventLoopGroup` 3. `NioEventLoopGroup` 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 `NioEventLoop` 4. `NioEventLoop` 表示一个不断循环的执行处理任务的线程,每个 `NioEventLoop` 都有一个 `Selector`,用于监听绑定在其上的 `socket` 的网络通讯 5. `NioEventLoopGroup` 可以有多个线程,即可以含有多个 `NioEventLoop` 6. 每个 `BossGroup`下面的`NioEventLoop` 循环执行的步骤有 `3` 步 - 轮询 `accept` 事件 - 处理 `accept` 事件,与 `client` 建立连接,生成 `NioScocketChannel`,并将其注册到某个 `workerGroup` `NIOEventLoop` 上的 `Selector` - 继续处理任务队列的任务,即 `runAllTasks` 7. 每个 `WorkerGroup` `NIOEventLoop` 循环执行的步骤 - 轮询 `read`,`write` 事件 - 处理 `I/O` 事件,即 `read`,`write` 事件,在对应 `NioScocketChannel` 处理 - 处理任务队列的任务,即 `runAllTasks` 8. 每个 Worker NIOEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel(通道),即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器。 ```java //过程再说明 1 Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。 2 NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket网络通道。 3 NioEventLoop 内部采用串行化设计,从消息的 读取->解码->处理->编码->发送,始终由 IO 线程NioEventLoop 负责 4 NioEventLoopGroup 下包含多个 NioEventLoop 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上 每个 NioChannel 都绑定有一个自己的 ChannelPipeline ``` ## 异步模型 ### 基本介绍 1. 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。 2. `Netty` 中的 `I/O` 操作是异步的,包括 `Bind、Write、Connect` 等操作会首先简单的返回一个 `ChannelFuture`。 3. 调用者并不能立刻获得结果,而是通过 `Future-Listener` 机制,用户可以方便的主动获取或者通过通知机制获得 `IO` 操作结果。 4. `Netty` 的异步模型是建立在 `future` 和 `callback` 的之上的。`callback` 就是回调。重点说 `Future`,它的核心思想是:假设一个方法 `fun`,计算过程可能非常耗时,等待 `fun` 返回显然不合适。那么可以在调用 `fun` 的时候,立马返回一个 `Future`,后续可以通过 `Future` 去监控方法 `fun` 的处理过程(即:`Future-Listener` 机制) ### Future 说明 1. 表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,比如检索计算等等。 2. `ChannelFuture` 是一个接口:`public interface ChannelFuture extends Future` 我们可以添加监听器,当监听的事件发生时,就会通知到监听器。 ### 工作原理示意图 下面第一张图就是管道,中间会经过多个handler ![image-20220427163707200](netty.assets/image-20220427163707200.png) ![image-20220427163718616](netty.assets/image-20220427163718616.png) 说明: 1. 在使用 `Netty` 进行编程时,拦截操作和转换出入站数据只需要您提供 `callback` 或利用 `future` 即可。这使得链式操作简单、高效,并有利于编写可重用的、通用的代码。 2. `Netty` 框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来、解脱出来。 ## 快速入门实例 - HTTP服务 ### TestServer ```java JAVA package com.atguigu.netty.http; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; public class TestServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new TestServerInitializer()); ChannelFuture channelFuture = serverBootstrap.bind(6668).sync(); channelFuture.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` ### TestServerInitializer ``` JAVA package com.atguigu.netty.http; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpServerCodec; public class TestServerInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) throws Exception { //向管道加入处理器 //得到管道 ChannelPipeline pipeline = ch.pipeline(); //加入一个netty 提供的httpServerCodec codec =>[coder - decoder] //HttpServerCodec 说明 //1. HttpServerCodec 是netty 提供的处理http的 编-解码器 pipeline.addLast("MyHttpServerCodec",new HttpServerCodec()); //2. 增加一个自定义的handler pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler()); System.out.println("ok~~~~"); } } ``` ### TestHttpServerHandler ``` JAVA package com.atguigu.netty.http; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; import java.net.URI; /* 说明 1. SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter 2. HttpObject 客户端和服务器端相互通讯的数据被封装成 HttpObject */ public class TestHttpServerHandler extends SimpleChannelInboundHandler { //channelRead0 读取客户端数据 @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { System.out.println("对应的channel=" + ctx.channel() + " pipeline=" + ctx .pipeline() + " 通过pipeline获取channel" + ctx.pipeline().channel()); System.out.println("当前ctx的handler=" + ctx.handler()); //判断 msg 是不是 httprequest请求 if(msg instanceof HttpRequest) { System.out.println("ctx 类型="+ctx.getClass()); System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode()); System.out.println("msg 类型=" + msg.getClass()); System.out.println("客户端地址" + ctx.channel().remoteAddress()); //获取到 HttpRequest httpRequest = (HttpRequest) msg; //获取uri, 过滤指定的资源 URI uri = new URI(httpRequest.uri()); if("/favicon.ico".equals(uri.getPath())) { System.out.println("请求了 favicon.ico, 不做响应"); return; } //回复信息给浏览器 [http协议] ByteBuf content = Unpooled.copiedBuffer("hello, 我是服务器", CharsetUtil.UTF_8); //构造一个http的相应,即 httpresponse FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); //将构建好 response返回 ctx.writeAndFlush(response); } } } ``` ## Netty 核心模块组件 ### EventLoopGroup 和其实现类 NioEventLoopGroup 1. `EventLoopGroup` 是一组 `EventLoop` 的抽象,`Netty` 为了更好的利用多核 `CPU` 资源,一般会有多个 `EventLoop` 同时工作,每个 `EventLoop` 维护着一个 `Selector` 实例。 2. `EventLoopGroup` 提供 `next` 接口,可以从组里面按照一定规则获取其中一个 `EventLoop` 来处理任务。在 `Netty` 服务器端编程中,我们一般都需要提供两个 `EventLoopGroup`,例如:`BossEventLoopGroup` 和 `WorkerEventLoopGroup`。 3. 通常一个服务端口即一个 `ServerSocketChannel` 对应一个 `Selector` 和一个 `EventLoop` 线程。`BossEventLoop` 负责接收客户端的连接并将 `SocketChannel` 交给 `WorkerEventLoopGroup` 来进行 `IO` 处理,如下图所示 | EventLoopGroup工作示意图 | | ------------------------------ | | ![0022](netty.assets/0022.png) | 4. 常用方法 `public NioEventLoopGroup()`,构造方法 `public Future shutdownGracefully()`,断开连接,关闭线程 ### Bootstrap、ServerBootstrap 1. `Bootstrap` 意思是引导,一个 `Netty` 应用通常由一个 `Bootstrap` 开始,主要作用是配置整个 `Netty` 程序,串联各个组件,`Netty` 中 `Bootstrap` 类是客户端程序的启动引导类,`ServerBootstrap` 是服务端启动引导类。 2. 常见的方法有 - `public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)`,该方法用于服务器端,用来设置两个 `EventLoop` - `public B group(EventLoopGroup group)`,该方法用于客户端,用来设置一个 `EventLoop` - `public B channel(Class channelClass)`,该方法用来设置一个服务器端的通道实现 - `public B option(ChannelOption option, T value)`,用来给 `ServerChannel` 添加配置 - `public ServerBootstrap childOption(ChannelOption childOption, T value)`,用来给接收到的通道添加配置 - `public ServerBootstrap childHandler(ChannelHandler childHandler)`,该方法用来设置业务处理类(自定义的`handler`) - `public ChannelFuture bind(int inetPort)`,该方法用于服务器端,用来设置占用的端口号 - `public ChannelFuture connect(String inetHost, int inetPort)`,该方法用于客户端,用来连接服务器端 ### Future、ChannelFuture `Netty` 中所有的 `IO` 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 `Future` 和 `ChannelFutures`,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件 常见的方法有 - `Channel channel()`,返回当前正在进行 `IO` 操作的通道 - `ChannelFuture sync()`,等待异步操作执行完毕 ### Channel 1. `Netty` 网络通信的组件,能够用于执行网络 `I/O` 操作。 2. 通过 `Channel` 可获得当前网络连接的通道的状态 3. 通过 `Channel` 可获得网络连接的配置参数(例如接收缓冲区大小) 4. `Channel` 提供异步的网络 `I/O` 操作(如建立连接,读写,绑定端口),异步调用意味着任何 `I/O` 调用都将立即返回,并且不保证在调用结束时所请求的 `I/O` 操作已完成 5. 调用立即返回一个 `ChannelFuture` 实例,通过注册监听器到 `ChannelFuture` 上,可以 `I/O` 操作成功、失败或取消时回调通知调用方 6. 支持关联 `I/O` 操作与对应的处理程序 7. 不同协议、不同的阻塞类型的连接都有不同的 `Channel` 类型与之对应,常用的 `Channel` 类型: - `NioSocketChannel`,异步的客户端 `TCP` `Socket` 连接。 - `NioServerSocketChannel`,异步的服务器端 `TCP` `Socket` 连接。 - `NioDatagramChannel`,异步的 `UDP` 连接。 - `NioSctpChannel`,异步的客户端 `Sctp` 连接。 - `NioSctpServerChannel`,异步的 `Sctp` 服务器端连接,这些通道涵盖了 `UDP` 和 `TCP` 网络 `IO` 以及文件 `IO`。 ### Selector 1. `Netty` 基于 `Selector` 对象实现 `I/O` 多路复用,通过 `Selector` 一个线程可以监听多个连接的 `Channel` 事件。 2. 当向一个 `Selector` 中注册 `Channel` 后,`Selector` 内部的机制就可以自动不断地查询(`Select`)这些注册的 `Channel` 是否有已就绪的 `I/O` 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 `Channel` ### ChannelHandler 及其实现类 1. `ChannelHandler` 是一个接口,处理 `I/O` 事件或拦截 `I/O` 操作,并将其转发到其 `ChannelPipeline`(业务处理链)中的下一个处理程序。 2. `ChannelHandler` 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类 3. `ChannelHandler` 及其实现类一览图 ![image-20220427171746212](netty.assets/image-20220427171746212.png) 4. 我们经常需要自定义一个 `Handler` 类去继承 `ChannelInboundHandlerAdapter`,然后通过重写相应方法实现业务逻辑,我们接下来看看一般都需要重写哪些方法 0016 ### Pipeline 和 ChannelPipeline 1. `ChannelPipeline` 是一个 `Handler` 的集合,它负责处理和拦截 `inbound` 或者 `outbound` 的事件和操作,相当于一个贯穿 `Netty` 的链。(也可以这样理解:`ChannelPipeline` 是保存 `ChannelHandler` 的 `List`,用于处理或拦截 `Channel` 的入站事件和出站操作) 2. `ChannelPipeline` 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 `Channel` 中各个的 `ChannelHandler` 如何相互交互 3. 在 `Netty` 中每个 `Channel` 都有且仅有一个 `ChannelPipeline` 与之对应,它们的组成关系如下 ![image-20220427190821671](netty.assets/image-20220427190821671.png) 4. 常用方法 `ChannelPipeline addFirst(ChannelHandler... handlers)`,把一个业务处理类(`handler`)添加到链中的第一个位置`ChannelPipeline addLast(ChannelHandler... handlers)`,把一个业务处理类(`handler`)添加到链中的最后一个位置 ![image-20220427191009514](netty.assets/image-20220427191009514.png) - `TestServerInitializer`和`HttpServerCodec`这些东西本身也是`handler` ### ChannelHandlerContext 1. 保存 `Channel` 相关的所有上下文信息,同时关联一个 `ChannelHandler` 对象 2. 即 `ChannelHandlerContext` 中包含一个具体的事件处理器 `ChannelHandler`,同时 `ChannelHandlerContext` 中也绑定了对应的 `pipeline` 和 `Channel` 的信息,方便对 `ChannelHandler` 进行调用。 3. 常用方法 - `ChannelFuture close()`,关闭通道 - `ChannelOutboundInvoker flush()`,刷新 - `ChannelFuture writeAndFlush(Object msg)`,将数据写到 - `ChannelPipeline` 中当前 `ChannelHandler` 的下一个 `ChannelHandler` 开始处理(出站) ### ChannelOption 1. `Netty` 在创建 `Channel` 实例后,一般都需要设置 `ChannelOption` 参数。 2. `ChannelOption` 参数如下: - `ChannelOption.SO_ BACKLOG`:对应TCP/IP协议listen函数中的backlog参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定了队列的大小。 - `ChannelOption.SO KEEPALIVE`:一直保持连接活动状态 ### Unpooled 类 1. `Netty` 提供一个专门用来操作缓冲区(即 `Netty` 的数据容器)的工具类 2. 常用方法 ```java //通过给定的数据和字符编码返回一个ByteBuf对象(类似于NIO中的ByteBuffer但有区别) public static ByteBuf copiedBuffer(CharSequence string, Charset charset) ``` 3. 举例说明 `Unpooled` 获取 `Netty` 的数据容器 `ByteBuf` 的基本使用 ``` package com.atguigu.netty.buf; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; public class NettyByteBuf01 { public static void main(String[] args) { //创建一个ByteBuf //说明 //1. 创建 对象,该对象包含一个数组arr , 是一个byte[10] //2. 在netty 的buffer中,不需要使用flip 进行反转 // 底层维护了 readerindex 和 writerIndex //3. 通过 readerindex 和 writerIndex 和 capacity, 将buffer分成三个区域 // 0--->readerindex 已经读取的区域 // readerindex--->writerIndex , 可读的区域 // writerIndex ---> capacity, 可写的区域 ByteBuf buffer = Unpooled.buffer(10); for (int i = 0; i < 10; i++) { buffer.writeByte(i); } System.out.println("capacity=" + buffer.capacity());//10 //输出 // for(int i = 0; i就到这里吧知道protobuf是个啥就行了 .... 具体使用..鸽