Netty(一)IO模型
1. Netty介绍
Netty 是由JBOSS提供的一个Jave开源框架,是一个异步地、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络IO程序。
Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者P2P场景下的大量数据持续传输的应用。Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景。
Netty的应用场景
在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少。Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架调用。例如阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
Netty作为高性能的基础通信组件,提供了TCP/UDP和HTTP协议栈,方便定制和开发私有协议栈。地图服务器之间也可以方便地通过Netty进行高性能的通信。
Hadoop的高性能通信和序列化框架组件AVRO的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service是基于Netty框架二次封装实现。Spark、Flink也使用了Netty作为基础通信框架。
由于Netty基于的是NIO,为了更好的理解Netty,下面我们首先介绍IO模型的概念。
2. I/O模型
I/O模型简单的理解,就是用什么样的通道进行数据的发送和接受,很大程度上决定了程序通信的性能。Java支持3种网络编程模型(IO模式):BIO、NIO、AIO
- Java BIO(Blocking-IO):同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务端就需要启动一个线程进行处理。如果这个连接不做任何事情,会造成不必要的开销
- Java NIO(Non-Blocking-IO):同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
- Java AIO(Asynchronous IO):异步非阻塞,AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
NIO适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。Netty基于的是NIO。
AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。AIO暂未得到广泛应用。
2.1. BIO
BIO编程的简单流程:
- 服务端启动一个ServerSocket
- 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通信
- 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
- 如果有响应,客户端线程会等待请求结束后,再继续执行
2.2. NIO
NIO有3大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)。
NIO是面向缓冲区,或是面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可以在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
Java NIO的非阻塞模式,使一个线程从某channel发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞。所以直到数据变的可以读取之前,该线程可以继续做其他事情。非阻塞写也是如此,一个线程请求写入一下数据到某channel,但不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗的说,NIO可以做到用一个线程处理多个操作。假设有10000个请求过来,根据实际情况,可以分配50或100个线程来处理。不像之前的BIO那样必须得10000个处理。
HTTP 2.0 中使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP 1.1 大了好几个数量级。
NIO与BIO的比较:
- BIO以流的方式处理数据,而NIO以块的方式处理数据。块I/O的效率比流I/O高很多
- BIO是阻塞的,NIO是非阻塞
- BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作。数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求、数据到达等),因此使用单个线程就可以监听多个客户端通道)
NIO三大核心原理示意图:
此关系图说明:
- 每个Channel都会对应一个Buffer
- Selector对应一个线程,一个线程对应多个Channel
- 该图反应了有3个channel注册到该selector程序
- 程序切换到哪个channel是由事件决定的,Event就是一个重要概念
- Selector会根据不同的事件,在各个通道上切换
- Buffer就是一个内存块,底层是有一个数组
- 数据的读取写入是通过Buffer,这个和BIO是有本质不同的。BIO中要么是输入流,要么是输出流,不能双向的。但是NIO的Buffer是可读可写的,需要flip() 方法进行切换
- Channel是双向的,可以反应底层操作系统的情况,比如Linux底层的操作系统通道就是双向的
Buffer(缓冲区)
缓冲区本质上是一个可以读写数据的内存块(实际上操作的是一个数组),该对象提供一组方法,可以更方便的使用内存块。缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
// 举例说明 NIO 中 Buffer 的使用
// 创建一个 buffer,大小为5,即可以存放5个int
IntBuffer intBuffer = IntBuffer.allocate(5); // 向 buffer 中存放数据
for(int i = 0; i < intBuffer.capacity(); i++)
intBuffer.put(i * 2); // 从 buffer 取数据
// 将 buffer 转换,读写切换
// intBuffer.flip(); while(intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
Channel(通道)
NIO的Channel类似于流,但有些区别:
- Channel可以同时进行读写,而流只能读或只能写
- Channel可以实现异步读写数据
- Channel可以从缓冲区读取数据,也可以写入数据到缓冲区
BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的Channel是双向的,可以读、写。
Channel在NIO中是一个接口public interface Channel extends Closeable{},常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。
FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。
以FileChannel为例,FileChannel主要用于对本地文件进行IO操作,主要有4个方法:
- public int read(ByteBuffer dst),从Channel读取数据并放入缓冲区中
- public int write(ByteBuffer src),把缓冲区的数据写到Channel中
- public long transferFrom(ReadableByteChannel src, long position, long count),从目标Channel中复制数据到当前Channel => 例如做文件拷贝
- public long transferTo(long position, long count, WritableByteChannel target),把数据从当前Channel复制给目标Channel
使用样例:
String str = "hello, File Channel"; // 创建一个输出流 -> channel
FileOutputStream fileOutputStream = new FileOutputStream("data/file01.txt"); // 通过 fileOutputStream 获取对应的 FileChannel
// 其实FileChannel是被FileOutputStream包了一层
// fileChannel 的类型其实是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel(); // 创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 将 str 放入 byteBuffer
byteBuffer.put(str.getBytes()); // buffer flip
byteBuffer.flip(); // 将 buffer 数据写入到 fileChannel
fileChannel.write(byteBuffer); fileOutputStream.close();
两个文件之间copy数据:
FileInputStream fileInputStream = new FileInputStream("data/file01.txt");
FileChannel fileChannel1 = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("data/file02.txt");
FileChannel fileChannel2 = fileOutputStream.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(512); while (true){
buffer.clear();
int read = fileChannel1.read(buffer); if (read == -1) break; buffer.flip();
fileChannel2.write(buffer);
} fileInputStream.close();
fileOutputStream.close();
关于Buffer和Channel的注意事项:
- ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException异常
- 可以将一个普通Buffer转成只读Buffer
- NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由NIO来完成
- 前面我们讲的读写操作,都是通过一个Buffer完成的,NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即Scattering和Gatering
Selector(选择器)
Jave的NIO使用的是非阻塞IO方式,可以用一个线程处理多个客户端的连接,就会使用到Selector。
Selector能够检测多个注册的通道上是否有事件发生(注意:多个channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件,然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个channel,也就是管理多个连接和请求。
只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。避免了多线程之间的上下文切换导致的开销。
Selector示意图:
特点说明:
- Netty的IO线程NioEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接
- 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
- 线程通常将非阻塞IO的空闲时间用于在其他channel上执行IO操作,所以单独的线程可以管理多个输入和输出通道
- 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起
- 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升
NIO中的ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket。Selector相关方法:
- selector.select() // 阻塞
- selector.select(1000) // 阻塞1000ms,在1000ms后返回
- selector.wakeup() // 唤醒 selector
- selector.selectNow() // 不阻塞,立马返还
1. 当客户端连接时,会通过ServerSocketChannel得到对应的SocketChannel
2. selector开始监听
3. 将SocketChannel注册到Selector上,register(Selector sel, int ops),一个selector上可以注册多个SocketChannel
4. 注册后返回一个SelectionKey,会和该Selector关联(以集合的方式)
5. Selector进行监听,select()方法,返回有事件发生的通道的个数
6. 进一步得到各个SelectionKey(有事件发生的)
7. 再通过SelectionKey反向获取SocketChannel,channel()
8. 可以通过得到的channel,完成业务处理
服务端代码:
package com.tang.nio; import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Set; public class NIOServer { public static void main(String[] args) throws Exception{ // 创建 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 得到一个 Selector 对象
Selector selector = Selector.open(); // 绑定一个端口,在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 设置为非阻塞
serverSocketChannel.configureBlocking(false); // 把 serverSocketChannel 注册到 selector,关心事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 循环等待客户端连接
while (true){ // 等待1秒,如果没有事件发生,返回
if(selector.select(1000) == 0){ // 没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
} // 如果返回的 >0,就获取到相关的 SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 遍历 Set<SelectionKey>
for (SelectionKey key : selectionKeys){ // 根据 key 对应的通道发生的事件做相应处理
if(key.isAcceptable()){ // 如果是 OP_ACCEPT,有新的客户端连接
// 该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功,生成一个socketChannel:" + socketChannel.hashCode()); // 将 socketChannel 设置为非阻塞
socketChannel.configureBlocking(false); // 将当前的 socketChannel 注册到 selector,关注事件为 OP_READ,同时给 SocketChannel 关联一个 Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } if(key.isReadable()){ // 发生 OP_READ 事件
// 通过key 反向获取到对应 channel
SocketChannel channel = (SocketChannel)key.channel();
// 获取到该 channel 关联的 buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("from client: " + new String(buffer.array()));
} // 手动从集合中移除当前的 selectionKey,防止重复操作
selectionKeys.remove(key);
} } }
}
客户端代码:
package com.tang.nio; import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel; public class NIOClient { public static void main(String[] args) throws Exception{
// 得到一个网络Channel
SocketChannel socketChannel = SocketChannel.open(); // 设置非阻塞模式
socketChannel.configureBlocking(false); // 提供服务器端 ip 与 端口
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6666); // 连接服务器
if (!socketChannel.connect(socketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作...");
}
} // 如果连接成功,发送数据
String str = "hello NIO~";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes()); // 发送数据,将buffer数据写入 channel
socketChannel.write(buffer); // 让代码停在这
System.in.read(); }
}
SelectionKey
SelectionKey,表示Selector和网络通道的注册关系,共4种:
int OP_ACCEPT:有新的网络连接可以 accept,值为16
int OP_CONNECT:代表连接已经建立,值为8
int OP_READ:代表读操作,值为1
int OP_WRITE:代表写操作,值为4
ServerSocketChannel
在服务端监听新的客户端Socket连接。
SocketChannel
SocketChannel,网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
2.3. NIO 与零拷贝
零拷贝是网络编程的关键,很多性能优化都离不开。在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。
以下是传统 IO 模型下,读取硬盘文件,并经由网络发出:
这里DMA是 Direct Memory Access,表示的是直接内存拷贝(不使用CPU)
mmap优化
mmap通过内存映射,将文件映射到内核缓冲区。同时,用户空间可以共享内核空间的数据。这样在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图所示:
mmap并不是零拷贝,只是优化后减少了一次拷贝。
sendFile优化
Linux2.1 版本提供了sendFile函数,其基本原理为:数据根本不经过用户态,直接从内核缓冲进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
如下图所示:
这种方法相较于之前的方式,减少了上下文切换以及拷贝次数,但仍未实现真正零拷贝。
零拷贝并不是指没有任何拷贝,而是没有CPU拷贝。
在Linux 2.4 版本做了修改,避免了从内核缓冲区拷贝到Socket buffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
如下图所示:
这里其实还是有一次 cpu 拷贝的,从kernel buffer 到 socket buffer。但是拷贝的信息很少,例如kernel buffer 的 length、offset,消耗低,可以忽略。
零拷贝
提到零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有kernel buffer有一份数据)。
零拷贝不仅是带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算。
mmap与sendFile的区别
mmap适合小数据量读写,sendFile适合大文件传输。
sendFile可以利用DMA方式,减少CPU拷贝,mmap不能(必须从内核拷贝到Socket缓冲区)。
NIO零拷贝
NIO零拷贝使用的是transferTo实现
2.4. AIO
JDK 7 引入了Asynchronous I/O,即AIO。在进行I/O编程中,常用的2种模式:Reactor和Proactor。Java的NIO就是Reactor:当有事件触发时,服务器端得到通知,进行相应的处理。
AIO即NIO2.0,叫做异步非阻塞IO。AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
目前AIO还没有广泛应用,Netty也是基于NIO,而不是AIO。
2.5. BIO、NIO、AIO对比
举例:
- 同步阻塞:到理发店理发,一直等待理发师,直到轮到自己理发
- 同步非阻塞:到理发店理发,发现前面有人在理发,给理发师说下,自己先干其他事情,一会过来看是否轮到自己
- 异步非阻塞:给理发师打电话,让理发师上门服务,自己干其他事情,理发师来家里给自己理发
------------------------------------
此 Netty 笔记为学习尚硅谷韩老师讲的 Netty 整理完成,原视频讲解十分详细,建议对 Netty 框架感兴趣的同学们可以看一遍原视频:
https://www.bilibili.com/video/BV1DJ411m7NR
Netty(一)IO模型的更多相关文章
- Netty学习(1):IO模型之BIO
概述 Netty其实就是一个异步的.基于事件驱动的框架,其作用是用来开发高性能.高可靠的IO程序. 因此下面就让我们从Java的IO模型来逐步深入学习Netty. IO模型 IO模型简单来说,就是采用 ...
- 聊聊Netty那些事儿之从内核角度看IO模型
从今天开始我们来聊聊Netty的那些事儿,我们都知道Netty是一个高性能异步事件驱动的网络框架. 它的设计异常优雅简洁,扩展性高,稳定性强.拥有非常详细完整的用户文档. 同时内置了很多非常有用的模块 ...
- NetCore Netty 框架 BT.Netty.RPC 系列随讲 二 WHO AM I 之 NETTY/NETTY 与 网络通讯 IO 模型之关系?
一:NETTY 是什么? Netty 是什么? 这个问题其实百度上一搜一堆. 这是官方话的描述:Netty 是一个基于NIO的客户.服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个 ...
- Netty学习(2):IO模型之NIO初探
NIO 概述 前面说到 BIO 有着创建线程多,阻塞 CPU 等问题,因此为解决 BIO 的问题,NIO 作为同步非阻塞 IO模型,随 JDK1.4 而出生了. 在前面我们反复说过4个概念:同步.异步 ...
- 深入了解Netty【四】IO模型
引言 IO模型就是操作数据输入输出的方式,在Linux系统中有5大IO模型:阻塞式IO模型.非阻塞式IO模型.IO复用模型.信号驱动式IO模型.异步IO模型. 因为学习Netty必不可少的要了解IO多 ...
- Netty学习之IO模型
目录 1.1 同步.异步.阻塞.非阻塞 同步 VS 异步 同步 异步 阻塞 VS 非阻塞 阻塞 非阻塞 举例 ...
- netty详解之io模型
提起IO模型首先想到的就是同步,异步,阻塞,非阻塞这几个概念.每个概念的含义,解释,概念间的区别这些都是好理解,这里深入*nix系统讲一下IO模型. 在*nix中将IO模型分为5类. Blocking ...
- Java 网络 IO 模型
在进入主题之前先看个 Java 网络编程的一个简单例子:代码很简单,客户端和服务端进行通信,对于客户端的每次输入,服务端回复 get.注意,服务端可以同时允许多个客户端连接. 服务端端代码: // 创 ...
- Java IO模型
Linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符).而对一个Socket的读写也会有相应的描述 ...
- 服务器端网络编程之 IO 模型
引言 从 T 跳槽到 A 之后,我的编程语言也从 C++ 转为 了 Java.在 T 做的偏服务器端开发,而在 A 更偏向于业务开发.上周在 A 公司组内做了一个<服务器端高性能网络编程> ...
随机推荐
- Modelsim使用指南
Modelsim使用指南 本文讲述Modelsim的使用步骤. 添加一个测试文件,比如modulename.v. 编辑这个Verilog模块. 为了方便讲述,顶层模块名命名为"top&quo ...
- Swift中的Tuple类型
Swift中的Tuple类型可以包含任何值,并且这些值的类型可以互相不一样.Tuple本身比较简单,需要记得也就是访问Tuple的方式. 使用变量名访问 let http404Error = (404 ...
- SpringBoot 利用Timer 在指定时间2小时后执行任务
/** * @Description * @Author songwp * @Date 2022/8/5 12:51 * @Version 1.0.0 **/ @Component public cl ...
- 使用js有效括号匹配封装函数
点击查看代码 function isValidParentheses(str) { // 定义一个栈,用于存储待匹配的左括号 let stack = []; // 定义一个对象,用于快速判断括号是否成 ...
- PPO-KL散度近端策略优化玩cartpole游戏
其实KL散度在这个游戏里的作用不大,游戏的action比较简单,不像LM里的action是一个很大的向量,可以直接用surr1,最大化surr1,实验测试确实是这样,而且KL的系数不能给太大,否则惩罚 ...
- Spring 面向切面编程AOP 详细讲解
1. Spring 面向切面编程AOP 详细讲解 @ 目录 1. Spring 面向切面编程AOP 详细讲解 每博一文案 2. AOP介绍说明 2.1 AOP的七大术语 2.2 AOP 当中的 切点表 ...
- linux文件权限管理:文件权限类型,文件权限影响,设定文件权限,取消文件权限
目录 一.关于文件权限 二.查看文件权限 三.linux下常见文件类型 四.linux下常见的文件权限 五.权限对文件和目录的影响 六.文件的用户分类 七.更改文件的属主和属组 八.一个文件取消所有权 ...
- MFC之多字节和宽字节的总结
ANSI字符集 所支持的就是多字节的也叫窄字节,类型来说就对应char类型.Unicode字符集 也叫宽字符集 所支持的就是宽字符集,从类型上来说就是 wchar_t类型.gb2312是中国的编码, ...
- AIAGC导航(aiagc.com): 最全的AI工具导航网站
AIAGC导航是一个专注于AI人工智能工具网站推荐的导航网站,可以帮助大家发现最新.最好用.最有趣的AI绘画.AI智能写作助手.AI聊天机器人.AI配音.AI音乐.AI换脸等各种AI工具应用软件,让A ...
- 揭秘华为如此多成功项目的产品关键——Charter模板
很多推行IPD(集成产品开发)体系的公司在正式研发产品前,需要开发Charter,以确保产品研发方向的正确.Charter,即项目任务书或商业计划书.Charter的呈现标志着产品规划阶段的完成,能为 ...