Netty

什么是Netty

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

Netty的应用场景

  • 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用。

  • 典型的应用有:阿里分布式服务框架Dubbo 的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty 作为基础通信组件,用于实现各进程节点之间的内部通信

image-20230714230043140

  • 无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用

  • Netty作为高性能的基础通信组件,提供了TCP/UDP和 HTTP协议栈,方便定制和开发私有协议栈,账号登录服务器

  • 地图服务器之间可以方便的通过Netty进行高性能的通信

  • 经典的Hadoop的高性能通信和序列化组件(AVRP实现数据文件共享)的RPC框架,默认采用Netty进行跨界点通信

  • 它的Netty Service基于Netty框架二次封装实现。

BIO基础

  • Java BIO 就是传统的java io 编程,其相关的类和接口在 java.io

  • BlO(blockingl/0): 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。后有应用实例

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解

工作原理图

image-20230715011751369

BIO 执行流程

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

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Demo01 {
public static void main(String[] args) throws IOException {
//线程池机制


//思路
//1.创建一个线程池
ExecutorService threadPool = Executors.newCachedThreadPool();

ServerSocket serverSocket = new ServerSocket(6666);

System.out.println("服务已启动");

while (true){
//监听等待客户端连接
Socket accept = serverSocket.accept();
System.out.println("有客户连接到客户端咯");
//创建一个线程与之通信
threadPool.execute(()->{
handlerSocket(accept);
});
}
}


private static void handlerSocket(Socket socket) {
byte[] bytes = new byte[1024];
//通过Socket获取一个输入流
try {
InputStream inputStream = socket.getInputStream();
System.out.println("该客户端的线程名为:"+Thread.currentThread().getName());
while (true){
int read =inputStream.read(bytes);
if (read!=-1){
System.out.println(new String(bytes,0,read));
}//读取完毕
else {
break;
}
}

} catch (IOException e) {
e.printStackTrace();
}
finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}
}

当我们使用telnet进行测试时

image-20230716131801890

image-20230716131822881

发送字符串

1
send hello

image-20230716131951970

同时我们再开启一个客户端

image-20230716132025359

image-20230716132042863

同时也发送 hello

image-20230716132123381

所以上述的代码实验完美的体现了BIO的执行原理图

BIO所存在的问题

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

NIO基础

IO模型

IO模型基本说明

  • I/O模型简单的理解:就是用什么样的通道进行数据的发 送和接收,很大程度上决定了程序通信的性能
  • Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO
  • Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销【简单示意图】
  • Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理
  • NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络

non-blocking io非阻塞IO

  • Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。【后面有案例说明】
  • 通俗理解:NIO是可以做到用一个线 程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
  • HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

BIO和NIO区别

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

三大组件

NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)

selector Buffer Channel之间的关系

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

image-20230716172049264

Buffer

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

image-20230716172707030

Buffer类及其子类

在NIO中Buffer就是一个顶层父类,他是一个抽象类,类的层级图如下:image-20230716174716122

  • ByteBuffer,存储字节数据到缓冲区

  • ShorBuffer,存储字符串数据到缓冲区

  • CharBuffer,存储字符数据到缓冲区

  • lntBuffer,存储整数数据到缓冲区

  • TongBuffer,存储长整型数据到缓冲区

  • DoubleBuffer,存储小数到缓冲区F

  • loatBuffer,存储小数到缓冲区

Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:

image-20230716175755145

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo2 {
public static void main(String[] args) {
//创建一个缓冲区,大小可以存放5个Buffer
IntBuffer intBuffer = IntBuffer.allocate(5);
for(int i=0;i<intBuffer.capacity();i++){
intBuffer.put(i);
}

intBuffer.flip();

while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}

}
}
ByteBuffer

从前面可以看出对于Java中的基本数据类型(boolean除外),都有一个 Buffer类型与之相对应,最常用的自然是ByteBuffer类(二进制数据),该类的主要方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//创建直接缓冲区  
public static ByteBuffer allocateDirect(int var0) {
return new DirectByteBuffer(var0);
}
//设置缓冲区初始容量
public static ByteBuffer allocate(int var0) {
if (var0 < 0) {
throw new IllegalArgumentException();
} else {
return new HeapByteBuffer(var0, var0);
}
}
//把一个数组放到缓冲区中使用

public static ByteBuffer wrap(byte[] var0) {
try {
return new HeapByteBuffer(var0, var1, var2);
} catch (IllegalArgumentException var4) {
throw new IndexOutOfBoundsException();
}
}
//构造初始化位置offset和上界length的缓冲区

public static ByteBuffer wrap(byte[] var0, int var1, int var2) {
try {
return new HeapByteBuffer(var0, var1, var2);
} catch (IllegalArgumentException var4) {
throw new IndexOutOfBoundsException();
}
}
//缓存区存取相关的API
//获取当前position位置上的的数据,get后position+1
public abstract byte get();
//从绝对位置上get
public abstract byte get(int var1);
//从当前位置上put,put后position会+1
public abstract ByteBuffer put(byte var1);
//从绝对位置上put
public abstract ByteBuffer put(int var1, byte var2);

image-20230716182512170

Channel

  • NIO的通道类似于流,但有些区别如下:
    • 通道可以同时进行读写,而流只能读或者只能写
    • 通道可以实现异步读写数据
    • 通道可以从缓冲读数据,也可以写数据到缓冲:

image-20230716182623703

基本介绍
  • BIO中的stream是单向的,例如 FilelnputStream对象只能进行读取数据的操作,而NIO中的通道 (Channel)是双向的,可以读操作,也可以写操作。
  • Channel在NIO中是一个接口
  • public interface Channel extends Closeable0常用的 Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。
  • 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 transfer To(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
案例

案例:将字符串 写入到桌面

image-20230717085938332

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Demo2 {
public static void main(String[] args) throws IOException {
String str="123一二三";

FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Public\\Desktop\\asd");
//通过fileOutPutStream获取对应的FileChannel
FileChannel channel = fileOutputStream.getChannel();

//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将字符串放入缓冲区
byteBuffer.put(str.getBytes());
//翻转缓冲区position
byteBuffer.flip();
//将缓冲区数据写入到通道
try {
channel.write(byteBuffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
finally {
fileOutputStream.close();
}

}
}

image-20230717085435826

案例:在文件中读数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo03 {
public static void main(String[] args) throws IOException {
//创建文件对象
File file = new File("C:\\Users\\Public\\Desktop\\asd");
//创建文件输入流
FileInputStream fileInputStream=new FileInputStream(file);
//通过流来获取通道
FileChannel channel = fileInputStream.getChannel();
//创建缓冲区,定义缓存区大小
ByteBuffer buffer = ByteBuffer.allocate(((int)file.length()));
//从通道中读取数据到缓冲区
channel.read(buffer);
//获取缓冲区的数据
System.out.println(new String(buffer.array()));
fileInputStream.close();

}

}

image-20230717091658570

使用transferform对照片进行复制(用一个通道进行复制)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo05 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("3.jpg");
FileOutputStream fileOutputStream=new FileOutputStream("4.jpg");
FileChannel Outchannel = fileOutputStream.getChannel();
FileChannel inputChannel = fileInputStream.getChannel();

ByteBuffer buffer = ByteBuffer.allocate(1024);
Outchannel.transferFrom(inputChannel,0,inputChannel.size()); //将原通道的信息,复制到此通道
fileInputStream.close();
fileOutputStream.close();
}
}

image-20230717100617707

Buffer与Channel的使用

  • ByteBuffer支持类型化的put和 get, put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException异常。

  • 可以将一个普通Buffer转成只读Buffer(传入参数)buffer.asReadOnlyBuffer()

    • NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成.(零拷贝)
  • 前面我们讲的读写操作,都是通过一个Buffer完成的,NIO还支持通过多个Buffer(即 Buffer数组)完成读写操作,即Scattering 和 Gatering

scattering:将数据写入到buffer时,可以采用buffer数组,依次写入[分散]
Gathering: 从buffer读取数据时,可以采用buffer数组,依次读

selector

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

image-20230717222834380

epoll poll

NIO中实现多路复用的核心类是Selector,当多路复用器Selector调用select方法时,将会查找发生事件的channel,问题是,该如何在多个注册到selector上的channel中找到哪些channel发生了事件,此时NIO不同的版本有不同的做法。

image-20231005151731552

Reactor模型

不同的线程模型决定了程序的性能。在IO线程模型领域,并发编程之父Doug Lea提出了Reactor模型。Netty的线程模型就是基于Reactor模型设计的。

●传统的阻塞IO模型

传统的BIO模型中,服务端的一条线程必须处理完一个客户端的所有请求后才能处理另一个客户端的请求。

image-20231005151824199

●基于事件响应式的基础Reactor模型
基础版本的Reactor模型,由单线程逐一处理来自于多个客户端的不同事件,而不需要等待某一个客户端的请求全部执行完才能处理另一个客户端的请求。

image-20231005152148876

●提升业务处理能力的Reactor模型
在基础Reactor模型中,Reactor在处理某一个客户端的读请求时,其他请求依然需要阻塞等待,如何提升这一块的并发性呢,就可以使用线程池来提升处理某一个具体业务的并发操作。

image-20231005152554913

●多主多从的模式

如果只把业务处理模块交由多线程来解决,但是在实际场景中,有可能会面临成千上万客户端的连接,而这些客户端的连接请求只能等待Reactor完成业务处理后才能被处理。此时创建一个独立的Reactor专门负责处理客户端的连接请求,而且这样的Reactor也可以支持多线程,那么就能解决海量客户端连接的应用场景。

image-20231005154533571

Netty的线程模型

image-20231005160539546

image-20231005160549808

Netty核心组件

BootStrap与ServerBootStrap

Bootstrap是Netty的启动程序,一个Netty应用通常由一个Bootstrap开始。Bootstrap的主要作用是配置Netty程序,串联Netty的各个组件。

  • Bootstrap:客户端的启动程
  • ServerBootstrap:服务端启动程序

Future和ChannelFuture

Netty的所有操作都是异步的,即不能立刻得知消息是否被正确处理。因此需要通过Future和ChannelFuture来注册监听器,当操作执行成果或者失败来调用具体的监听器。
Future通过sync方法来获得同步执行的效果。
ChannelFuture和Future的子类,提供了针对于Channel的异步监听操作。

Channel

Channel是Netty网络通信的重要组件,用于执行网络IO操作。Channel具备以下能力∶

  • 获得当前网络连接通道的状态
  • 网络连接的配置参数
  • 提供异步的网络IO操作,比如建立连接、绑定端口、读写操作等
  • 获得ChannelFuture实例,并注册监听器到ChannelFuture上,用于监听IO操作的成功、失败、取消时的事件回调。

Channel具体的实现类有以下几种︰

  • NioSocketChannel:异步的客户端TCP Socket连接通道
  • NioServerSocketChannel:异步的服务端TCP Socket连接通道
  • NioDatagramChannel:异步的UDP连接通道
  • NioSctpChannel:异步的客户端Sctp连接通道
  • NioSctpServerChannel:异步的服务端Sctp连接通道

Selector

通过Selector多路复用器实现IO的多路复用。Selector可以监听多个连接的Channel事件,同时可以不断的查询已注册Channel是否处于就绪状态,实现一个线程可以高效的管理多个Channel。

NioEventLoop

NioEventLoop内部维护了一个线程和任务队列,支持异步提交执行任务。当线程启动时会调用NioEventLoop的run方法来执行io任务或非io任务。

  • io任务︰如accept、connect、read、write事件等,由processSelectedKeys方法触发。
  • 非io任务:如register0、bindO等任务将会被添加到taskQueue任务队列中,由runAllTasks方法触发。

NioEventLoopGroup

管理NioEventLoop的生命周期,可以理解为是线程池,内部维护了一组线程。每个线程(即NioEventLoop)负责处理多个Channel上的事件。注意,一个Channel只对应一个线程,NioEventLoop和Channel它们是一对多的关系。

ByteBuf

ByteBuf优化了ByteBuffer的使用逻辑。ByteBuf底层是一个字节数组组成,它提供了两个指针分别标识可读的位置和可写的位置,配合capacity容量属性,将数组划分成三个区域,分别为∶

  • 已经读取的区域:[O, readerlndex)
  • 可读取的区域:[readerlndex,writerlndex)
  • 可写的区域:「writerlndex,capacity)

其方法不同的为如果buf使用getBytes()或setBytes() 它的writerIndex与readerIndex不变

而使用writeBytes()与readBytes() 其writerIndex与readerIndex会变化

ChannelHandler

  • ChannelHandler用于处理拦截IO事件,往往在ChannelHandler中可以加入业务处理逻辑。
  • ChannelHandler执行完后会将IO事件转发到ChannelPipeline中的下一个处理程序。

ChannelHandlerContext

保存Channel相关的上下文信息,并关联一个ChannelHandler对象。

ChannelPipeline

ChannelPipeline是一个双向链表,其中保存着多个ChannelHandler。ChannelPipeline实现了一种高级形式的过滤器模式,在IO操作时发生的入站和出站事件,会导致ChannelPipeline中的多个ChannelHandler被依次调用。

Netty心跳机制及断线重连

心跳机制

在分布式系统中,心跳机制常常在注册中心组件中提及,比如Zookeeper、Eureka、Nacos等,通过维护客户端的心跳,来判断客户端是否正常在线。如果客户端达到超时次数等预设的条件时,服务端将释放客户端的连接资源。

Netty在TCP的长连接中,客户端定期向服务端发送一种特殊的数据包,告知对方自己正常在线,以确保TCP连接的有效性。Netty实现心跳的关键需要通过ldleStateHandler。当ldleStateHandler类描述三种空闲的状态:

  • 读空闲∶在指定时间间隔内没有从Channel中读到数据,将会创建状态为READER_IDLE的IdleStateEvent对象。
  • 写空闲∶在指定时间间隔内没有数据写入到Channel中,将会创建状态为WRITER_IDLE的lIdleStateEvent对象。
  • 读写空闲︰在指定时间间隔内Channel中没有发生读写操作,将会创建状态为ALL_IDLE的IdleStateEvent对象。

创建ldleStateHandler对象时,IdleStateHandler会初始化定时任务的线程,用于在指定延时时间后执行相关的处理。该线程将满足条件的超时事件ldleStateEvent对象传递给pipeline中下一个handler。