2021年12月
北京
xxd
一、Netty是什么
- Netty 是由 JBOSS 提供的一个 Java 开源框架,现为 Github 上的独立项目。
- Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络 IO 程序。
- Netty 主要针对在 TCP 协议下,面向 Client 端的高并发应用,或者 Peer-to-Peer 场景下的大量数据持续传输的应用。
- Netty 本质是一个 NIO 框架,适用于服务器通讯相关的多种应用场景。
- 要透彻理解 Netty,需要先学习 NIO,这样我们才能阅读 Netty 的源码。
二、java IO模型
I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
Java 共支持 3 种网络编程模型 I/O 模式:BIO、NIO、AIO。
适用场景
- BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序简单易理解。
- NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
- AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
BIO
事件驱动 多路复用
Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
Java BIO 就是传统的 Java I/O 编程,其相关的类和接口在 java.io。
BIO(BlockingI/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。
public class BIOServerTest {
public static void main(String[] args) throws IOException {
//创建线程池 如果有客户端连接 创建线程进行通信
ExecutorService executorService = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了.....");
while(true){
//监听 等待客户端连接
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
executorService.execute(new Runnable() {
public void run() {
//客户端通信
handel(socket);
}
});
}
}
public static void handel(Socket socket){
byte[] bytes = new byte[1024];
try {
System.out.println("Thread id: "+Thread.currentThread().getId()+"Thread name: "+Thread.currentThread().getName());
InputStream in = socket.getInputStream();
while (true){
int read = in.read(bytes);
if(read != -1){
//将数据读取到bytes数组中 然后转为string打印
System.out.println(new String(bytes,0,read));
}else{
System.out.println("本次读取数据完毕..");
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//在cmd中 使用telnet 127.0.0.1 6666 进行连接
//使用 ctrl + ] 快捷键发送可以发送数据到服务端
//每次启动一个cmd窗口 就会打开一个新的线程
//输出结果
服务器启动了
连接到一个客户端
Thread id: 11Thread name: pool-1-thread-1
ok100
ok200
连接到一个客户端
Thread id: 12Thread name: pool-1-thread-2
hello100
hello200
出现的问题:
- 每个请求都需要创建独立的线程,与对应的客户端进行数据
Read
,业务处理,数据Write
。- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在
Read
操作上,造成线程资源浪费。
NIO
Java NIO
:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O
请求就进行处理。
NIO有三大核心部分,Channel(通道),Buffer(缓冲区),Selecter(选择器),NIO是面向缓冲区,面向块的
每个channel对应一个buffer
每个selector对应一个线程,一个线程可以对应多个channel(连接)
channel会对应着注册到selector中
selector切换到哪个channel哪个channel是由事件Event决定的。
selector会根据不同的事件,在各个通道上面切换。
buffer就是一个内存块,底层是由一个数组,阻塞队列;
在NIO里面数据的读取都是需要通过Buffer,Buffer是可以读也可以写的;BIO中都是直接对数据进行读写的,要么是输入流要么是输出流,不能双向。
channel是不阻塞的双向的,可以很好地反映底层的操作系统,例如linux底层的操作系统通道就是双向的。
1、Buffer
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
Buffer 类是 java.nio 的构造基础。一个 Buffer 对象是固定数量的数据的容器,其作用是一个存储器,或者分段运输区,在这里,数据可被存储并在之后用于检索。缓冲区可以被写满或释放。对于每个非布尔原始数据类型都有一个缓冲区类,即 Buffer 的子类有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer,是没有 BooleanBuffer 之说的。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的.
缓冲区buffer的四个属性
所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息,这四个属性尽管简单,但其至关重要,需熟记于心:
容量(Capacity):
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
上界(Limit):
缓冲区的第一个不能被读或写的元素。缓冲创建时,limit 的值等于 capacity 的值。假设 capacity = 1024,我们在程序中设置了 limit = 512,说明,Buffer 的容量为 1024,但是从 512 之后既不能读也不能写,因此可以理解成,Buffer 的实际可用大小为 512。
位置(Position):
下一个要被读或写的元素的索引。位置会自动由相应的 get() 和 put() 函数更新。 这里需要注意的是positon的位置是从0开始的。
标记(Mark):
一个备忘位置。标记在设定前是未定义的(undefined)。使用场景是,假设缓冲区中有 10 个元素,position 目前的位置为 2(也就是如果get的话是第三个元素),现在只想发送 6 - 10 之间的缓冲数据,此时我们可以 buffer.mark(buffer.position()),即把当前的 position 记入 mark 中,然后 buffer.postion(6),此时发送给 channel 的数据就是 6 - 10 的数据。发送完后,我们可以调用 buffer.reset() 使得 position = mark,因此这里的 mark 只是用于临时记录一下位置用的。
// 牢记这一点: mark <= position <= limit <= capacity
// 标记值,使用 mark() 方法之后,记录position的值,再使用reset()方法,将记录的mark值重新赋值给position
private int mark = -1;
// 在写的模式下,position值等于已经写入数据最大位置值,在读的模式下,position值等于已经读过数据最大位置值,
private int position = 0;
// 在写的模式下,limit值等于capacity;在读的模式下,limit值等于已经写入数据最大位置值。
private int limit;
// 总容量值
private int capacity;
// 这个值只有在 direct buffer 中使用。表示这个 direct buffer 的内存地址。
long address;
常用方法
get 系列方法
public abstract byte get();
获取当前位置(即position + offset
)的数据,并将 position
的值增加1
public abstract byte get(int index);
获取给与位置(即position + offset + index
)的数据。但是千万注意不会改变 position
的值。
例子:
public static void main(String[] args) throws Exception {
FileChannel fileChannel = aFile.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
for (int index = 0; index < 10; index++) {
byteBuffer.put((byte) ('0' + index));
}
// 一定要调用这个方法。
byteBuffer.flip();
System.out.println((char)byteBuffer.get());
System.out.println((char)byteBuffer.get(5));
System.out.println((char)byteBuffer.get());
}
运行结果:
0
5
1
可以看出 get(int index)
方法不会改变 position
的值。
public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
}
public ByteBuffer get(byte[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
if (length > remaining())
throw new BufferUnderflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
dst[i] = get();
return this;
}
将Buffer
中的一些数据写入到给与的 dst
数组中。如果想将Buffer
中写入数据全部读取到一个byte[]
,可以这么做:
public static void main(String[] args) throws Exception {
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
for (int index = 0; index < 10; index++) {
byteBuffer.put((byte) ('0' + index));
}
// 一定要调用这个方法。
byteBuffer.flip();
// byteBuffer.remaining() 可以得到读取数据的大小
byte[] dst = new byte[byteBuffer.remaining()];
byteBuffer.get(dst);
System.out.println(new String(dst));
}
put 系列方法
public abstract ByteBuffer put(byte b);
在当前位置(即position + offset
)写入数据b
,并将 position
的值增加1
public abstract ByteBuffer put(int index, byte b);
在给与位置(即position + offset + index
)写入数据b
。但是千万注意不会改变 position
的值。
public final ByteBuffer put(byte[] src) {
return put(src, 0, src.length);
}
public ByteBuffer put(byte[] src, int offset, int length) {
checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
this.put(src[i]);
return this;
}
将给与的byte[]
数组src
中的数据写入到本Buffer
中。
public ByteBuffer put(ByteBuffer src) {
if (src == this)
throw new IllegalArgumentException();
if (isReadOnly())
throw new ReadOnlyBufferException();
int n = src.remaining();
if (n > remaining())
throw new BufferOverflowException();
for (int i = 0; i < n; i++)
put(src.get());
return this;
}
将给与缓冲区src
中的数据写入到本Buffer
中。
public static void main(String[] args) throws Exception {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将数据写入 byteBuffer 中
byteBuffer.put("你好世界".getBytes());
// 切换成 读模式
byteBuffer.flip();
// 将byteBuffer中的数据读取到 dst 中
byte[] dst = new byte[byteBuffer.remaining()];
byteBuffer.get(dst);
System.out.println(new String(dst));
}
通过 wrap()
方法创建 HeapByteBuffer
对象
public static ByteBuffer wrap(byte[] array) {
return wrap(array, 0, array.length);
}
public static ByteBuffer wrap(byte[] array,
int offset, int length)
{
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
HeapByteBuffer(byte[] buf, int off, int len) {
super(-1, off, off + len