Netty的介绍:

Netty 是由 JBOSS 提供的一个 Java 开源框架,现为 Github上的独立项目。Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可 靠性的网络 IO 程序。Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下 的大量数据持续传输的应用。Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景要透彻理解Netty , 需要先学习 NIO , 这样我们才能阅读 Netty 的源码。

IO模型:

I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程 序通信的性能Java共支持3种网络编程模型/IO模式:BIO、NIO、AIOJava BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端 有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成 不必要的线程开销 【简单示意图】Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发 送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理Java AIO(NIO.2) : 异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简 化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程

适用场景分析

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕 系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分 调用OS参与并发操作,编程比较复杂,JDK7开始支持。

Java BIO 基本介绍:

Java BIO 就是传统的java io 编程,其相关的类和接口在 java.ioBIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连 接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造 成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。 【后有 应用实例】BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解

Java BIO 工作机制:

BIO编程简单流程:

服务器端启动一个ServerSocket客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯客户端发出请求后, 先咨询服务器 是否有线程响应,如果没有则会等 待,或者被拒绝如果有响应,客户端线程会等待请 求结束后,在继续执行

Java BIO 问题分析:

每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。当并发数较大时,需要创建大量线程来处理连接,系统资源占 用较大。连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞 在 Read 操作上,造成线程资源浪费

Java NIO 基本介绍:

Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。 从 JDK1.4 开始,Java 提供了一系列改进的输入/输出 的新特性,被统称为 NIO(即 New IO),是同步非阻塞的 NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。 NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器) NIO是 面向缓冲区 ,或者面向 块 编程的。 数据读取到一个 它稍后处理的缓冲区,需要时可在缓冲区中前后移动, 这就 增加了处理过程中的灵活性,使用它可以提供非阻塞式的高 伸缩性网络 Java NIO的非阻塞模式, 使一个线程从某通道发送请求或者读取数据,但是它仅能得 到目前可用的数据, 如果目前没有数据可用时,就什么都不会获取,而不是保持线 程阻塞, 所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞 写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这 个线程同时可以去做别的事情。 通俗理解: NIO是可以做到用一个线程来处理多个操作的。 假设有10000个请求过来, 根据实际情况,可以分配50或者100个线程来处理。 不像之前的阻塞IO那样,非得分 配10000个。 HTTP2.0使用了多路复用的技术, 做到同一个连接并发处理多个请求,而且并发请求 的数量比HTTP1.1大了好几个数量级。

NIO 和 BIO 的比较:

BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很 多BIO 是阻塞的,NIO 则是非阻塞的BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进 行操作, 数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。 Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因 此使用单个线程就可以监听多个客户端通道

NIO 三大核心原理示意图:

每个channel 都会对应一个BufferSelector 对应一个线程, 一个线程对应多个channel(连接)该图反应了有三个channel 注册到 该selector //程序程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念Selector 会根据不同的事件,在各个通道上切换Buffer 就是一个内存块 , 底层是有一个数组数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是 输出流, 不能双向, 但是NIO的Buffer 是可以读也可以写, 需要flip 方法切换channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统 通道就是双向的

缓冲区(Buffer):

基本介绍: 缓冲区(Buffer): 缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个 容器对象(含数组), 该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对 象内置了一些机制, 能够跟踪和记录缓冲区的状态变化情况。 Channel 提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,如图:

Buffer 类及其子类:

在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类, 类的层级关系图

Buffer 类及其子类:

Buffer 类及其子类 2) Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素 的信息: Buffer 类及其子类 3) Buffer类相关方法 一览: ByteBuffer: 从前面可以看出对于 Java 中的基本数据类型(boolean除外), 都有一个 Buffer 类型与之 相对应,最常用的自然是ByteBuffer 类(二进制数据), 该类的主要方法如下

通道(Channel):

NIO的通道类似于流,但有些区别如下: • 通道可以同时进行读写,而流只能读或者只能写 • 通道可以实现异步读写数据 • 通道可以从缓冲读数据,也可以写数据到缓冲:BIO 中的 stream 是单向的,例如 FileInputStream 对 象只能进行读取数据的操作,而 NIO 中的通道 (Channel)是双向的, 可以读操作,也可以写操作。Channel在NIO中是一个接口 public interface Channel extends Closeable{}常用的 Channel 类有: FileChannel、 DatagramChannel、ServerSocketChannel 和 SocketChannel。 【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】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),把数据从当 前通道复制给目标通道

写:

public static void main(String[] args) throws Exception{

String str = "hello,NETTY";

//创建一个输出流->channel

FileOutputStream fileOutputStream = new FileOutputStream("/Desktop/微服务/Netty相关资料/代码/NettyPro/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();

}

读:

public static void main(String[] args) throws Exception {

//创建文件的输入流

File file = new File("/Desktop/微服务/Netty相关资料/代码/NettyPro/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();

}

读—》buffer----》写

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();

}

public static void main(String[] args) throws Exception {

//创建相关流

FileInputStream fileInputStream = new FileInputStream("/Desktop/微服务/Netty相关资料/代码/NettyPro/a.jpg");

FileOutputStream fileOutputStream = new FileOutputStream("/Desktop/微服务/Netty相关资料/代码/NettyPro/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();

}

关于Buffer 和 Channel的注意事项和细节:

ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用 相应的数据类型来取出, 否则可能有BufferUnderflowException 异常可以将一个普通Buffer 转成只读Buffer

public static void main(String[] args) {

//创建一个buffer

ByteBuffer buffer = ByteBuffer.allocate(64);

for(int i = 0; i < 64; i++) {

buffer.put((byte)i);

}

//读取

buffer.flip();

//得到一个只读的Buffer

ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

System.out.println(readOnlyBuffer.getClass());

//读取

while (readOnlyBuffer.hasRemaining()) {

System.out.println(readOnlyBuffer.get());

}

readOnlyBuffer.put((byte)100); //ReadOnlyBufferException

}

NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进 行修改, 而如何同步到文件由NIO 来完成.

/*

说明

1. MappedByteBuffer 可让文件直接在内存(堆外内存)修改, 操作系统不需要拷贝一次

*/

public class MappedByteBufferTest {

public static void main(String[] args) throws Exception {

RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");

//获取对应的通道

FileChannel channel = randomAccessFile.getChannel();

/**

* 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式

* 参数2: 0 : 可以直接修改的起始位置

* 参数3: 5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存

* 可以直接修改的范围就是 0-5

* 实际类型 DirectByteBuffer

*/

MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

mappedByteBuffer.put(0, (byte) 'H');

mappedByteBuffer.put(3, (byte) '9');

// mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException

randomAccessFile.close();

System.out.println("修改成功~~");

}

}

前面我们讲的读写操作,都是通过一个Buffer 完成的, NIO 还支持 通过多个 Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.util.Arrays;

/**

* Scattering:将数据写入到buffer时,可以采用buffer数组,依次写入 [分散]

* Gathering: 从buffer读取数据时,可以采用buffer数组,依次读

*/

public class ScatteringAndGatheringTest {

public static void main(String[] args) throws Exception {

//使用 ServerSocketChannel 和 SocketChannel 网络

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);

//绑定端口到socket ,并启动

serverSocketChannel.socket().bind(inetSocketAddress);

//创建buffer数组

ByteBuffer[] byteBuffers = new ByteBuffer[2];

byteBuffers[0] = ByteBuffer.allocate(5);

byteBuffers[1] = ByteBuffer.allocate(3);

//等客户端连接(telnet)

SocketChannel socketChannel = serverSocketChannel.accept();

int messageLength = 8; //假定从客户端接收8个字节

//循环的读取

while (true) {

int byteRead = 0;

while (byteRead < messageLength ) {

long l = socketChannel.read(byteBuffers);

byteRead += l; //累计读取的字节数

System.out.println("byteRead=" + byteRead);

//使用流打印, 看看当前的这个buffer的position 和 limit

Arrays.asList(byteBuffers).stream().map(buffer -> "postion=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println);

}

//将所有的buffer进行flip

Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());

//将数据读出显示到客户端

long byteWirte = 0;

while (byteWirte < messageLength) {

long l = socketChannel.write(byteBuffers); //

byteWirte += l;

}

//将所有的buffer 进行clear

Arrays.asList(byteBuffers).forEach(buffer-> {

buffer.clear();

});

System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messagelength" + messageLength);

}

}

}

Selector(选择器):

基本介绍: 1.java的NIO,用的是非阻塞的IO方式。可以用一个线程,处理多个客户端连接,就会使用selector选择器 2) Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以 事件的方式可以注册到同一个Selector), 如果有事件发生,便获取事件然 后针对每个事件进行相应的处理。 这样就可以只用一个单线程去管理多个 通道,也就是管理多个连接和请求。 3) 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少 了系统开销, 并且不必为每个连接都创建一个线程,不用去维护多个线程 4) 避免了多线程之间的上下文切换导致的开销

特点再说明:

Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器, 也叫多路复用器),可以同时并发处理成百上千个客 户端连接。当线程从某客户端 Socket 通道进行读写数据时,若没 有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上 执行 IO 操作,所以单独的线程可以管理多个输入和 输出通道。由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程 挂起。一个 I/O 线程可以并发处理 N 个客户端连接和读写操 作,这从根本上解决了传统同步阻塞 I/O 一连接一线 程模型,架构的性能、弹性伸缩能力和可靠性都得到 了极大的提升

Selector类相关方法:

注意事项:

NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类 似Socketselector 相关方法说明 selector.select()//阻塞 selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回 selector.wakeup();//唤醒selector selector.selectNow();//不阻塞,立马返还

NIO 非阻塞 网络编程原理分析图:

当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannelSelector 进行监听 select 方法, 返回有事 件发生的通道的个数将socketChannel注册到Selector上, register(Selector sel, int ops), 一个 selector上可以注册多个SocketChannel注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)进一步得到各个 SelectionKey (有事件发 生)在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()可以通过 得到的 channel , 完成业务处 理

server:

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.*;

import java.util.Iterator;

import java.util.Set;

public class NIOServer {

public static void main(String[] args) throws Exception{

//创建ServerSocketChannel -> ServerSocket

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

//得到一个Selecor对象

Selector selector = Selector.open();

//绑定一个端口6666, 在服务器端监听

serverSocketChannel.socket().bind(new InetSocketAddress(6666));

//设置为非阻塞

serverSocketChannel.configureBlocking(false);

//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1

//循环等待客户端连接

while (true) {

//这里我们等待1秒,如果没有事件发生, 返回

if(selector.select(1000) == 0) { //没有事件发生

System.out.println("服务器等待了1秒,无连接");

continue;

}

//如果返回的>0, 就获取到相关的 selectionKey集合

//1.如果返回的>0, 表示已经获取到关注的事件

//2. selector.selectedKeys() 返回关注事件的集合

// 通过 selectionKeys 反向获取通道

Set selectionKeys = selector.selectedKeys();

System.out.println("selectionKeys 数量 = " + selectionKeys.size());

//遍历 Set, 使用迭代器遍历

Iterator keyIterator = selectionKeys.iterator();

while (keyIterator.hasNext()) {

//获取到SelectionKey

SelectionKey key = keyIterator.next();

//根据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));

System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..

}

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("form 客户端 " + new String(buffer.array()));

}

//手动从集合中移动当前的selectionKey, 防止重复操作

keyIterator.remove();

}

}

}

}

client:

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SocketChannel;

public class NIOClient {

public static void main(String[] args) throws Exception{

//得到一个网络通道

SocketChannel socketChannel = SocketChannel.open();

//设置非阻塞

socketChannel.configureBlocking(false);

//提供服务器端的ip 和 端口

InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);

//连接服务器

if (!socketChannel.connect(inetSocketAddress)) {

while (!socketChannel.finishConnect()) {

System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");

}

}

//...如果连接成功,就发送数据

String str = "hello, 尚硅谷~";

//Wraps a byte array into a buffer

ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());

//发送数据,将 buffer 数据写入 channel

socketChannel.write(buffer);

System.in.read();

}

}

SelectionKey:

SelectionKey,表示 Selector 和网络通道的注册关系, 共四种: int OP_ACCEPT:有新的网络连接可以 accept,值为 16 int OP_CONNECT:代表连接已经建立,值为 8 int OP_READ:代表读操作,值为 1 int OP_WRITE:代表写操作,值为 4 源码中: public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4; 2) SelectionKey相关方法:

ServerSocketChannel:

serverSocketChannel:在服务器端监听新的客户端socket连接

SocketChannel:

SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通 道,或者把通道里的数据读到缓冲区。

NIO与零拷贝:

零拷贝是网络编程的关键,很多性能优化都离不开。在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile 传统IO数据读写 传统IO

mmap 优化:

mmap 通过内存映射,将文件映射到内核缓冲区,同时, 用户空间可以共享内核 空间的数据。这样,在进行网络传输时, 就可以减少内核空间到用户控件的拷贝 次数。

sendFile 优化:

Linux 2.1 版本 提供了 sendFile 函数,其基本 原理如下: 数据根本不 经过用户态,直接从内 核缓冲区进入到 Socket Buffer, 同时,由于和用 户态完全无关,就减少 了一次上下文切换

提示:零拷贝从操作系统角 度,是没有cpu 拷贝

sendFile 优化: 3) Linux 在 2.4 版本中,做了 一些修改,避免了从内核 缓冲区拷贝到 Socket buffer 的操作, 直接拷贝到 协议栈,从而再一次减少 了数据拷贝。 这里其实有 一次cpu 拷贝 kernel buffer -> socket buffer 但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以 忽略

零拷贝的再次理解:

我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是 重复的(只有 kernel buffer 有一份数据)。零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下 文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

mmap 和 sendFile 的区别:

mmap 适合小数据量读写,sendFile 适合大文件传输mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最 少 2 次数据拷贝。sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝 到 Socket 缓冲区)。

BIO、NIO、AIO对比表:

好文推荐

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: