remove netty
21
README.md
|
@ -75,7 +75,6 @@
|
|||
* [常用框架](#常用框架)
|
||||
* [Spring/SpringBoot](#springspringboot)
|
||||
* [MyBatis](#mybatis)
|
||||
* [Netty](#netty-1)
|
||||
* [认证授权](#认证授权)
|
||||
* [JWT](#jwt)
|
||||
* [SSO(单点登录)](#sso单点登录)
|
||||
|
@ -181,26 +180,6 @@
|
|||
1. [计算机网络常见面试题](docs/network/计算机网络.md)
|
||||
2. [计算机网络基础知识总结](docs/network/干货:计算机网络知识总结.md)
|
||||
|
||||
## Netty
|
||||
|
||||
- [Netty简介](docs/netty/Netty简介.md)
|
||||
|
||||
- [Netty特性](docs/netty/Netty特性.md)
|
||||
|
||||
- [Netty组件](docs/netty/Netty组件.md)
|
||||
|
||||
- [Transport传输](docs/netty/Transport传输.md)
|
||||
|
||||
- [ByteBuf容器](docs/netty/ByteBuf容器.md)
|
||||
|
||||
- [ChannelHandler和ChannelPipeline](docs/netty/ChannelHandler和ChannelPipeline.md)
|
||||
|
||||
- [Netty线程模型和EventLoop事件循环](docs/netty/Netty线程模型和EventLoop.md)
|
||||
|
||||
- [Bootstrap引导](docs/netty/Bootstrap引导.md)
|
||||
|
||||
- [Codec编码与解码](docs/netty/Codec编码与解码.md)
|
||||
|
||||
|
||||
## 操作系统
|
||||
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
<!-- TOC -->
|
||||
|
||||
* [Bootstrap引导](#bootstrap引导)
|
||||
* [Bootstrap类](#bootstrap类)
|
||||
* [引导客户端和无连接协议](#引导客户端和无连接协议)
|
||||
* [引导服务端](#引导服务端)
|
||||
|
||||
<!--/ TOC -->
|
||||
|
||||
# Bootstrap引导
|
||||
在了解ChanelPipeline,EventLoop等组件之后,我们需要将这些组件组织起来,使其成为一个可运行的应用程序。
|
||||
这里就需要引导这些组件了。
|
||||
|
||||
|
||||
## Bootstrap类
|
||||
引导类的层次结构包括一个抽象的父类和两个具体的引导子类:
|
||||
|
||||
![Bootstrap类层次结构](../../media/pictures/netty/Bootstrap类层次结构.png)
|
||||
|
||||
ServerBootstrap总是需要一个ServerSocketChannel来处理客户端的连接通信,而
|
||||
Bootstrap则只需要一个普通的Channel用于与服务端的通信。
|
||||
|
||||
下面是AbstractBootstrap的主要方法:
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| group | 设置用语处理所有事件的EventLoopGroup |
|
||||
| channel | 指定服务端或客户端的Channel |
|
||||
| channelFactory | 如果引导没有指定Channel,那么可以指定ChannelFactory来创建Channel |
|
||||
| localAddress | 指定Channel需要绑定的本地地址,如果不指定,则将由系统随机分配一个地址 |
|
||||
| remoteAddress | 设置Channel需要连接的远程地址 |
|
||||
| attr | 指定新创建的Channel的属性值 |
|
||||
| handler | 设置添加到ChannelPipeline中的ChannelHandler |
|
||||
| connect | 连接到远程主机,返回ChannelFuture,用于连接完成的回调 |
|
||||
| bind | 绑定指定地址,返回ChannelFuture,用于绑定完成的回调 |
|
||||
|
||||
|
||||
### 引导客户端和无连接协议
|
||||
Bootstrap负责Netty应用程序的客户端引导,作为客户端,我们需要使用到connect API来连接到远程
|
||||
服务端,其过程如下:
|
||||
|
||||
![Bootstrap客户端引导过程](../../media/pictures/netty/Bootstrap客户端引导过程.png)
|
||||
|
||||
客户端引导的编程模型如下:
|
||||
|
||||
````text
|
||||
|
||||
//创建EventLoopGroup
|
||||
EventLoopGroup group = new NioEventLoopGroup();
|
||||
//创建客户端引导
|
||||
Bootstrap bootstrap = new Bootstrap();
|
||||
//配置各种属性,如Channel,ChannelHandler等
|
||||
bootstrap.group(group)
|
||||
.channel(NioSocketChannel.class)
|
||||
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
|
||||
@Override
|
||||
protected void channeRead0(
|
||||
ChannelHandlerContext channelHandlerContext,
|
||||
ByteBuf byteBuf) throws Exception {
|
||||
System.out.println("Received data");
|
||||
byteBuf.clear();
|
||||
}
|
||||
});
|
||||
//连接到远程主机
|
||||
ChannelFuture future = bootstrap.connect(
|
||||
new InetSocketAddress("www.manning.com", 80));
|
||||
//设置连接成功后的回调
|
||||
future.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture channelFuture)
|
||||
throws Exception {
|
||||
if (channelFuture.isSuccess()) {
|
||||
System.out.println("Connection established");
|
||||
} else {
|
||||
System.err.println("Connection attempt failed");
|
||||
channelFuture.cause().printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
````
|
||||
|
||||
|
||||
### 引导服务端
|
||||
ServerBootstrap负责Netty应用程序的服务端引导,作为服务端,我们需要使用bind API来
|
||||
与本地地址绑定,从而接收客户端连接,其过程如下:
|
||||
|
||||
![ServerBootStrap服务端引导过程](../../media/pictures/netty/ServerBootStrap服务端引导过程.png)
|
||||
|
||||
服务端引导的编程模型如下:
|
||||
|
||||
````text
|
||||
//创建EventLoopGroup
|
||||
NioEventLoopGroup group = new NioEventLoopGroup();
|
||||
//创建服务端引导
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
//配置各种属性,如Channel,ChannelHandler等
|
||||
bootstrap.group(group)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx,
|
||||
ByteBuf byteBuf) throws Exception {
|
||||
System.out.println("Reveived data");
|
||||
byteBuf.clear();
|
||||
}
|
||||
}
|
||||
);
|
||||
//绑定本地地址
|
||||
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
|
||||
//设置绑定成功后的回调
|
||||
future.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture channelFuture)
|
||||
throws Exception {
|
||||
if (channelFuture.isSuccess()) {
|
||||
System.out.println("Server bound");
|
||||
} else {
|
||||
System.err.println("Bound attempt failed");
|
||||
channelFuture.cause().printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
````
|
|
@ -1,242 +0,0 @@
|
|||
<!-- TOC -->
|
||||
|
||||
* [ByteBuf--Netty的数据容器](#bytebuf--netty的数据容器)
|
||||
* [ByteBuf实现](#bytebuf实现)
|
||||
* [ByteBuf使用模式](#bytebuf使用模式)
|
||||
* [字节级别的操作](#字节级别的操作)
|
||||
* [索引访问](#索引访问)
|
||||
* [可丢弃字节](#可丢弃字节)
|
||||
* [可读字节](#可读字节)
|
||||
* [可写字节](#可写字节)
|
||||
* [索引管理](#索引管理)
|
||||
* [查找操作](#查找操作)
|
||||
* [衍生缓冲区](#衍生缓冲区)
|
||||
* [ByteBufHolder](#bytebufholder)
|
||||
* [ByteBuf分配](#bytebuf分配)
|
||||
* [ByteBufAllocator](#bytebufallocator)
|
||||
* [Unpooled](#unpooled)
|
||||
* [ByteBufUtil](#bytebufutil)
|
||||
* [引用计数](#引用计数)
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
# ByteBuf--Netty的数据容器
|
||||
网络传输的基本单位是字节,在Java NIO中,JDK提供了Buffer接口,以及其相关的实现作为NIO操作
|
||||
数据的容器,如ByteBuffer等等。 而Netty为了解决Buffer原生接口的复杂操作提供了ByteBuf,
|
||||
ByteBuf是一个很好的经过优化过的数据容器,我们可以将字节数据添加到ByteBuf中或从ByteBuf中获取数据,
|
||||
相比于原生Buffer,ByteBuf更加灵活和易用。
|
||||
|
||||
Netty的数据处理主要通过两个API提供:
|
||||
|
||||
1. abstract class ByteBuf
|
||||
2. interface ByteBufHolder
|
||||
|
||||
使用ByteBuf API能够给我们带来良好的编码体验,如
|
||||
|
||||
- 正常情况下,ByteBuf比ByteBuffer的性能更好;
|
||||
|
||||
- 实现了ReferenceCounted引用计数接口,优化了内存的使用;
|
||||
|
||||
- 容量可以动态增长,如StringBuilder之于String;
|
||||
|
||||
- 在读和写这两种模式切换时,无需像ByteBuffer一样调用flip方法,更易于操作;
|
||||
|
||||
...
|
||||
|
||||
ByteBuf还有很多好处,上面列举的只是一部分,其他优点就需要各位同学慢慢了解了。
|
||||
|
||||
|
||||
### ByteBuf实现
|
||||
ByteBuf维护了两个不同的索引:一个是用于读取的readerIndex , 一个是用于写入的writerIndex。
|
||||
当我们写入字节到ByteBuf后,writerIndex增加,开始读取字节后,readerIndex开始增加。读取字节直到
|
||||
readerIndex和writerIndex到达同一位置(已经读取到末尾了),ByteBuf就变为不可读。
|
||||
这就好比当我们访问数组时,超出了它的范围时,程序会抛出IndexOutOfBoundException。
|
||||
|
||||
当我们调用ByteBuf以read或write开头的方法时,将会增加这个ByteBuf的读索引或写索引,而诸如set或get
|
||||
的方法则不会改变索引。
|
||||
|
||||
我们可以指定ByteBuf的最大容量,如果对ByteBuf的写入操作导致writerIndex超出了最大人容量,那么程序将会
|
||||
抛出一个异常,ByteBuf的最大人容量是Integer.MAX_VALUE。
|
||||
|
||||
ByteBuf大致的结构和状态:
|
||||
|
||||
![ByteBuf结构](../../media/pictures/netty/ByteBuf结构.png)
|
||||
|
||||
|
||||
### ByteBuf使用模式
|
||||
ByteBuf有多种使用模式,我们可以根据需求构建不同使用模式的ByteBuf。
|
||||
|
||||
- 堆缓冲区(HeapByteBuf): 最常用的ByteBuf模式是将数据存储在JVM的堆空间中,实际上是通过数组存储数据,
|
||||
所以这种模式被称为支撑数组(Backing Array )。所以这种模式被称为支撑数组可以在没有使用池化的情况下快速分配和释放,
|
||||
适合用于有遗留数据需要处理的情况。
|
||||
|
||||
![堆缓冲区模式](../../media/pictures/netty/堆缓冲区模式.png)
|
||||
|
||||
- 直接缓冲区(DirectByteBuf): 在Java中,我们创建的对象大部分都是存储在堆区之中的,但这不是绝对的,在NIO的API中,
|
||||
允许Buffer分配直接内存,即操作系统的内存,这样做的好处非常明显: 前面在传输章节介绍过的零拷贝技术的
|
||||
特点之一就是规避了多次IO拷贝,而现在数据直接就在直接内存中,而不是在JVM应用进程中,这不仅减少了拷贝次数,
|
||||
是否还意味着减少了用户态与内核态的上下文切换呢?
|
||||
直接缓冲区的缺点也比较明显: 直接内存的分配和释放都较为昂贵,而且因为直接
|
||||
缓冲区的数据不是在堆区的,所以我们在某些时候可能需要将直接缓冲区的数据先拷贝一个副本到堆区,
|
||||
再对这个副本进行操作。 与支撑数组相比,直接缓冲区的工作可能更多,所以如果事先知道数据会作为
|
||||
一个数组来被访问,那么我们应该使用堆内存。
|
||||
|
||||
![直接缓冲区模式](../../media/pictures/netty/直接缓冲区模式.png)
|
||||
|
||||
- 复合缓冲区(CompositeByteBuf): CompositeByteBuf为多个ByteBuf提供了一个聚合视图,
|
||||
我们可以根据需要,向CompositeByteBuf中添加或删除ByteBuf实例,所以CompositeByteBuf中可能
|
||||
同时包含直接缓冲区模式和堆缓冲区模式的ByteBuf。对于CompositeByteBuf的hasArray方法,
|
||||
**如果CompositeByteBuf中只有一个ByteBuf实例,那么CompositeByteBuf的hasArray方法
|
||||
将直接返回这唯一一个ByteBuf的hasArray方法的结果,否则返回false。**
|
||||
除此之外,CompositeByteBuf还提供了许多附加的功能,可以查看Netty的文档学习。
|
||||
|
||||
![复合缓冲区模式](../../media/pictures/netty/复合缓冲区模式.png)
|
||||
|
||||
|
||||
### 字节级别的操作
|
||||
除了普通的读写操作,ByteBuf还提供了修改数据的方法。
|
||||
|
||||
|
||||
#### 索引访问
|
||||
如数组的索引访问一样,ByteBuf的索引访问也是从零开始的,第一个字节的索引是0,最后一个字节的索引总是
|
||||
capacity - 1:
|
||||
|
||||
![随机访问索引](../../media/pictures/netty/随机访问索引.png)
|
||||
|
||||
注意:使用getByte方式访问,既不会改变readerIndex,也不会改变writerIndex。
|
||||
|
||||
JDK的ByteBuffer只有一个索引position,所以当ByteBuffer在读和写模式之间切换时,需要使用flip方法。
|
||||
而ByteBuf同时具有读索引和写索引,则无需切换模式,在ByteBuf内部,其索引满足:
|
||||
|
||||
````text
|
||||
0 <= readerIndex <= writerIndex <= capacity
|
||||
````
|
||||
|
||||
这样的规律,当使用readerIndex读取字节,或使用writerIndex写入字节时,ByteBuf内部的分段大致如下:
|
||||
|
||||
![ByteBuf内部分段](../../media/pictures/netty/ByteBuf内部分段.png)
|
||||
|
||||
上图介绍了在ByteBuf内部大致有3个分段,接下来我们就详细的介绍下这三个分段。
|
||||
|
||||
|
||||
#### 可丢弃字节
|
||||
上图中,当readerIndex读取一部分字节后,之前读过的字节就属于已读字节,可以被丢弃了,通过调用
|
||||
ByteBuf的discardReadBytes方法我们可以丢弃这个分段,丢弃这个分段实际上是删除这个分段的已读字节,
|
||||
然后回收这部分空间:
|
||||
|
||||
![DiscardReadBytes回收后ByteBuf内部分段](../../media/pictures/netty/DiscardReadBytes回收后ByteBuf内部分段.png)
|
||||
|
||||
|
||||
#### 可读字节
|
||||
ByteBuf的可读字节分段存储了尚未读取的字节,我们可以使用readBytes等方法来读取这部分数据,如果我们读取
|
||||
的范围超过了可读字节分段,那么ByteBuf会抛出IndexOutOfBoundsException异常,所以在读取数据之前,我们
|
||||
需要使用isReadable方法判断是否仍然有可读字节分段。
|
||||
|
||||
|
||||
#### 可写字节
|
||||
可写字节分段即没有被写入数据的区域,我们可以使用writeBytes等方法向可写字节分段写入数据,如果我们写入
|
||||
的字节超过了ByteBuf的容量,那么ByteBuf也会抛出IndexOutOfBoundsException异常。
|
||||
|
||||
|
||||
#### 索引管理
|
||||
我们可以通过markReaderIndex,markWriterIndex方法来标记当前readerIndex和writerIndex的位置,
|
||||
然后使用resetReaderIndex,resetWriterIndex方法来将readerIndex和writerIndex重置为之前标记过的
|
||||
位置。
|
||||
|
||||
我们还可以使用clear方法来将readerIndex和writerIndex重置为0,但是clear方法并不会清空ByteBuf的
|
||||
内容,下面clear方法的实现:
|
||||
|
||||
![ByteBuf的clear方法](../../media/pictures/netty/ByteBuf的clear方法.png)
|
||||
|
||||
其过程是这样的:
|
||||
|
||||
![ByteBuf的clear方法调用前](../../media/pictures/netty/ByteBuf的clear方法调用前.png)
|
||||
|
||||
![ByteBuf的clear方法调用后](../../media/pictures/netty/ByteBuf的clear方法调用后.png)
|
||||
|
||||
由于调用clear后,数据并没有被清空,但整个ByteBuf仍然是可写的,这比discardReadBytes轻量的多,
|
||||
DiscardReadBytes还要回收已读字节空间。
|
||||
|
||||
|
||||
#### 查找操作
|
||||
在ByteBuf中,有多种可以确定值的索引的方法,最简单的方法是使用ByteBuf的indexOf方法。
|
||||
较为复杂的查找可以通过ByteBuf的forEachByte方法,forEachByte方法所需的参数是ByteProcessor,
|
||||
但我们无需去实现ByteProcessor,因为ByteProcessor已经为我们定义好了两个易用的实现。
|
||||
|
||||
|
||||
#### 衍生缓冲区
|
||||
衍生缓冲区是专门展示ByteBuf内部数据的视图,这种视图常通过以下方法创建:
|
||||
|
||||
- duplicate
|
||||
|
||||
- slice
|
||||
|
||||
- order
|
||||
|
||||
- readSlice
|
||||
|
||||
这些方法都将以源ByteBuf创建一个新的ByteBuf视图,所以源ByteBuf内部的索引和数据都与视图一样,
|
||||
但这也意味着修改了视图的内容,也会修改源ByteBuf的内容。如果我们需要一个真实的ByteBuf的副本,
|
||||
我们应该使用copy方法来创建,copy方法创建的副本拥有独立的内存,不会影响到源ByteBuf。
|
||||
|
||||
|
||||
### ByteBufHolder
|
||||
从表面理解起来,ByteBufHolder是ByteBuf的持有者,的确没有错。 ByteBuf几乎唯一的作用就是存储
|
||||
数据,但在实际的数据传输中,除了数据,我们可能还需要存储各种属性值,Http便是一个很好的例子。
|
||||
除了Http Content,还包括状态码,cookie等等属性,总不能把这些属性与Content存储在一个ByteBuf中吧,
|
||||
所以Netty提供了ByteBufHolder。ByteBufHolder为Netty提供了高级特性的支持,如缓冲区持化,使得可以
|
||||
从池中借用ByteBuf,并且在需要的时候自动释放。
|
||||
|
||||
以下是ByteBufHolder常见的方法:
|
||||
|
||||
- content: 返回这个ByteBufHolder所持有的ByteBuf。
|
||||
|
||||
- copy: 返回ByteBufHolder的深拷贝,连它持有的ByteBuf也拷贝。
|
||||
|
||||
|
||||
### ByteBuf分配
|
||||
前面介绍了ByteBuf的一些基本操作和原理,但却并未说明如何分配一个ByteBuf,这里将讲解ByteBuf的分配方式。
|
||||
|
||||
|
||||
#### ByteBufAllocator
|
||||
为了减少分配和释放内存的开销,Netty通过 ByteBufAllocator 实现了ByteBuf的池化。以下是ByteBufAllocator
|
||||
的常见方法。
|
||||
|
||||
- buffer: 返回一个基于堆或直接内存的ByteBuf,具体取决于实现。
|
||||
|
||||
- heapBuffer: 返回一个基于堆内存的ByteBuf。
|
||||
|
||||
- directBuffer: 返回一个基于直接内存的ByteBuf。
|
||||
|
||||
- compositeBuffer: 返回一个组合ByteBuf。
|
||||
|
||||
- ioBuffer: 返回一个用于套接字的ByteBuf。
|
||||
|
||||
我们可以通过Channel或这ChannelHandlerContext的alloc方法获取到一个ByteBufAllocator
|
||||
|
||||
![获取ByteBufAllocator](../../media/pictures/netty/获取ByteBufAllocator.png)
|
||||
|
||||
Netty提供了两种ByteBufAllocator的实现: PooledByteBufAllocator和UnpooledByteBufAllocator。
|
||||
PooledByteBufAllocator池化了ByteBuf的实例以提高性能并最大限度的减少内存碎片,此实现的分配内存的方法
|
||||
是使用[jemalloc](https://people.freebsd.org/~jasone/jemalloc/bsdcan2006/jemalloc.pdf),此种
|
||||
方法分配内存的效率非常高,已被大量现代操作系统采用。 UnpooledByteBufAllocator则不会池化ByteBuf,
|
||||
Netty默认使用的是PooledByteBufALlocator。
|
||||
|
||||
|
||||
#### Unpooled
|
||||
当Channel或ChannelHandlerContext未引用ByteBufAllocator时,就无法使用ByteBufAllocator来分配
|
||||
ByteBUf,对于这种情况,Netty提供了Unpooled工具类,它提供了一系列的静态方法来分配未池化的ByteBuf。
|
||||
|
||||
|
||||
#### ByteBufUtil
|
||||
ByteBufUtil是ByteBuf的一个工具类,它提供大量操作ByteBuf的方法,,其中非常重要的一个方法就是
|
||||
hexDump,这个方法会以16进制的形式来表示ByteBuf的内容。另一个很重要的方法是equals,它被用于判断
|
||||
ByteBuf之间的相等性。
|
||||
|
||||
|
||||
### 引用计数
|
||||
学习过JVM的小伙伴应该知道垃圾回收有引用计数法和可达性分析这两种算法判断对象是否存活,Netty就使用了
|
||||
引用计数法来优化内存的使用。引用计数确保了当对象的引用计数大于1时,对象就不会被释放,当计数减少至0时,
|
||||
对象就会被释放,如果程序访问一个已被释放的引用计数对象,那么将会导致一个
|
||||
IllegalReferenceCountException异常。
|
||||
在Netty中,ByteBuf和ByteBufHolder都实现了ReferenceCounted接口。
|
|
@ -1,268 +0,0 @@
|
|||
<!--TOC-->
|
||||
|
||||
* [ChannelHandler和ChannelPipeline](#channelhandler和channelpipeline)
|
||||
* [ChannelHandler家族](#channelhandler家族)
|
||||
* [Channel的生命周期](#channel的生命周期)
|
||||
* [ChannelHandler生命周期](#channelhandler生命周期)
|
||||
* [ChannelInboundHandler接口](#channelinboundhandler接口)
|
||||
* [ChannelOutboundHandler接口](#channeloutboundhandler接口)
|
||||
* [资源管理](#资源管理)
|
||||
* [ChannelPipeline](#channelpipeline)
|
||||
* [ChannelPipeline相对论](#channelpipeline相对论)
|
||||
* [修改ChannelPipeline](#修改channelpipeline)
|
||||
* [ChannelHandler的执行和阻塞](#channelhandler的执行和阻塞)
|
||||
* [触发事件](#触发事件)
|
||||
* [ChannelHandlerContext](#channelhandlercontext)
|
||||
* [ChannelHandlerContext的高级用法](#channelhandlercontext的高级用法)
|
||||
|
||||
<!-- /TOC-->
|
||||
|
||||
|
||||
# ChannelHandler和ChannelPipeline
|
||||
在Netty组件中我们已经介绍了ChannelHandler和ChannelPipeline的关系,这里我们将继续深入了解这两个核心
|
||||
组件的细节。在学习本章内容之前,请各位同学温习一遍Netty组件部分的内容。
|
||||
|
||||
### ChannelHandler家族
|
||||
|
||||
#### Channel的生命周期
|
||||
在Channel的生命周期中,它的状态与ChannelHandler是密切相关的,下列是Channel组件的四个状态:
|
||||
|
||||
| 状态 | 描述 |
|
||||
| :--- | :--- |
|
||||
| ChannelUnregistered | Channel没有注册到EventLoop |
|
||||
| ChannelRegistered | Channel被注册到了EventLoop |
|
||||
| ChannelActive | Channel已经连接到它的远程节点,处于活动状态,可以收发数据 |
|
||||
| ChannelInactive | Channel与远程节点断开不再处于活动状态 |
|
||||
|
||||
Channel的生命周期如下图所示,当这些状态发生改变时,将会生成对应的事件,ChannelPipeline中的ChannelHandler
|
||||
就可以及时做出处理。
|
||||
|
||||
|
||||
#### ChannelHandler生命周期
|
||||
ChannelHandler接口定义了其生命周期中的操作,当ChanelHandler被添加到ChannelPipeline
|
||||
或从ChannelPipeline中移除时,会调用这些操作,ChannelHandler的生命周期如下:
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| handlerAdded | 当把ChannelHandler添加到ChannelPipeline中时调用此方法 |
|
||||
| handlerRemoved | 当把ChannelHandler从ChannelPipeline中移除的时候会调用此方法 |
|
||||
| exceptionCaught | 当ChannelHandler在处理数据的过程中发生异常时会调用此方法 |
|
||||
|
||||
|
||||
#### ChannelInboundHandler接口
|
||||
ChannelInboundHandler会在接受数据或者其对应的Channel状态发生改变时调用其生命周期的方法,
|
||||
ChannelInboundHandler的生命周期和Channel的生命周期其实是密切相关的。
|
||||
以下是ChannelInboundHandler的生命周期方法:
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| ChannelRegistered | 当Channel被注册到EventLoop且能够处理IO事件时会调用此方法 |
|
||||
| ChannelUnregistered | 当Channel从EventLoop注销且无法处理任何IO事件时会调用此方法 |
|
||||
| ChannelActive | 当Channel已经连接到远程节点(或者已绑定本地address)且处于活动状态时会调用此方法 |
|
||||
| ChannelInactive | 当Channel与远程节点断开,不再处于活动状态时调用此方法 |
|
||||
| ChannelReadComplete | 当Channel的某一个读操作完成时调用此方法 |
|
||||
| ChannelRead | 当Channel有数据可读时调用此方法 |
|
||||
| ChannelWritabilityChanged | 当Channel的可写状态发生改变时调用此方法,可以调用Channel的isWritable方法检测Channel的可写性,还可以通过ChannelConfig来配置write操作相关的属性 |
|
||||
| userEventTriggered | 当ChannelInboundHandler的fireUserEventTriggered方法被调用时才调用此方法。 |
|
||||
|
||||
**这里有一个细节一定需要注意:当我们实现ChannelInboundHandler的channelRead方法时,请一定要记住
|
||||
使用ReferenceCountUtil的release方法释放ByteBuf,这样可以减少内存的消耗,所以我们可以实现一个
|
||||
ChannelHandler来完成对ByteBuf的释放,就像下面这样:**
|
||||
|
||||
![ChannelInboundHandler释放ByteBuf](../../media/pictures/netty/ChannelInboundHandler释放ByteBuf.png)
|
||||
|
||||
|
||||
**一个更好的办法是继承SimpleChannelInboundHandler,因为SimpleChannelInboundHandler已经帮我们
|
||||
把与业务无关的逻辑在ChannelRead方法实现了,我们只需要实现它的channelRead0方法来完成我们的逻辑就够了:**
|
||||
|
||||
![SimpleChannelInboundHandler的ChannelRead方法](../../media/pictures/netty/SimpleChannelInboundHandler的ChannelRead方法.png)
|
||||
|
||||
**可以看到SimpleChannelInboundHandler已经将释放资源的逻辑实现了,而且会自动调用ChannelRead0方法
|
||||
来完成我们业务逻辑。**
|
||||
|
||||
|
||||
#### ChannelOutboundHandler接口
|
||||
出站数据将由ChannelOutboundHandler处理,它的方法将被Channel,ChannelPipeline以及ChannelHandlerContext调用
|
||||
(Channel,ChannelPipeline,ChannelHandlerContext都拥有write操作),以下是ChannelOutboundHandler的主要方法:
|
||||
|
||||
| 状态 | 描述 |
|
||||
| :--- | :--- |
|
||||
| bind | 当Channel绑定到本地address时会调用此方法 |
|
||||
| connect | 当Channel连接到远程节点时会调用此方法 |
|
||||
| disconnect | 当Channel和远程节点断开时会调用此方法 |
|
||||
| close | 当关闭Channel时会调用此方法 |
|
||||
| deregister | 当Channel从它的EventLoop注销时会调用此方法 |
|
||||
| read | 当从Channel读取数据时会调用此方法 |
|
||||
| flush | 当Channel将数据冲刷到远程节点时调用此方法 |
|
||||
| write | 当通过Channel将数据写入到远程节点时调用此方法 |
|
||||
|
||||
**ChannelOutboundHandler的大部分方法都需要一个ChannelPromise类型的参数,ChannelPromise是
|
||||
ChannelFuture的一个子接口,这样你就可以明白ChannelPromise实际的作用和ChannelFuture是一样的,
|
||||
没错,ChannelPromise正是用于在ChannelOutboundHandler的操作完成后执行的回调。**
|
||||
|
||||
|
||||
#### 资源管理
|
||||
当我们使用ChannelInboundHandler的read或ChannelOutboundHandler的write操作时,我们都需要保证
|
||||
没有任何资源泄露并尽可能的减少资源耗费。之前已经介绍过了ReferenceCount引用计数用于处理池化的
|
||||
ByteBuf资源。 为了帮助我们诊断潜在的的资源泄露问题,Netty提供了ResourceLeakDetector,它将
|
||||
对我们的Netty程序的已分配的缓冲区做大约1%的采样用以检测内存泄露,Netty目前定义了4种泄露检测级别,如下:
|
||||
|
||||
| 级别 | 描述 |
|
||||
| :--- | :--- |
|
||||
| Disabled | 禁用泄露检测。我们应当在详细测试之后才应该使用此级别。 |
|
||||
| SIMPLE | 使用1%的默认采样率检测并报告任何发现的泄露,这是默认的检测级别。 |
|
||||
| ADVANCED | 使用默认的采样率,报告任何发现的泄露以及对应的消息的位置。 |
|
||||
| PARANOID | 类似于ADVANCED,但是每次都会对消息的访问进行采样,此级别可能会对程序的性能造成影响,应该用于调试阶段。 |
|
||||
|
||||
我们可以通过JVM启动参数来设置leakDetector的级别:
|
||||
|
||||
````text
|
||||
java -Dio.netty.leakDetectionLevel=ADVANCED
|
||||
````
|
||||
|
||||
|
||||
### ChannelPipeline
|
||||
在Netty组件中也介绍过了,ChannelPipeline是一系列ChannelHandler组成的拦截链,每一个新创建的Channel
|
||||
都会被分配一个新的ChannelPipeline,Channel和ChannelPipeline之间的关联是持久的,无需我们干涉它们
|
||||
之间的关系。
|
||||
|
||||
|
||||
#### ChannelPipeline相对论
|
||||
Netty总是将ChannelPipeline的入站口作为头部,出站口作为尾部,当我们通过ChannelPipeline的add方法
|
||||
将入站处理器和出站处理器混合添加到ChannelPipeline后,ChannelHandler的顺序如下:
|
||||
|
||||
![ChannelPipeline的ChannelHandler顺序](../../media/pictures/netty/ChannelPipeline的ChannelHandler顺序.png)
|
||||
|
||||
一个入站事件将从ChannelPipeline的头部(左侧)向尾部(右侧)开始传播,出站事件的传播则是与入站的传播方向
|
||||
相反。当ChannelPipeline在ChannelHandler之间传播事件的时候,它会判断下一个ChannelHandler的类型
|
||||
是否与当前ChannelHandler的类型相同,如果相同则说明它们是一个方向的事件,
|
||||
如果不同则跳过该ChannelHandler并前进到下一个ChannelHandler,直到它找到相同类型的ChannelHandler。
|
||||
|
||||
|
||||
#### 修改ChannelPipeline
|
||||
ChannelPipeline可以通过添加,删除和修改ChannelHandler来修改它自身的布局,这是它最基本的能力,
|
||||
一下列举了ChannelPipeline的一些修改方法:
|
||||
|
||||
| 方法 | 描述 |
|
||||
| addXX | 将指定的ChannelHandler添加到ChannelPipeline中 |
|
||||
| remove | 将指定的ChannelHandler从ChannelPipeline中移除 |
|
||||
| replace | 将ChannelPipeline中指定的ChannelHandler替换成另一个ChannelHandler |
|
||||
|
||||
|
||||
#### ChannelHandler的执行和阻塞
|
||||
通常ChannelPipeline中的每个ChannelHandler都是通过它(ChannelPipeline)的EventLoop线程来处理
|
||||
传递给他的数据的,所以我们不能去阻塞这个线程,否则会对整体的IO操作产生负面影响。 但有时候不得已
|
||||
需要使用阻塞的API来完成逻辑处理,对于这种情况,ChannelPipeline的某些方法支持接受一个EventLoopGroup
|
||||
类型的参数,我们可以通过自定义EventLoopGroup的方式,使ChannelHandler在我们的EventLoopGroup内处理数据。
|
||||
这样一来,就可以避免阻塞线程的影响了。
|
||||
|
||||
|
||||
#### 触发事件
|
||||
ChannelPipeline的API不仅有对ChannelHandler的增删改操作,还有对入站和出站操作的附加方法,如下:
|
||||
|
||||
ChannelPipeline的入站方法:
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| fireChannelRegistered | 调用ChannelPipeline中下一个ChannelInboundHandler的channelRegistered方法 |
|
||||
| fireChannelUnregistered | 调用ChannelPipeline中下一个ChannelInboundHandler的channelUnregistered方法 |
|
||||
| fireChannelActive | 调用ChannelPipeline中下一个ChannelInboundHandler的channelActive方法 |
|
||||
| fireChannelInactive | 调用ChannelPipeline中下一个ChannelInboundHandler的channelInactive方法 |
|
||||
| fireExceptionCaught | 调用ChannelPipeline中下一个ChannelInboundHandler的exceptionCaught方法 |
|
||||
| fireUserEventTriggered | 调用ChannelPipeline中下一个ChannelInboundHandler的userEventTriggered方法 |
|
||||
| fireChannelRead | 调用ChannelPipeline中下一个ChannelInboundHandler的channelRead方法 |
|
||||
| fireChannelReadComplete | 调用ChannelPipeline中下一个ChannelInboundHandler的channelReadComplete方法 |
|
||||
| fireChannelWritabilityChanged | 调用ChannelPipeline中下一个ChannelInboundHandler的channelWritabilityChanged方法 |
|
||||
|
||||
|
||||
ChannelPipeline的出站方法:
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| bind | 调用ChannelPipeline中下一个ChannelOutboundHandler的bind方法,将Channel与本地地址绑定 |
|
||||
| connect | 调用ChannelPipeline中下一个ChannelOutboundHandler的connect方法,将Channel连接到远程节点 |
|
||||
| disconnect | 调用ChannelPipeline中下一个ChannelOutboundHandler的disconnect方法,将Channel与远程连接断开 |
|
||||
| close | 调用ChannelPipeline中下一个ChannelOutboundHandler的close方法,将Channel关闭 |
|
||||
| deregister | 调用ChannelPipeline中下一个ChannelOutboundHandler的deregister方法,将Channel从其对应的EventLoop注销 |
|
||||
| flush | 调用ChannelPipeline中下一个ChannelOutboundHandler的flush方法,将Channel的数据冲刷到远程节点 |
|
||||
| write | 调用ChannelPipeline中下一个ChannelOutboundHandler的write方法,将数据写入Channel |
|
||||
| writeAndFlush | 先调用write方法,然后调用flush方法,将数据写入并刷回远程节点 |
|
||||
| read | 调用ChannelPipeline中下一个ChannelOutboundHandler的read方法,从Channel中读取数据 |
|
||||
|
||||
|
||||
### ChannelHandlerContext
|
||||
ChannelHandlerContext代表的是ChannelHandler和ChannelPipeline之间的关联,每当有ChannelHandler
|
||||
添加到ChannelPipeline中时,都会创建ChannelHandlerContext。ChannelHandlerContext的主要功能是
|
||||
管理它所关联的ChannelHandler与同一个ChannelPipeline中的其他ChannelHandler之间的交互:
|
||||
|
||||
![ChannelHandlerContext和ChannelHandler之间的关系](../../media/pictures/netty/ChannelHandlerContext和ChannelHandler之间的关系.png)
|
||||
|
||||
ChannelHandlerContext的大部分方法和Channel和ChannelPipeline相似,但有一个重要的区别是:
|
||||
调用Channel或ChannelPipeline的方法,如:
|
||||
|
||||
````text
|
||||
//使用Chanel write
|
||||
Channel channel = ctx.channel();
|
||||
ctx.write(xxx);
|
||||
|
||||
//使用Pipeline write
|
||||
ChannelPipeline pipeline = ctx.pipeline();
|
||||
pipeline.write(xxx);
|
||||
````
|
||||
|
||||
,其影响是会沿着整个ChannelPipeline进行传播:
|
||||
|
||||
![通过Channel或ChannelPipeline进行的事件传播](../../media/pictures/netty/通过Channel或ChannelPipeline进行的事件传播.png)
|
||||
|
||||
|
||||
而调用ChannelHandlerContext的方法,如:
|
||||
|
||||
````text
|
||||
//使用ChannelContext write
|
||||
ctx.write(xxx);
|
||||
````
|
||||
则是从其关联的ChannelHandler开始,并且只会传播给位于该ChannelPipeline中的下一个能够处理该事件的
|
||||
ChannelHandler:
|
||||
|
||||
![通过ChannelHandlerContext进行的事件传播](../../media/pictures/netty/通过ChannelHandlerContext进行的事件传播.png)
|
||||
|
||||
|
||||
|
||||
下面是一些比较重要的方法,有些和ChannelPipeline功能相似的方法就不再罗列了,各位同学可以直接查看原API。
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| alloc | 获取与当前ChannelHandlerContext所关联的Channel的ByteBufAllocator |
|
||||
| handler | 返回与当前ChannelHandlerContext绑定的ChannelHandler |
|
||||
| pipeline | 返回与当前ChannelHandlerContext关联的ChannelPipeline |
|
||||
| ... | ... |
|
||||
|
||||
|
||||
#### ChannelHandlerContext的高级用法
|
||||
有时候我们需要在多个ChannelPipeline之间共享一个ChannelHandler,以此实现跨管道处理(获取)数据
|
||||
的功能,此时的ChannelHandler属于多个ChannelPipeline,且会绑定到不同的ChannelHandlerContext上。
|
||||
在多个ChannelPipeline之间共享ChannelHandler我们需要使用 **@Sharable注解**,这代表着它是一个共享的
|
||||
ChannelHandler,如果一个ChannelHandler没有使用@Sharable注解却被用于多个ChannelPipeline,那么
|
||||
将会触发异常。 还有非常重要的一点:**一个ChannelHandler被用于多个ChannelPipeline肯定涉及到多线程
|
||||
数据共享的问题,因此我们需要保证ChannelHandler的方法同步。** 下面是一个很好的例子:
|
||||
|
||||
`````java
|
||||
@Sharable
|
||||
public class UnsafeSharableChannelHandler extends ChannelInboundHandlerAdapter
|
||||
{
|
||||
private int count;
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx,Object msg)
|
||||
{
|
||||
count++;
|
||||
System.out.println("count : " + count);
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
`````
|
||||
上面这个ChannelHandler标识了@Sharable注解,这代表它需要被用于多个ChannelPipeline之间,
|
||||
但是这个ChannelHandler之中有一个不易察觉的问题: 它声明了一个实例变量count,且ChannelRead方法
|
||||
不是线程安全的。 那么这个问题的后果我相信学习了多线程的同学应该都明白,一个最简单的方法
|
||||
就是给修改了count的变量的方法加synchronized关键字,确保即使在多个ChannelPipeline之间共享,
|
||||
ChannelHandler也能保证数据一致。
|
|
@ -1,156 +0,0 @@
|
|||
<!-- TOC -->
|
||||
|
||||
* [什么是编码器和解码器?](#什么是编码器和解码器)
|
||||
* [解码器](#解码器)
|
||||
* [ByteToMessageDecoder](#bytetomessagedecoder)
|
||||
* [ReplayingDecoder](#replayingdecoder)
|
||||
* [MessageToMessageDecoder](#messagetomessagedecoder)
|
||||
* [编码器](#编码器)
|
||||
* [MessageToByteEncoder](#messagetobyteencoder)
|
||||
* [MessageToMessageEncoder](#messagetomessageencoder)
|
||||
* [编解码器](#编解码器)
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
# 什么是编码器和解码器?
|
||||
从网络传输的角度来讲,数组总是以字节的格式在网络之中进行传输的。
|
||||
每当源主机发送数据到目标主机时,数据会从本地格式被转换成字节进行传输,这种转换被称为编码,编码的逻辑由
|
||||
编码器处理。
|
||||
每当目标主机接受来自源主机的数据时,数据会从字节转换为我们需要的格式,这种转换被称为解码,解码的逻辑由
|
||||
解码器处理。
|
||||
|
||||
在Netty中,编码解码器实际上是ChannelOutboundHandler和ChannelInboundHandler的实现,
|
||||
因为编码和解码都属于对数据的处理,由此看来,编码解码器被设计为ChannelHandler也就无可厚非。
|
||||
|
||||
|
||||
## 解码器
|
||||
在Netty中,解码器是ChannelInboundHandler的实现,即处理入站数据。
|
||||
解码器主要分为两种:
|
||||
|
||||
- 将字节解码为Message消息: ByteToMessageDecoder和ReplayingDecoder。
|
||||
- 将一种消息解码为另一种消息: MessageToMessageDecoder。
|
||||
|
||||
|
||||
### ByteToMessageDecoder
|
||||
ByteToMessageDecoder用于将字节解码为消息,如果我们想自定义解码器,就需要继承这个类并实现decode方法。
|
||||
decode方法是自定解码器必须实现的方法,它被调用时会传入一个包含了数据的ByteBuf和一个用来添加解码消息的List。
|
||||
对decode方法的调用会重复进行,直至确认没有新元素被添加到该List或ByteBuf没有可读字节为止。最后,如果List不为空,
|
||||
那么它的内容会被传递给ChannelPipeline中的下一个ChannelInboundHandler。
|
||||
|
||||
下面是ByteToMessageDecoder的编程模型:
|
||||
|
||||
````java
|
||||
public class ToIntegerDecoder extends ByteToMessageDecoder //扩展ByteToMessageDecoder
|
||||
{
|
||||
|
||||
@Override
|
||||
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
|
||||
throws Exception
|
||||
{
|
||||
//检查ByteBuf是否仍有4个字节可读
|
||||
if (in.readableBytes() >= 4)
|
||||
{
|
||||
out.add(in.readInt()); //从ByteBuf读取消息到List中
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
上面这种编程模式很简单,但是在读取ByteBuf之前验证其是否可读的步骤显得有些多余,所以可以使用ReplayingDecoder
|
||||
来解决这个问题。
|
||||
|
||||
|
||||
### ReplayingDecoder
|
||||
ReplayingDecoder扩展了ByteToMessageDecoder,这使得我们不再需要检查ByteBuf,因为ReplayingDecoder
|
||||
自定义了ByteBuf的实现:ReplayingDecoderByteBuf,这个包装后的ByteBuf在内部会自动检查是否可读。以下是
|
||||
ReplayingDecoderByteBuf的内部实现:
|
||||
|
||||
![ReplayingDecoderByteBuf内部实现](../../media/pictures/netty/ReplayingDecoderByteBuf内部实现.png)
|
||||
|
||||
虽然ReplayingDecoderByteBuf可以自动检查可读性,但是对于某些操作并不支持,会抛出
|
||||
UnsupportedOperationException异常。其编程模型如下:
|
||||
|
||||
`````java
|
||||
public class ToIntegerDecoder2 extends ReplayingDecoder<Void> //扩展ReplayingDecoder
|
||||
{
|
||||
@Override
|
||||
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
|
||||
throws Exception
|
||||
{
|
||||
out.add(in.readInt());//从ByteBuf读取消息到List中
|
||||
}
|
||||
}
|
||||
`````
|
||||
|
||||
### MessageToMessageDecoder
|
||||
MessageToMessageDecoder用于将一种类型的消息解码另一种类型的消息,如从DTO转为POJO。
|
||||
这是MessageToMessageDecoder的原型:
|
||||
|
||||
````text
|
||||
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter
|
||||
````
|
||||
MessageToMessageDecoder的泛型I定义了我们转换何种类型的参数。
|
||||
和ByteToMessageDecoder一样,自定义MessageToMessageDecoder的解码器也需要实现其decode方法。
|
||||
|
||||
以下是它的编程模型:
|
||||
|
||||
````java
|
||||
|
||||
public class IntegerToStringDecoder extends
|
||||
MessageToMessageDecoder<Integer>
|
||||
{
|
||||
|
||||
@Override
|
||||
public void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out)
|
||||
throws Exception
|
||||
{
|
||||
out.add(String.valueOf(msg));
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
## 编码器
|
||||
在Netty中,编码器是ChannelOutboundHandler的实现,即处理出站数据。
|
||||
编码器同样分为两种:
|
||||
|
||||
- 将消息编码为字节: MessageToByteEncoder。
|
||||
- 将消息编码为消息: MessageToMessageEncoder。
|
||||
|
||||
|
||||
### MessageToByteEncoder
|
||||
MessageToByteEncoder用于将消息编码为字节,如果我们需要自定编码器,就需要继承它并实现它的encode方法。
|
||||
encode方法是自定义编码器必须实现的方法,它被调用时会传入相应的数据和一个存储数据的ByteBuf。
|
||||
在encode被调用之后,该ByteBuf会被传递给ChannelPipeline中下一个ChannelOutboundHandler。
|
||||
|
||||
以下是MessageToByteEncoder的编程模型:
|
||||
|
||||
````java
|
||||
public class ShortToByteEncoder extends MessageToByteEncoder<Short> //扩展MessageToByteEncoder
|
||||
{
|
||||
|
||||
@Override
|
||||
public void encode(ChannelHandlerContext ctx , Short data, ByteBuf out)
|
||||
throws Exception
|
||||
{
|
||||
out.writeShort(data);//将data写入ByteBuf
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
### MessageToMessageEncoder
|
||||
MessageToMessageEncoder用于将一种类型的消息编码另一种类型的消息,其原型和
|
||||
MessageToMessageDecoder相似,所以这里也不再细说。
|
||||
|
||||
|
||||
## 编解码器
|
||||
上面的内容讲的是单独的编码器和解码器,编码器处理出站数据,是ChannelOutboundHandler的实现,
|
||||
解码器负责处理入站数据,是ChannelInboundHandler的实现。除了编码器和解码器,Netty还提供了集编码与解码
|
||||
于一身的编解码器ByteToMessageCodec和MessageToMessageCodec,它们同时实现了ChannelInboundHandler和ChannelOutboundHandler,其结构如下:
|
||||
|
||||
![编码解码器codec层次结构](../../media/pictures/netty/~~~~编码解码器codec层次结构.png)
|
||||
|
||||
虽然使用编码解码器可以同时编码和解码数据,但这样不利于代码的可重用性。
|
||||
相反,单独的编码器和解码器最大化了代码的可重用性和可扩展性,所以我们应该优先考虑分开使用二者。
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# Netty特性
|
||||
|
||||
## 强大的数据容器
|
||||
Netty使用自建的Buffer API实现ByteBuf,而不是使用JDK NIO的ByteBuffer来表示一个连续的字节序列。与JDK NIO的
|
||||
ByteBuffer相比,Netty的ByteBuf有更加明显的优势,这些优势可以弥补Java原生ByteBuffer的底层缺点,并提供
|
||||
更加方便的编程模型:
|
||||
|
||||
- 正常情况下,ByteBuf比ByteBuffer的性能更好;
|
||||
|
||||
- 实现了ReferenceCounted引用计数接口,优化了内存的使用;
|
||||
|
||||
- 容量可以动态增长,如StringBuilder之于String;
|
||||
|
||||
- 在读和写这两种模式切换时,无需像ByteBuffer一样调用flip方法,更易于操作;
|
||||
|
||||
...
|
||||
|
||||
|
||||
### ByteBuf的自动容量扩展
|
||||
在JDK NIO中,一旦ByteBuffer被分配了内存就不能再改变大小,这可能会带来很多不便。
|
||||
我们在创建字符串时可能不确定字符串的长度,这种情况下如果使用String可能会有多次拼接的消耗,
|
||||
所以这就是StringBuilder的作用,同样的,ByteBuf也是如此。
|
||||
|
||||
````text
|
||||
// 一种 新的动态缓冲区被创建。在内部,实际缓冲区是被“懒”创建,从而避免潜在的浪费内存空间。
|
||||
ByteBuf b = Unpooled.buffer(4);
|
||||
|
||||
// 当第一个执行写尝试,内部指定初始容量 4 的缓冲区被创建
|
||||
b.writeByte('1');
|
||||
|
||||
b.writeByte('2');
|
||||
b.writeByte('3');
|
||||
b.writeByte('4');
|
||||
|
||||
// 当写入的字节数超过初始容量 4 时,
|
||||
//内部缓冲区自动分配具有较大的容量
|
||||
b.writeByte('5');
|
||||
````
|
||||
|
||||
## 通用的传输API
|
||||
传统的Java IO在应对不同的传输协议的时候需要使用不同的API,比如java.net.Socket和java.net.DatagramSocket。
|
||||
它们分别是TCP和UDP的传输API,因此在使用它们的时候我们就需要学习不同的编程方式。这种编程模式会使得在维护
|
||||
或修改其对应的程序的时候变得繁琐和困难,简单来说就是降低了代码的可维护性。
|
||||
|
||||
这种情况还发生在Java的NIO和AIO上,由于所有的IO API无论是性能还是设计上都有所不同,所以注定这些API之间是不兼容的,
|
||||
因此我们不得不在编写程序之前先选好要使用的IO API。
|
||||
|
||||
在Netty中,有一个通用的传输API,也是一个IO编程接口: Channel,这个API抽象了所有的IO模型,如果你的应用
|
||||
已经使用了Netty的某一种传输实现,那么你的无需付出太多代价就能换成另一种传输实现。
|
||||
|
||||
Netty提供了非常多的传输实现,如nio,oio,epoll,kqueue等等,通常切换不同的传输实现只需要对几行代码进行
|
||||
修改就行了,例如选择一个不同的 [ChannnelFactory](http://netty.io/4.0/api/io/netty/bootstrap/ChannelFactory.html),
|
||||
这也是面向接口编程的一大好处。
|
||||
|
||||
|
||||
## 基于拦截链模式的事件模型
|
||||
Netty具有良好的IO事件模型,它允许我们在不破坏原有代码结构的情况下实现自己的事件类型。 很多IO框架
|
||||
没有事件模型或者在这方面做的不够好,这也是Netty的优秀设计的体现之一。
|
||||
关于事件模型可以看我编写的: [ChannelHandler](https://github.com/guang19/framework-learning/blob/dev/netty-learning/Netty%E7%BB%84%E4%BB%B6.md#ChannelHandler)
|
|
@ -1,77 +0,0 @@
|
|||
# Netty
|
||||
|
||||
**本文章总结了 《Netty in Action》(Netty实战) 和 《Netty 4.x User Guide 》(Netty 4.x 用户指南),
|
||||
再根据本人实际学习体验总结而成。本部分内容可能不那么全面,但是我尽量挑选Netty中我认为比较重要的部分做讲解。**
|
||||
|
||||
学习Netty,相信大部分同学都会选择 《Netty in Action》 和 《Netty 4.x User Guide 》,
|
||||
这里我推荐它们的通读版本,这二本书的通读版本的作者都为同一人,通读版本对《Netty in Action》做出了更为精简的概述,
|
||||
所以各位同学可酌情挑选阅读。
|
||||
|
||||
- [《Netty in Action》](https://waylau.com/essential-netty-in-action/index.html)
|
||||
|
||||
- [《Netty 4.x User Guide》](https://waylau.gitbooks.io/netty-4-user-guide/content)
|
||||
|
||||
其次我认为只看书是不够的,这里我推荐一些关于Netty入门比较优秀的视频供各位同学参考,
|
||||
推荐视频观看的顺序即下列顺序,各位同学不需要每个视频的每个章节都看,只需要挑选互补的内容学习即可:
|
||||
|
||||
- [韩顺平Netty教程](https://www.bilibili.com/video/BV1DJ411m7NR)
|
||||
|
||||
- [张龙Netty教程](https://www.bilibili.com/video/BV1cb411F7En)
|
||||
|
||||
- [索南杰夕Netty RPC实现](https://www.bilibili.com/video/BV1Rb411h7jZ)
|
||||
|
||||
|
||||
最后,在学习Netty之前,我们需要对 IO模型(网络IO模型)有一个大概的认知,可以参考我编写的:
|
||||
[Linux IO模型](../java/Linux_IO模型.md) 。
|
||||
|
||||
|
||||
````text
|
||||
如有错误之处,敬请指教。
|
||||
````
|
||||
|
||||
|
||||
## Netty是什么?
|
||||
|
||||
Netty是Red Hat开源的,一个利用Java的高级网络能力,隐藏其(Java API)背后的复杂性而提供一个易于使用的 NIO 客户端/服务端框架。
|
||||
Netty提供了高性能和可扩展性,让你自由地专注于你真正感兴趣的东西。 Netty简化了网络程序的开发过程,使用它
|
||||
我们可以快速简单地开发网络应用程序,比如客户端和服务端的通信协议,TCP和UDP的Socket开发。
|
||||
|
||||
|
||||
### Netty的特点
|
||||
Netty作为一款优秀的网络框架,自然有令人折服的特点:
|
||||
|
||||
- 设计:
|
||||
|
||||
- 针对多种传输类型的同一接口。
|
||||
|
||||
- 简单但更强大的线程模型。
|
||||
|
||||
- 真正的无连接的数据报套接字支持。
|
||||
|
||||
- 链接逻辑复用。
|
||||
|
||||
- 性能: Netty的高性能是它被广泛使用的一个重要的原因,我们可能都认为Java不太适合
|
||||
编写游戏服务端程序,但Netty的到来无疑是降低了怀疑的声音。
|
||||
|
||||
- 较原生Java API有更好的吞吐量,较低的延时。
|
||||
|
||||
- 资源消耗更少(共享池和重用)。
|
||||
|
||||
- 减少内存拷贝。
|
||||
|
||||
- 健壮性: 原生NIO的客户端/服务端程序编写较为麻烦,如果某个地方处理的不好,可能会
|
||||
导致一些意料之外的异常,如内存溢出,死循环等等,而Netty则为我们简化了原生API
|
||||
的使用,这使得我们编写出来的程序不那么容易出错。
|
||||
|
||||
- 社区: Netty快速发展的一个重要的原因就是它的社区非常活跃,这也使得采用它的开发者越来越多。
|
||||
|
||||
|
||||
## Netty架构总览
|
||||
下面是Netty的模块设计部分:
|
||||
|
||||
![Netty架构总览](../../media/pictures/netty/Netty架构总览.png)
|
||||
|
||||
Netty提供了通用的传输API(TCP/UDP...);多种网络协议(HTTP/WebSocket...);基于事件驱动的IO模型;
|
||||
超高性能的零拷贝...
|
||||
|
||||
上面说的这些模块和功能只是Netty的一部分,具体的组件在后面的部分会有较为详细的介绍。
|
|
@ -1,101 +0,0 @@
|
|||
<!-- TOC -->
|
||||
|
||||
* [Netty线程模型和EventLoop](#netty线程模型和eventloop)
|
||||
* [线程模型概述](#线程模型概述)
|
||||
* [EventLoop事件循环](#eventloop事件循环)
|
||||
* [任务调度](#任务调度)
|
||||
* [JDK任务调度](#jdk任务调度)
|
||||
* [EventLoop任务调度](#eventloop任务调度)
|
||||
* [线程管理](#线程管理)
|
||||
* [线程分配](#线程分配)
|
||||
* [非阻塞传输](#非阻塞传输)
|
||||
* [阻塞传输](#阻塞传输)
|
||||
|
||||
<!--/ TOC -->
|
||||
|
||||
# Netty线程模型和EventLoop
|
||||
由于线程模型确定了代码执行的方式,它可能带来一些副作用以及不确定因素,
|
||||
可以说这是并发编程中最大的难点,因此,我们需要了解Netty所采用的线程模型,这样
|
||||
在遇到相关问题时不至于手足无措。
|
||||
|
||||
|
||||
## 线程模型概述
|
||||
现代操作系统几乎都具有多个核心的CPU,所以我们可以使用多线程技术以有效地利用系统资源。在早期的
|
||||
Java多线程编程中,我们使用线程的方式一般都是继承Thread或者实现Runnable以此创建新的Thread,
|
||||
这是一种比较原始且浪费资源的处理线程的方式。JDK5之后引入了Executor API,其核心思想是使用池化技术
|
||||
来重用Thread,以此达到提高线程响应速度和降低资源浪费的目的。
|
||||
|
||||
|
||||
## EventLoop事件循环
|
||||
事件循环正如它的名字,处于一个循环之中。我们以前在编写网络程序的时候,会使我们处理连接的逻辑
|
||||
处于一个死循环之中,这样可以不断的处理客户端连接。
|
||||
|
||||
Netty的EventLoop采用了两个基本的API:并发和网络。
|
||||
Netty的并发包io.netty.util.concurrent是基于Java的并发包java.util.concurrent之上的,
|
||||
它主要用于提供Executor的支持。Netty的io.netty.channel包提供了与客户端Channel的事件交互的支持。
|
||||
以下是EventLoop类层次结构图:
|
||||
|
||||
![EventLoop类层次结构图](../../media/pictures/netty/EventLoop类层次结构图.png)
|
||||
|
||||
在EventLoop模型中,**EventLoop将有一个永远不会改变的Thread,即Netty会给EventLoop分配一个
|
||||
Thread,在EventLoop生命周期之中的所有IO操作和事件都由这个Thread执行。 根据配置和CPU核心的不同,
|
||||
Netty可以创建多个EventLoop,且单个EventLoop可能会服务于多个客户端Channel。**
|
||||
|
||||
在EventLoop中,**事件或任务的执行总是以FIFO先进先出的顺序执行的,这样可以保证字节总是按正确的
|
||||
顺序被处理,消除潜在的数据损坏的可能性。**
|
||||
|
||||
|
||||
## 任务调度
|
||||
有时候我们需要在指定的时间之后触发任务或者周期性的执行某一个人物,这都需要使用到任务调度。
|
||||
|
||||
|
||||
### JDK任务调度
|
||||
JDK主要有Timer和ScheduledExecutorService两种实现任务调度的方式,但是这两种原生的API的
|
||||
性能都不太适合高负载应用。
|
||||
|
||||
|
||||
### EventLoop任务调度
|
||||
上面介绍过的EventLoop类层次结构图,可以看到EventLoop扩展了ScheduledExecutorService,
|
||||
所以我们可以通过EventLoop来实现任务调度,其编程模型如下:
|
||||
|
||||
![EventLoop任务调度](../../media/pictures/netty/EventLoop任务调度.png)
|
||||
|
||||
使用Channel获取其对应的EventLoop,然后调用schedule方法给其分配一个Runnable执行。Netty的任务调度
|
||||
比JDK的任务调度性能性能要好,这主要是由于Netty底层的线程模型设计的非常优秀。
|
||||
|
||||
|
||||
## 线程管理
|
||||
Netty线程模型的卓越性能取决于当前执行任务的Thread,我们看一张图就明白了:
|
||||
|
||||
![EventLoop执行逻辑](../../media/pictures/netty/EventLoop执行逻辑.png)
|
||||
|
||||
**如果处理Chanel任务的线程正是支撑EventLoop的线程,那么与Channel的任务会被直接执行。
|
||||
否则EventLoop会将该任务放入任务队列之中稍后执行。
|
||||
需要注意的是每个EventLoop都有自己的任务队列,独立于其他EventLoop的任务队列。**
|
||||
|
||||
|
||||
## 线程分配
|
||||
每个EventLoop都注册在一个EventLoopGroup之中,一个EventLoopGroup可以包含多个EventLoop,根据不同的传输实现,
|
||||
EventLoop的创建和分配方式也不同。
|
||||
|
||||
|
||||
#### 非阻塞传输
|
||||
我们说过一个EventLoop可以处理多个Channel,Netty这样设计的目的就是尽可能的通过少量Thread来支撑大量的Channel,
|
||||
而不是每个Channel都分配一个Thread。
|
||||
|
||||
![EventLoop非阻塞分配](../../media/pictures/netty/EventLoop非阻塞分配.png)
|
||||
|
||||
EventLoopGroup负责为每个新创建的Channel分配一个EventLoop,一旦一个Channel被分配给EventLoop,它将在
|
||||
整个生命周期中都使用这个EventLoop及其Thread处理事件和任务。
|
||||
|
||||
**注意:EventLoop的分配方式对ThreadLocal的使用是很有很大影响的。因为注册在一个EventLoop上的Channel
|
||||
共有这一个线程,那么在这些Channel之间使用ThreadLocal,其ThreadLocal的状态都是一样的,无法发挥ThreadLocal
|
||||
本来的作用。**
|
||||
|
||||
|
||||
#### 阻塞传输
|
||||
阻塞传输即OIO(BIO),此种传输方式的EventLoop只会被分配一个Channel,如下图:
|
||||
|
||||
![EventLoop阻塞分配](../../media/pictures/netty/EventLoop阻塞分配.png)
|
||||
|
||||
这样带来的会是线程资源的巨大消耗,导致并发量降低。
|
|
@ -1,141 +0,0 @@
|
|||
<!-- TOC -->
|
||||
|
||||
* [Netty组件介绍](#netty组件介绍)
|
||||
* [Bootstrap/ServerBootstrap](#bootstrapserverbootstrap)
|
||||
* [Channel](#channel)
|
||||
* [EventLoop](#eventloop)
|
||||
* [ChannelFuture](#channelfuture)
|
||||
* [ChannelHandler](#channelhandler)
|
||||
* [ChannelPipeline](#channelpipeline)
|
||||
* [入站事件和出站事件的流向](#入站事件和出站事件的流向)
|
||||
* [进一步了解ChannelHandler](#进一步了解channelhandler)
|
||||
* [编码器和解码器](#编码器和解码器)
|
||||
* [SimpleChannelInboundHandler](#simplechannelinboundhandler)
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
# Netty组件介绍
|
||||
|
||||
Netty有 Bootstrap/ServerBootstrap,Channel,EventLoop,ChannelFuture,
|
||||
ChannelHandler,ChannelPipeline,编码器和解码器等核心组件。
|
||||
|
||||
**在学习Netty组件之前建议各位同学先编写一个Netty的Demo,你不必了解这个Demo的细节,
|
||||
只需要让它在你的脑海中留下一个记忆,然后对照Demo来学习以下组件,会事半功倍。**
|
||||
|
||||
[Demo](https://github.com/guang19/framework-learning/tree/dev/netty-learning/src/main/java/com/github/guang19/nettylearning/echo)
|
||||
|
||||
|
||||
|
||||
#### Bootstrap/ServerBootstrap
|
||||
Bootstrap和ServerBootstrap是Netty应用程序的引导类,它提供了用于应用程序网络层的配置。
|
||||
一般的Netty应用程序总是分为客户端和服务端,所以引导分为客户端引导Bootstrap和服务端引导ServerBootstrap,
|
||||
ServerBootstrap作为服务端引导,它将服务端进程绑定到指定的端口,而Bootstrap则是将客户端连接到
|
||||
指定的远程服务器。
|
||||
Bootstrap和ServerBootstrap除了职责不同,它们所需的EventLoopGroup的数量也不同,
|
||||
Bootstrap引导客户端只需要一个EventLoopGroup,而ServerBootstrap则需要两个EventLoopGroup。
|
||||
|
||||
![Bootstrap引导类功能](../../media/pictures/netty/Bootstrap引导类功能.png)
|
||||
|
||||
|
||||
#### Channel
|
||||
在我们使用某种语言,如c/c++,java,go等,进行网络编程的时候,我们通常会使用到Socket,
|
||||
Socket是对底层操作系统网络IO操作(如read,write,bind,connect等)的封装,
|
||||
因此我们必须去学习Socket才能完成网络编程,而Socket的操作其实是比较复杂的,想要使用好它有一定难度,
|
||||
所以Netty提供了Channel(io.netty.Channel,而非java nio的Channel),更加方便我们处理IO事件。
|
||||
|
||||
|
||||
#### EventLoop
|
||||
EventLoop用于服务端与客户端连接的生命周期中所发生的事件。
|
||||
EventLoop 与 EventLoopGroup,Channel的关系模型如下:
|
||||
|
||||
![EventLoop模型](../../media/pictures/netty/EventLoop模型.png)
|
||||
|
||||
一个EventLoopGroup通常包含一个或多个EventLoop,一个EventLoop可以处理多个Channel的IO事件,
|
||||
一个Channel也只会被注册到一个EventLoop上。**在EventLoop的生命周期中,它只会和一个Thread线程绑定,这个
|
||||
EventLoop处理的IO事件都将在与它绑定的Thread内被处理。**
|
||||
|
||||
|
||||
#### ChannelFuture
|
||||
在Netty中,所有的IO操作都是异步执行的,所以一个操作会立刻返回,但是如何获取操作执行完的结果呢?
|
||||
Netty就提供了ChannelFuture接口,它的addListener方法会向Channel注册ChannelFutureListener,
|
||||
以便在某个操作完成时得到通知结果。
|
||||
|
||||
|
||||
#### ChannelHandler
|
||||
我们知道Netty是一个款基于事件驱动的网络框架,当特定事件触发时,我们能够按照自定义的逻辑去处理数据。
|
||||
**ChannelHandler则正是用于处理入站和出站数据钩子**,它可以处理几乎所有类型的动作,所以ChannelHandler会是
|
||||
我们开发者更为关注的一个接口。
|
||||
|
||||
ChannelHandler主要分为处理入站数据的 ChannelInboundHandler和出站数据的 ChannelOutboundHandler 接口。
|
||||
|
||||
![ChannelHandler接口层次图](../../media/pictures/netty/ChannelHandler接口层次图.png)
|
||||
|
||||
Netty以适配器的形式提供了大量默认的 ChannelHandler实现,主要目的是为了简化程序开发的过程,我们只需要
|
||||
重写我们关注的事件和方法就可以了。 通常我们会以继承的方式使用以下适配器和抽象:
|
||||
|
||||
- ChannelHandlerAdapter
|
||||
- ChannelInboundHandlerAdapter
|
||||
- ChannelDuplexHandler
|
||||
- ChannelOutboundHandlerAdapter
|
||||
|
||||
|
||||
#### ChannelPipeline
|
||||
上面介绍了ChannelHandler的作用,它使我们更关注于特定事件的数据处理,但如何使我们自定义的
|
||||
ChannelHandler能够在事件触发时被使用呢? Netty提供了ChannelPipeline接口,它
|
||||
提供了存放ChannelHandler链的容器,且ChannelPipeline定义了在这条ChannelHandler链上
|
||||
管理入站和出站事件流的API。
|
||||
当一个Channel被初始化时,会使用ChannelInitializer接口的initChannel方法在ChannelPipeline中
|
||||
添加一组自定义的ChannelHandler。
|
||||
|
||||
|
||||
#### 入站事件和出站事件的流向
|
||||
|
||||
从服务端角度来看,如果一个事件的运动方向是从客户端到服务端,那么这个事件是入站的,如果事件运动的方向
|
||||
是从服务端到客户端,那么这个事件是出站的。
|
||||
|
||||
![Netty出站入站](../../media/pictures/netty/Netty出站入站.png)
|
||||
|
||||
上图是Netty事件入站和出站的大致流向,入站和出站的ChannelHandler可以被安装到一个ChannelPipeline中,
|
||||
**如果一个消息或其他的入站事件被[读取],那么它会从ChannelPipeline的头部开始流动,并传递给第一个ChannelInboundHandler
|
||||
,这个ChannelHandler的行为取决于它的具体功能,不一定会修改消息。 在经历过第一个ChannelInboundHandler之后,
|
||||
消息会被传递给这条ChannelHandler链的下一个ChannelHandler,最终消息会到达ChannelPipeline尾端,消息的读取也就结束了。**
|
||||
|
||||
**数据的出站(消息被[写出])流程与入站是相似的,在出站过程中,消息从ChannelOutboundHandler链的尾端开始流动,
|
||||
直到到达它的头部为止,在这之后,消息会到达网络传输层进行后续传输。**
|
||||
|
||||
|
||||
#### 进一步了解ChannelHandler
|
||||
鉴于入站操作和出站操作是不同的,可能有同学会疑惑:为什么入站ChannelHandler和出站ChannelHandler的数据
|
||||
不会窜流呢(为什么入站的数据不会到出站ChannelHandler链中)? 因为Netty可以区分ChannelInboundHandler和
|
||||
ChannelOutboundHandler的实现,并确保**数据只在两个相同类型的ChannelHandler直接传递,即数据要么在
|
||||
ChannelInboundHandler链之间流动,要么在ChannelOutboundHandler链之间流动。**
|
||||
|
||||
**当ChannelHandler被添加到ChannelPipeline中后,它会被分配一个ChannelHandlerContext,
|
||||
它代表了ChannelHandler和ChannelPipeline之间的绑定。 我们可以使用ChannelHandlerContext
|
||||
获取底层的Channel,但它最主要的作用还是用于写出数据。**
|
||||
|
||||
|
||||
#### 编码器和解码器
|
||||
当我们通过Netty发送(出站)或接收(入站)一个消息时,就会发生一次数据的转换,因为数据在网络中总是通过字节传输的,
|
||||
所以当数据入站时,Netty会解码数据,即把数据从字节转为为另一种格式(通常是一个Java对象),
|
||||
当数据出站时,Netty会编码数据,即把数据从它当前格式转为为字节。
|
||||
|
||||
Netty为编码器和解码器提供了不同类型的抽象,这些编码器和解码器其实都是ChannelHandler的实现,
|
||||
它们的名称通常是ByteToMessageDecoder和MessageToByteEncoder。
|
||||
|
||||
对于入站数据来说,解码其实是解码器通过重写ChannelHandler的read事件,然后调用它们自己的
|
||||
decode方法完成的。
|
||||
对于出站数据来说,编码则是编码器通过重写ChannelHandler的write事件,然后调用它们自己的
|
||||
encode方法完成的。
|
||||
|
||||
**为什么编码器和解码器被设计为ChannelHandler的实现呢?**
|
||||
|
||||
我觉得这很符合Netty的设计,上面已经介绍过Netty是一个事件驱动的框架,其事件由特定的ChannelHandler
|
||||
完成,我们从用户的角度看,编码和解码其实是属于应用逻辑的,按照应用逻辑实现自定义的编码器和解码器就是
|
||||
理所应当的。
|
||||
|
||||
|
||||
#### SimpleChannelInboundHandler
|
||||
在我们编写Netty应用程序时,会使用某个ChannelHandler来接受入站消息,非常简单的一种方式
|
||||
是扩展SimpleChannelInboundHandler< T >,T是我们需要处理消息的类型。 继承SimpleChannelInboundHandler
|
||||
后,我们只需要重写其中一个或多个方法就可以完成我们的逻辑。
|
|
@ -1,161 +0,0 @@
|
|||
<!-- TOC -->
|
||||
|
||||
* [传输(Transport)](#传输transport)
|
||||
* [传输API](#传输api)
|
||||
* [Netty内置的传输](#netty内置的传输)
|
||||
* [零拷贝](#零拷贝)
|
||||
* [内存映射(Memory Mapped)](#内存映射memory-mapped)
|
||||
* [文件传输(SendFile)](#文件传输sendfile)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
|
||||
# 传输(Transport)
|
||||
在网络中传递的数据总是具有相同的类型:字节。 这些字节流动的细节取决于网络传输,它是一个帮我们抽象
|
||||
底层数据传输机制的概念,我们不需要关心字节流动的细节,只需要确保字节被可靠的接收和发送。
|
||||
|
||||
当我们使用Java网络编程时,可能会接触到多种不同的网络IO模型,如NIO,BIO(OIO: Old IO),AIO等,我们可能因为
|
||||
使用这些不同的API而遇到问题。
|
||||
Netty则为这些不同的IO模型实现了一个通用的API,我们使用这个通用的API比直接使用JDK提供的API要
|
||||
简单的多,且避免了由于使用不同API而带来的问题,大大提高了代码的可读性。
|
||||
在传输这一部分,我们将主要学习这个通用的API,以及它与JDK之间的对比。
|
||||
|
||||
|
||||
### 传输API
|
||||
传输API的核心是Channel(io.netty.Channel,而非java nio的Channel)接口,它被用于所有的IO操作。
|
||||
|
||||
Channel结构层次:
|
||||
|
||||
![Channel接口层次](../../media/pictures/netty/Channel接口层次.png)
|
||||
|
||||
每个Channel都会被分配一个ChannelPipeline和ChannelConfig,
|
||||
ChannelConfig包含了该Channel的所有配置,并允许在运行期间更新它们。
|
||||
|
||||
ChannelPipeline在上面已经介绍过了,它存储了所有用于处理出站和入站数据的ChannelHandler,
|
||||
我们可以在运行时根据自己的需求添加或删除ChannelPipeline中的ChannelHandler。
|
||||
|
||||
此外,Channel还有以下方法值得留意:
|
||||
|
||||
| 方法名 | 描述 |
|
||||
| :---: | :---: |
|
||||
| eventLoop | 返回当前Channel注册到的EventLoop |
|
||||
| pipeline | 返回分配给Channel的ChannelPipeline |
|
||||
| isActive | 判断当前Channel是活动的,如果是则返回true。 此处活动的意义依赖于底层的传输,如果底层传输是TCP Socket,那么客户端与服务端保持连接便是活动的;如果底层传输是UDP Datagram,那么Datagram传输被打开就是活动的。 |
|
||||
| localAddress | 返回本地SocketAddress |
|
||||
| remoteAddress | 返回远程的SocketAddress |
|
||||
| write | 将数据写入远程主机,数据将会通过ChannelPipeline传输 |
|
||||
| flush | 将之前写入的数据刷新到底层传输 |
|
||||
| writeFlush | 等同于调用 write 写入数据后再调用 flush 刷新数据 |
|
||||
|
||||
|
||||
### Netty内置的传输
|
||||
Netty内置了一些开箱即用的传输,我们上面介绍了传输的核心API是Channel,那么这些已经封装好的
|
||||
传输也是基于Channel的。
|
||||
|
||||
Netty内置Channel接口层次:
|
||||
|
||||
![Netty内置Channel接口层次](../../media/pictures/netty/Netty内置Channel接口层次.png)
|
||||
|
||||
| 名称 | 包 | 描述 |
|
||||
| :---: | :---: | :---|
|
||||
| NIO | io.netty.channel.socket.nio | NIO Channel基于java.nio.channels,其io模型为IO多路复用 |
|
||||
| Epoll | io.netty.channel.epoll | Epoll Channel基于操作系统的epoll函数,其io模型为IO多路复用,不过Epoll模型只支持在Linux上的多种特性,比NIO性能更好 |
|
||||
| KQueue | io.netty.channel.kqueue | KQueue 与 Epoll 相似,它主要被用于 FreeBSD 系统上,如Mac等 |
|
||||
| OIO(Old Io) | io.netty.channel.socket.oio | OIO Channel基于java.net包,其io模型是阻塞的,且此传输被Netty标记为deprecated,故不推荐使用,最好使用NIO / EPOLL / KQUEUE 等传输 |
|
||||
| Local | io.netty.channel.local | Local Channel 可以在VM虚拟机内部进行本地通信 |
|
||||
| Embedded | io.netty.channel.embedded | Embedded Channel允许在没有真正的网络传输中使用ChannelHandler,可以非常有用的测试ChannelHandler |
|
||||
|
||||
|
||||
### 零拷贝
|
||||
零拷贝(Zero-Copy)是一种目前只有在使用NIO和Epoll传输时才可使用的特性。
|
||||
在我之前写过的IO模型中,所有的IO的数据都是从内核复制到用户应用进程,再由用户应用进程处理。
|
||||
而零拷贝则可以快速地将数据从源文件移动到目标文件,无需经过用户空间。
|
||||
|
||||
在学习零拷贝技术之前先学习一下普通的IO拷贝过程吧,
|
||||
这里举个栗子: 我要使用一个程序将一个目录下的文件复制到另一个目录下,
|
||||
在普通的IO中,其过程如下:
|
||||
|
||||
![普通IO拷贝](../../media/pictures/netty/普通IO拷贝.png)
|
||||
|
||||
**应用程序启动后,向内核发出read调用(用户态切换到内核态),操作系统收到调用请求后,
|
||||
会检查文件是否已经缓存过了,如果缓存过了,就将数据从缓冲区(直接内存)拷贝到用户应用进程(内核态切换到用户态),
|
||||
如果是第一次访问这个文件,则系统先将数据先拷贝到缓冲区(直接内存),然后CPU将数据从缓冲区拷贝到应用进程内(内核态切换到用户态),
|
||||
应用进程收到内核的数据后发起write调用,将数据拷贝到目标文件相关的堆栈内存(用户态切换到内核态),
|
||||
最后再从缓存拷贝到目标文件。**
|
||||
|
||||
根据上面普通拷贝的过程我们知道了其缺点主要有:
|
||||
|
||||
1. 用户态与内核态之间的上下文切换次数较多(用户态发送系统调用与内核态将数据拷贝到用户空间)。
|
||||
2. 拷贝次数较多,每次IO都需要DMA和CPU拷贝。
|
||||
|
||||
而零拷贝正是针对普通拷贝的缺点做了很大改进,使得其拷贝速度在处理大数据的时候很是出色。
|
||||
|
||||
零拷贝主要有两种实现技术:
|
||||
|
||||
1. 内存映射(mmp)
|
||||
2. 文件传输(sendfile)
|
||||
|
||||
可以参照我编写的demo进行接下来的学习:
|
||||
|
||||
[zerocopy](https://github.com/guang19/framework-learning/tree/dev/netty-learning/src/main/java/com/github/guang19/nettylearning/zerocopy/ZeroCopyTest.java)
|
||||
|
||||
|
||||
#### 内存映射(Memory Mapped)
|
||||
````text
|
||||
内存映射对应JAVA NIO的API为
|
||||
FileChannel.map。
|
||||
````
|
||||
|
||||
当用户程序发起 mmp 系统调用后,操作系统会将文件的数据直接映射到内核缓冲区中,
|
||||
且缓冲区会与用户空间共享这一块内存,这样就无需将数据从内核拷贝到用户空间了,用户程序接着发起write
|
||||
调用,操作系统直接将内核缓冲区的数据拷贝到目标文件的缓冲区,最后再将数据从缓冲区拷贝到目标文件。
|
||||
|
||||
其过程如下:
|
||||
|
||||
![mmp零拷贝](../../media/pictures/netty/mmp零拷贝.png)
|
||||
|
||||
内存映射由原来的四次拷贝减少到了三次,且拷贝过程都在内核空间,这在很大程度上提高了IO效率。
|
||||
|
||||
但是mmp也有缺点: 当我们使用mmp映射一个文件到内存并将数据write到指定的目标文件时,
|
||||
如果另一个进程同时对这个映射的文件做出写的操作,用户程序可能会因为访问非法地址而产生一个错误的信号从而终止。
|
||||
|
||||
试想一种情况:我们的服务器接收一个客户端的下载请求,客户端请求的是一个超大的文件,服务端开启一个线程
|
||||
使用mmp和write将文件拷贝到Socket进行响应,如果此时又有一个客户端请求对这个文件做出修改,
|
||||
由于这个文件先前已经被第一个线程mmp了,可能第一个线程会因此出现异常,客户端也会请求失败。
|
||||
|
||||
解决这个问题的最简单的一种方法就对这个文件加读写锁,当一个线程对这个文件进行读或写时,其他线程不能操作此文件,
|
||||
不过这样处理并发的能力可能就大打折扣了。
|
||||
|
||||
|
||||
#### 文件传输(SendFile)
|
||||
|
||||
````text
|
||||
文件传输对应JAVA NIO的API为
|
||||
FileChannel.transferFrom/transferTo
|
||||
````
|
||||
|
||||
在了解sendfile之前,先来看一下它的函数原型(linux系统的同学可以使用 man sendfile 查看):
|
||||
|
||||
````text
|
||||
#include<sys/sendfile.h>
|
||||
|
||||
ssize_t
|
||||
|
||||
sendfile(int out_fd,
|
||||
int in_fd,
|
||||
off_t *offset,
|
||||
size_t count);
|
||||
````
|
||||
|
||||
sendfile在代表输入文件的文件描述符 in_fd 和 输入文件的文件描述符 out_fd 之间传输文件内容,
|
||||
这个传输过程完全是在内核之中进行的,程序只需要把输入文件的描述符和输出文件的描述符传递给
|
||||
sendfile调用,系统自然会完成拷贝。 当然,sendfile和mmp一样都有相同的缺点,在传输过程中,
|
||||
如果有其他进程截断了这个文件的话,用户程序仍然会被终止。
|
||||
|
||||
sendfile传输过程如下:
|
||||
|
||||
![sendfile零拷贝](../../media/pictures/netty/sendfile零拷贝.png)
|
||||
|
||||
它的拷贝次数与mmp一样,但是无需像mmp一样与用户进程共享内存了。
|
||||
|
||||
|
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 17 KiB |