remove netty

This commit is contained in:
guide 2020-09-01 10:39:13 +08:00
parent 34f83aa619
commit ce5869db4b
49 changed files with 0 additions and 1351 deletions

View File

@ -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)
## 操作系统

View File

@ -1,125 +0,0 @@
<!-- TOC -->
* [Bootstrap引导](#bootstrap引导)
* [Bootstrap类](#bootstrap类)
* [引导客户端和无连接协议](#引导客户端和无连接协议)
* [引导服务端](#引导服务端)
<!--/ TOC -->
# Bootstrap引导
在了解ChanelPipelineEventLoop等组件之后我们需要将这些组件组织起来使其成为一个可运行的应用程序。
这里就需要引导这些组件了。
## 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();
//配置各种属性如ChannelChannelHandler等
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();
//配置各种属性如ChannelChannelHandler等
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();
}
}
}
);
````

View File

@ -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中获取数据
相比于原生BufferByteBuf更加灵活和易用。
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异常。
#### 索引管理
我们可以通过markReaderIndexmarkWriterIndex方法来标记当前readerIndex和writerIndex的位置
然后使用resetReaderIndexresetWriterIndex方法来将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接口。

View File

@ -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处理它的方法将被ChannelChannelPipeline以及ChannelHandlerContext调用
ChannelChannelPipelineChannelHandlerContext都拥有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
都会被分配一个新的ChannelPipelineChannel和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也能保证数据一致。

View File

@ -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)
虽然使用编码解码器可以同时编码和解码数据,但这样不利于代码的可重用性。
相反,单独的编码器和解码器最大化了代码的可重用性和可扩展性,所以我们应该优先考虑分开使用二者。

View File

@ -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提供了非常多的传输实现如niooioepollkqueue等等通常切换不同的传输实现只需要对几行代码进行
修改就行了,例如选择一个不同的 [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)

View File

@ -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提供了通用的传输APITCP/UDP...多种网络协议HTTP/WebSocket...基于事件驱动的IO模型
超高性能的零拷贝...
上面说的这些模块和功能只是Netty的一部分具体的组件在后面的部分会有较为详细的介绍。

View File

@ -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可以处理多个ChannelNetty这样设计的目的就是尽可能的通过少量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)
这样带来的会是线程资源的巨大消耗,导致并发量降低。

View File

@ -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/ServerBootstrapChannelEventLoopChannelFuture
ChannelHandlerChannelPipeline编码器和解码器等核心组件。
**在学习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 与 EventLoopGroupChannel的关系模型如下
![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
后,我们只需要重写其中一个或多个方法就可以完成我们的逻辑。

View File

@ -1,161 +0,0 @@
<!-- TOC -->
* [传输(Transport)](#传输transport)
* [传输API](#传输api)
* [Netty内置的传输](#netty内置的传输)
* [零拷贝](#零拷贝)
* [内存映射Memory Mapped](#内存映射memory-mapped)
* [文件传输(SendFile)](#文件传输sendfile)
<!-- /TOC -->
# 传输(Transport)
在网络中传递的数据总是具有相同的类型:字节。 这些字节流动的细节取决于网络传输,它是一个帮我们抽象
底层数据传输机制的概念,我们不需要关心字节流动的细节,只需要确保字节被可靠的接收和发送。
当我们使用Java网络编程时可能会接触到多种不同的网络IO模型如NIOBIO(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一样与用户进程共享内存了。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB