Netty实战第一章
简述
在学习Netty源码的过程中,十分吃力看来还是需要先从使用在到研究过程,因此将《Netty实战》一书重新拾起,以下是相关笔记;
Netty基础组件
- Channel;
- 回调
- Future
- 事件和ChannelHandler
Channel
Channel是对Java NIO的一个抽象;
代表一个到实体(例如硬件设备、文件、网络套接字)的开发连接,可以执行读操作和写操作,可以把Channel看作传入或传出数据的载体.因此它们可以被打开或关闭;
回调
一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用,这使得后者方法可以在适当的时候来回调前者;
Future
Future提供了另外一种在操作完成时通知应用程序的方式.这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问;在JDK中提供的已有的Future实现,需要手动检查对于操作是否完成或者需要柱塞,十分繁琐;因此在Netty提供了ChannelFuture实现,用于在执行异步操作的时候使用;
事件和ChannelHandler
Netty中使用不同的事件来通知我们状态的改变或者操作的状态,如下图所示的事件驱动
!(ChannelHandler链的入站和出站事件)[]
-
Future、回调和ChannelHandler
Netty的异步编程模型是建立在Future和回调的概念之上的,而将事件分派到ChannelHandler的方法则是建立在这个基础之上更高层的概念; -
选择器、事件和EventLoop
Netty通过触发事件将Selector从应用程序中抽象出来,简化了原来需要手动编写的派发代码.在netty内部会为每一个Channel分配一个EventLoop用于处理所有事件,包括:
- 注册感兴趣的事件
- 将事件派发给ChannelHandler
- 安排进一步动作
EventLoop本身就是一个线程驱动,其处理了一个Channel的所有I/O事件,并且在该EventLoop的整个生命周期内都不会发生改变;
Netty的Hello World

服务端是可以同时处理多个客户端连接的,客户端在建立一个连接之后,它会先服务端发送一个或者多个消息,反过来,服务器邮费将每个消息发回客户端,这就是典型的请求⇌响应模式;
服务端代码
- EchoServerHandler实现了业务处理逻辑
EchoServer创建引导过程:
- 创建一个ServerBootstrap的实例来进行引导和绑定服务器
- 创建并分配一个NioEventLoopGroup实例以进行事件的处理,如接受新连接以及读写数据
- 指定服务器绑定的本地InetSocketAddress
- 使用一个EchoServerHandler实例初始化每一个新的Channel
- 调用ServerBootstrap.bind()方法以绑定服务器
客户端代码
EchoClient.kt
EchoClientHandler.kt
EchoClientHandler业务逻辑:
- channelActive - 在到服务器的连接已经建立之后将被调用
- channelRead - 当从服务器收到一条消息时被调用
- exceptionCaught - 在处理过程中引发异常时将被调用
channelRead方法,每当接收到数据时,都会调用这个方法,需要注意的是,它在接受服务器发送的消息时可能会被分段接收;
举一个例子就是,当服务器发送5 Byte时,客户端不能保证这5 Byte会被一次性全部接收,可能是第一次接受到3 Byte,第二次接受到2 Byte;或者反之亦然;
TCP协议作为一个面向流的协议,它会保证数据流会按照服务端的发送顺序来进行接收;
EchoClient引导逻辑:
- 与服务端引导逻辑类似
- 创建BootStrap,客户端这里是BootStrap,不是ServerBootStrap
- 指定EventLoopGroup处理客户端事件
- 选择NIO传输的Channel类型
- 设置服务端的地址
- 设置业务处理链
执行结果

小结
通过Echo服务端与客户端的配合,我们初步了解到了创建一个Netty程序需要有两部分组成:引导程序、业务处理,接下来我们详细的了解一下Netty的组件和设计;
Netty的组件和设计
Netty的组件设计可以分为两个大类:Netty网络抽象的代表和管理数据流和业务逻辑组件;
Netty网络抽象的代表:
- Channel - 对Socket的抽象
- EventLoop - 控制流、多线程处理、并发
- ChannelFuture - 异步通知
管理数据流和业务逻辑组件:
- ChannelHandler - 业务程序逻辑的容器
- ChannelPipeline - 事件处理链
Channel接口
Channel是对Socket的抽象接口,提供的API用于简化Socket类使用的复杂性,常见的实现类有:
- EmbeddedChannel
- LocalServerChannel
- NioDatagramChannel
- NioSctpChannel
- NioSocketChannel
EventLoop接口
EventLoop接口是Netty的核心抽象,用于处理连接的生命周期中所发生的事件;

ChannelFuture
在Netty中所有的I/O操作都是异步进行处理的,因为一个操作可能不会立即返回结果,所以Netty中定义了一种用于在之后某个事件点确定其结果的方法,这个结果就是ChannelFuture,其addListener方法注册了一个ChannelFutureListener,以便在某个操作完成时得到通知;
ChannelPipeline
ChannelPipeline为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API.当Channel被创建时,它被自动分配到它专属的ChannelPipenlines上;

ChannelHandler安装到ChannelPipeline中的过程如下所示:
- 一个ChannelInitializer的实现被注册到了ServerBootstrap中
- 当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在ChannelPipline中安装一组自定义的ChannelHandler
- ChannelInitializer将它自己从ChannelPipeline中移除
源代码如下:
@SuppressWarnings("unchecked")
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
if (initMap.add(ctx)) { // Guard against re-entrance.
try {
initChannel((C) ctx.channel());
} catch (Throwable cause) {
// Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...).
// We do so to prevent multiple calls to initChannel(...).
exceptionCaught(ctx, cause);
} finally {
if (!ctx.isRemoved()) {
ctx.pipeline().remove(this);
}
}
return true;
}
return false;
}
.handler(object : ChannelInitializer<SocketChannel?>() {
override fun initChannel(ctx: SocketChannel?) {
ctx!!.pipeline().addLast(EchoClientHandler())
}
ChannelInitializer.initChannel()将一组自定义的ChannelHandler注册到ChannelPipeline链上;ChannelHandler是专门为支持广泛的用途而设计,可以将它看作为是处理往来ChannelPipeline事件的任何代码的通用容器;使得事件流经ChannelPipline是ChannelHandler的工作,它们是在应用程序的初始化或者引导阶段被安装的;
ChannelHandler
在Netty中以适配器的形式提供了大量默认的ChannelHandler实现类,用于简化应用程序处理逻辑的开发过程;常用的适配器类有:ChannelHandlerAdapter、ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter、ChannelDuplexHandler
- 编码器和解码器
当你通过Netty发送或接收一个消息的时候,就将会发生一次数据转换,也就是说字节码会转换为另外一种格式,通常就是一个java对象;如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节;Netty为编码器和解码器提供了不同类型的抽象类;
Netty提供的编码器和解码器适配器类都实现了ChannelOutboundHandler接口或者ChannelInboundHandler接口
引导程序
从上面的例子中可以看到,我们在设置Netty的客户端或者服务端配置时,首先选择引导类BootStrap或者ServerBootStrap,下面是这两种引导类下一些区别;
类别 | BootStrap | ServerBootStrap |
---|---|---|
网络编程的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup的数目 | 1 | 2 |

服务器设置两组不同的Channel,第一组只包含服务器自身的已绑定到某个本地端口的正在监听的套接字、第二组将包含所有已创建的用来处理传入的客户端连接的Channel;
与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel的EventLoop;一旦连接被接受,第二个EventLoopGroup将会给它的Channel分配一个EventLoop;
传输
Netty中最为重要的知识,传输-可以使用阻塞传输、异步传输、Local等方式,在Netty中它为所有的传输实现都提供了通用的API;
例子
- JDK阻塞处理的例子
在这段代码在每次接受连接时都会创建一个新的线程。这会导致大量的线程被创建和销毁,消耗大量的系统资源。为了避免这种情况,可以使用线程池来管理线程,重复使用现有的线程来处理新的连接、或者使用非柱塞的方式来进行,下面是一个非阻塞的例子;
- JDK非阻塞的例子
从阻塞切换到非阻塞的代码十分复杂,下面看一下Netty是如何屏蔽这部分差异的
- 使用Netty的阻塞网络处理
- 使用Netty的非阻塞网络处理
可以看到Netty切换网络通道只需要调整NioEventLoopGroup、NioServerSocketChannel即可;
传输API
传输API的核心是interface Channel,它被用于所有的I/O操作,Channel类的层次结构如下所示:

每一个Channel都会被分配一个ChannelPipeline和ChannelConfig,ChannelConfig中包含了该Channel的所有配置设置,并且支持热更新;
由于Channel是独一无二的,所以为了保证顺序Channel实现了Comparable接口;
ChannelPipeline是用于持有处理入站和出站以及业务处理事件的ChannelHandler实例,在代码中主要是add/remove/get等方法;
ChannelPipeline是典型的过滤器模式,数据经过不同的ChannelHandler进行处理;
在上一个步骤中引出了一个重要的类ChannelHandler,它实现了所有应用程序用于处理状态变化以及数据处理的逻辑,常用的类型有:
- 数据格式转换,将数据同二进制格式转换为业务格式,反之亦然
- 提供异常的通知
- 提供Channel变为活动的或者非活动的通知
- 提供当Channel注册到EventLoop或者从EventLoop注销时的通知
- 提供有关用户自定义事件的通知
channel重要的方法列表:

channel是线程安全的,可以多个线程同时操作;
内置的传输类型
Netty内置了一些开箱即用的传输;这些传输包括jdk、linux等的特殊类型;
名称 | 包 | 描述 |
---|---|---|
NIO | io.netty.channel.socket.nio | 使用java.nio.channels包作为基础,基于选择器的方式 |
Epooll | io.netty.channel.epoll | 由JNI驱动的epoll()和非阻塞I/O,这个传输支持只有在Linux上可用的多种特性.如SO_REUSEPORT,比NIO传输更快,并且是完全非阻塞的; |
OIO | io.netty.channel.socket.oio | 使用java.net包作为基础,使用的是阻塞流 |
Local | io.netty.channel.local | 在JVM内部通过管道进行通信的本地传输形式 |
Embedded | io.netty.channel.embedded | 测试channelHandler使用 |
NIO-非阻塞I/O
NIO提供了一个所有I/O操作的全异步的实现,它是基于JDK1.4引入的NIO子系统中的选择器API来进行实现;
选择器背后的基本概念是充当一个注册表,在哪里你将可以请求在Channel的状态发生变化时得到通知,可能发生的状态变化有:
- 新的Channel已被接受并且就绪
- Channel连接已经完成
- Channel有已经就绪的可供读取的数据
- Channel可用于写数据
选择器运行在一个检查状态变化并对其做出相应响应的线程上,在应用程序对状态的改变做出响应之后,选择器状态会被重置,然后继续重复这过程;


零拷贝其实在Windows下有API进行支持;
Epooll-用于Linux的本地非阻塞传输
Netty为Linux提供了一组NIO API,其以一种和它本身的设计更加一致的方式使用epoll,实现类是EpollEventLoopGroup
OIO-旧的阻塞I/O
Netty的OIO传输实现代表了一种折中的办法;它可以通过常规的传输API使用,但是由于它是建立在java.net包的阻塞实现之上的,所以它不是异步的;Netty利用SO_TIMEOUT这个Socket标识,它指定了等待一个I/O操作完成的最大毫秒数.如果这个操作在指定时间间隔内没有完成,则将会抛出一个SocketTimeout_Exception,Netty将捕获这个异常并继续循环处理,在下次运行EventLoop时,它将再次尝试;
用于JVM内部通信的Local传输
Netty提供了一个Local传输,用于在同一个JVM中运行的客户端和服务端程序之间的异步通信.在这个传输中,和服务器Channel相关联的SocketAddress并没有绑定物理网络地址;
Embedded 传输
Netty提供了一种额外的传输方式,使得你可以将一组ChannelHandler作为帮助器嵌入到其他的ChannelHandler内部;
ByteBuf
网络数据的基本单位是字节,Java NIO提供了ByteBuffer</B作为它的字节容器,但是这个类使用过于复杂;因此Netty在内部进行了分封装,通过API提供了两个类:ByteBuf和ByteBufHolder;
ByteBuf内部会维护两个不同的索引,一个用于读取,一个用于写入,当你从ByteBuf中读取数据时,它的readderIndex将会递增已经被读取过的字节数.同样的,当你写入ByteBuf时,它的writeIndex也会进行递增;
-
堆缓冲区
最常用的ByteBuf模式就是将数据存储在JVM的堆空间中; -
直接缓冲区
直接缓存区是另外一种ByteBuf模式,我们期望用用于对象创建的内存分配永远都来自于堆中,但这并不是必须的,在JDK 1.4中引入的ButeBuffer类允许JVM实现通过本地调用来分配内存;这样可以避免每次在调用本地I/O操作之前将缓冲区的内容复制到一个中间缓冲区中;
“直接缓冲区的内存将驻留在常规的会被垃圾回收的堆内存之外”,直接缓冲区的主要缺点是相对与基于堆内存的缓冲区,它们的分配和释放都比较昂贵 -
复合缓冲区
复合缓冲区指的是,它为多个ByteBuf提供一个聚合视图,通过这个聚合视图可以根据需要进行添加和删除ByteBuf实例,用于补充JDK中的ByteBuffer中缺失的这个特性;
Netty中通过ByteBuf的子类 -> CompositeByteBuf来实现这个功能,它提供了一个将多个缓冲区聚合成为单个合并缓冲区的虚拟实现;
字节级操作
- 随机访问索引
- 顺序访问索引
- 可丢弃字节
- 可读字节
- 可写字节
- 索引管理
- 查找操作
- 派生缓冲区