Netty 是一个 Java NIO 客户端服务器框架,使用它可以快速简单地开发网络应用程序,比如服务器和客户端的协议。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。
JDK 原生 NIO 程序的问题
JDK 原生也有一套网络应用程序 API,但是存在一系列问题,主要如下:
NIO 的类库和 API 繁杂,使用麻烦。你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
需要具备其他的额外技能做铺垫。例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。
可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。
NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。
JDK NIO 的 Bug。例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。
官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。
Netty 的特点
Netty 对 JDK 自带的 NIO 的 API 进行封装,解决上述问题,主要特点有:
设计优雅,适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池;真正的无连接数据报套接字支持(自 3.1 起)。
使用方便,详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。
高性能,吞吐量更高,延迟更低;减少资源消耗;最小化不必要的内存复制。
安全,完整的 SSL/TLS 和 StartTLS 支持。
社区活跃,不断更新,社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。
Netty 常见使用场景
Netty 常见的使用场景如下:
互联网行业。在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。
典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。
游戏行业。无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。
非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过 Netty 进行高性能的通信。
大数据领域。经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。
有兴趣的读者可以了解一下目前有哪些开源项目使用了 Netty:Related Projects。
Netty 高性能设计
Netty 作为异步事件驱动的网络,高性能之处主要来自于其 I/O 模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。
I/O 模型
用什么样的通道将数据发送给对方,BIO、NIO 或者 AIO,I/O 模型在很大程度上决定了框架的性能。
阻塞 I/O
传统阻塞型 I/O(BIO)可以用下图表示:
特点如下:
每个请求都需要独立的线程完成数据 Read,业务处理,数据 Write 的完整操作问题。
当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
在 I/O 复用模型中,会用到 Select,这个函数也会使进程阻塞,但是和阻塞 I/O 所不同的是这两个函数可以同时阻塞多个 I/O 操作。
而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。
Netty 的非阻塞 I/O 的实现关键是基于 I/O 复用模型,这里用 Selector 对象表示:
Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端连接。
当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
基于 Buffer
传统的 I/O 是面向字节流或字符流的,以流式的方式顺序地从一个 Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。
在 NIO 中,抛弃了传统的 I/O 流,而是引入了 Channel 和 Buffer 的概念。在 NIO 中,只能从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。
基于 Buffer 操作不像传统 IO 的顺序操作,NIO 中可以随意地读取任意位置的数据。
NIO与BIO
BIO(传统IO):
BIO是一个同步并阻塞的IO模式,传统的 java.io 包,它基于流模型实现,提供了我们
最熟知的一些 IO 功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,
也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在
那里,它们之间的调用是可靠的线性顺序。
NIO(Non-blocking/New I/O)
NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应 java.nio 包,提供
了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,
不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统
BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和
ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种
模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
NIO的特点:
一个线程可以处理多个通道,减少线程创建数量;
读写非阻塞,节约资源:没有可读/可写数据时,不会发生阻塞导致线程资源的浪费
Reactor 模型
Netty基本组件
channel
Channel
Channel是 Java NIO 的一个基本构造。可以看作是传入或传出数据的载体。
因此,它可以被打开或关闭,连接或者断开连接。
EventLoop 与 EventLoopGroup
EventLoop 与 EventLoopGroup
EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,
在内部,将会为每个Channel分配一个EventLoop。
EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。
Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、
对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命
周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。
一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命
周期内是不能改变的。一个 EventLoop 可以与多个 Channel 绑定。即 Channel
与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。
ServerBootstrap 与 Bootstrap
ServerBootstrap 与 Bootstrap
Bootstarp 和 ServerBootstrap 被称为引导类(启动类),指对应用程序进行配置,
并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。
Bootstrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和
connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、
没有父 Channel 的 Channel 来实现所有的网络交换。
ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind()
方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel
管理了多个子 Channel 用于同客户端之间的通信。
ChannelHandler 与 ChannelPipeline
ChannelHandler 与 ChannelPipeline
ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本
身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个
ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。
ChannelFuture
ChannelFuture
Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,
所以 Netty 中定义了一个 ChannelFuture 对象作为这个异步操作的“代言人”,
表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的
addListener() 方法为该异步操作添加监 NIO 网络编程框架 Netty 听器,为其注册
回调:当结果出来后马上调用执行。
Netty 的异步编程模型都是建立在 Future 与回调概念之上的。
Writing a Discard Server 写个抛弃服务器
世上最简单的协议不是'Hello, World!' 而是 DISCARD(抛弃服务)。这个协议将会抛弃任何收到的数据,而不响应。
为了实现 DISCARD 协议,你只需忽略所有收到的数据。让我们从 handler (处理器)的实现开始,handler 是由 Netty 生成用来处理 I/O 事件的。
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* 处理服务端 channel.
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
//每当客户端收到新数据时,这个方法会在收到消息是被调用,这个例子中收到的消息类型是ByteBuf
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
// 默默地丢弃收到的数据
((ByteBuf) msg).release(); // (3)为了实现 DISCARD 协议,处理器不得不忽略所有接受到的消息
}
//当出现 Throwable 对象才会被调用
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// 当出现异常就关闭连接
cause.printStackTrace();
ctx.close();
}
}
丢弃协议服务启动类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* 丢弃任何进入的数据
*/
public class DiscardServer {
private int port;
public DiscardServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)用来处理 I/O 操作的多线程事件循环器 ‘boss’,用来接收进来的连接
EventLoopGroup workerGroup = new NioEventLoopGroup();//‘worker’,用来处理已经被接收的连接
try {
ServerBootstrap b = new ServerBootstrap(); // (2)启动 NIO 服务的辅助启动类
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // (3)新的 Channel 如何接收进来的连接
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)事件处理类经常会被用来处理一个最近的已经接收的 Channel
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DisCardServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128) // (5)设置 Channel 实现的配置参数
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
// 绑定端口,开始接收进来的连接
ChannelFuture f = b.bind(port).sync(); // (7)绑定端口 异步
// 等待服务器 socket 关闭 。
// 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
//至此已完成了第一个基于 Netty 的服务端程序
public static void main(String[] args) throws Exception {
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8080;
}
new DiscardServer(port).run();
}
}
测试服务运行效果,main启动服务后;cmd--->telnet ip 端口
telnet localhost 8080
由于他是一个丢弃协议服务所以没有响应无法判断服务是否启动成功。
调整DiscardServerHandler 类的 channelRead() 方法,可以在数据被接收时调用。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
try {
while (in.isReadable()) { // (1)这个低效的循环事实上可以简化为:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
System.out.print((char) in.readByte());
System.out.flush();
}
} finally {
ReferenceCountUtil.release(msg); // (2)或者,你可以在这里调用 in.release()。
}
}
再次运行 telnet 命令,会看到服务端打印出了他所接收到的消息。
总结
现在稳定推荐使用的主流版本还是 Netty4,Netty5 中使用了 ForkJoinPool,增加了代码的复杂度,但是对性能的改善却不明显,所以这个版本不推荐使用,官网也没有提供下载链接。
Netty 入门门槛相对较高,是因为这方面的资料较少,并不是因为它有多难,大家其实都可以像搞透 Spring 一样搞透 Netty。
在学习之前,建议先理解透整个框架原理结构,运行过程,可以少走很多弯路。