Netty 是一个利用Java的高级网络能力,隐藏其(Java API)背后的复杂性而提供一个易于使用的 NIO 客户端/服务端框架。
它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
用官方的总结就是:Netty 成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。
为什么要用Netty
Netty作为一款优秀的网络框架,自然有令人折服的特点:
设计:针对多种传输类型的同一接口。简单但更强大的线程模型。真正的无连接的数据报套接字支持。链接逻辑复用。
性能:Netty的高性能是它被广泛使用的一个重要的原因,我们可能都认为Java不太适合 编写游戏服务端程序,但Netty的到来无疑是降低了怀疑的声音。
较原生Java API有更好的吞吐量,较低的延时。资源消耗更少(共享池和重用)。减少内存拷贝。
健壮性:原生NIO的客户端/服务端程序编写较为麻烦,如果某个地方处理的不好,可能会 导致一些意料之外的异常,如内存溢出,死循环等等,而Netty则为我们简化了原生API 的使用,这使得我们编写出来的程序不那么容易出错。
社区:Netty快速发展的一个重要的原因就是它的社区非常活跃,这也使得采用它的开发者越来越多。
Netty的简单使用
左边是服务端代码,右边是客户端代码。
上面的代码基本就是模板代码,每次使用都是这一个套路,唯一需要我们开发的部分是 handler(…) 和 childHandler(…) 方法中指定的各个 handler,如 EchoServerHandler 和 EchoClientHandler,当然 Netty 源码也给我们提供了很多的 handler,比如上面的 LoggingHandler,它就是 Netty 源码中为我们提供的,需要的时候直接拿过来用就好了。
我们先来看一下上述代码中涉及到的一些内容:
ServerBootstrap 类用于创建服务端实例,Bootstrap 用于创建客户端实例。
两个 EventLoopGroup:bossGroup 和 workerGroup,它们涉及的是 Netty 的线程模型,可以看到服务端有两个 group,而客户端只有一个,它们就是 Netty 中的线程池。
Netty 中的 Channel,没有直接使用 Java 原生的 ServerSocketChannel 和 SocketChannel,而是包装了 NioServerSocketChannel 和 NioSocketChannel 与之对应。
当然,也有对其他协议的支持,如支持 UDP 协议的 NioDatagramChannel,本文只关心 TCP 相关的。
左边 handler(…) 方法指定了一个 handler(LoggingHandler),这个 handler 是给服务端收到新的请求的时候处理用的。右边 handler(...) 方法指定了客户端处理请求过程中需要使用的 handlers。
如果你想在 EchoServer 中也指定多个 handler,也可以像右边的 EchoClient 一样使用 ChannelInitializer
左边 childHandler(…) 指定了 childHandler,这边的 handlers 是给新创建的连接用的,我们知道服务端 ServerSocketChannel 在 accept 一个连接以后,需要创建 SocketChannel 的实例,childHandler(…) 中设置的 handler 就是用于处理新创建的 SocketChannel 的,而不是用来处理 ServerSocketChannel 实例的。
pipeline:handler 可以指定多个(需要上面的 ChannelInitializer 类辅助),它们会组成了一个 pipeline,它们其实就类似拦截器的概念,现在只要记住一点,每个 NioSocketChannel 或 NioServerSocketChannel 实例内部都会有一个 pipeline 实例。pipeline 中还涉及到 handler 的执行顺序。
ChannelFuture:这个涉及到 Netty 中的异步编程,和 JDK 中的 Future 接口类似。
Netty核心组件
Bytebuf(字节容器)
网络通信最终都是通过字节流进行传输的。ByteBuf 就是 Netty 提供的一个字节容器,其内部是一个字节数组。当我们通过 Netty 传输数据的时候,就是通过 ByteBuf 进行的。
我们可以将 ByteBuf 看作是 Netty 对 Java NIO 提供了 ByteBuffer 字节容器的封装和抽象。
有很多小伙伴可能就要问了 :为什么不直接使用 Java NIO 提供的 ByteBuffer 呢?
因为 ByteBuffer 这个类使用起来过于复杂和繁琐。
Bootstrap 和 ServerBootstrap(启动引导类)
Bootstrap 是客户端的启动引导类/辅助类,具体使用方法如下:
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动引导/辅助类:Bootstrap
Bootstrap b = new Bootstrap();
//指定线程模型
b.group(group).
......
// 尝试建立连接
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
// 优雅关闭相关线程组资源
group.shutdownGracefully();
}
ServerBootstrap 客户端的启动引导类/辅助类,具体使用方法如下:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup).
......
// 6.绑定端口
ChannelFuture f = b.bind(port).sync();
// 等待连接关闭
f.channel().closeFuture().sync();
} finally {
//7.优雅关闭相关线程组资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
从上面的示例中,我们可以看出:
Bootstrap 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。
ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。
Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的 IO 处理。
Channel(网络操作抽象类)
Channel 接口是 Netty 对网络操作抽象类。通过 Channel 我们可以进行 I/O 操作。
一旦客户端成功连接服务端,就会新建一个 Channel 同该用户端进行绑定,示例代码如下:
// 通过 Bootstrap 的 connect 方法连接到服务端
public Channel doConnect(InetSocketAddress inetSocketAddress) {
CompletableFuture<Channel> completableFuture = new CompletableFuture<>();
bootstrap.connect(inetSocketAddress).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
completableFuture.complete(future.channel());
} else {
throw new IllegalStateException();
}
});
return completableFuture.get();
}
比较常用的Channel接口实现类是 :
NioServerSocketChannel(服务端)
NioSocketChannel(客户端)
这两个 Channel 可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上。
EventLoop(事件循环)
EventLoop 介绍
这么说吧!EventLoop(事件循环)接口可以说是 Netty 中最核心的概念了!
《Netty 实战》这本书是这样介绍它的:
EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
是不是很难理解?说实话,我学习 Netty 的时候看到这句话是没太能理解的。
说白了,EventLoop 的主要作用实际就是责监听网络事件并调用事件处理器进行相关 I/O 操作(读写)的处理。
Channel 和 EventLoop 的关系
那 Channel 和 EventLoop 直接有啥联系呢?
Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 的 I/O 操作,两者配合进行 I/O 操作。
EventloopGroup 和 EventLoop 的关系
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),它管理着所有的 EventLoop 的生命周期。
并且,EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。
下图是 Netty NIO 模型对应的 EventLoop 模型。通过这个图应该可以将EventloopGroup、EventLoop、 Channel三者联系起来。
ChannelHandler(消息处理器) 和 ChannelPipeline(ChannelHandler 对象链表)
下面这段代码使用过 Netty 的小伙伴应该不会陌生,我们指定了序列化编解码器以及自定义的 ChannelHandler 处理消息。
b.group(eventLoopGroup)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));
ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));
ch.pipeline().addLast(new KryoClientHandler());
}
});
ChannelHandler 是消息的具体处理器,主要负责处理客户端/服务端接收和发送的数据。
当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。一个Channel包含一个 ChannelPipeline。ChannelPipeline 为 ChannelHandler 的链,一个 pipeline 上可以有多个 ChannelHandler。
我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler (一个数据或者事件可能会被多个 Handler 处理) 。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。
当 ChannelHandler 被添加到的 ChannelPipeline 它得到一个 ChannelHandlerContext,它代表一个 ChannelHandler 和 ChannelPipeline 之间的“绑定”。ChannelPipeline 通过 ChannelHandlerContext来间接管理 ChannelHandler 。
ChannelFuture(操作执行结果)
public interface ChannelFuture extends Future<Void> {
Channel channel();
ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1);
......
ChannelFuture sync() throws InterruptedException;
}
Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。
Netty实际上是不支持异步io的,真正的异步io需要底层操作系统的支持,异步是说数据准备好之后由系统通知应用程序你可以来操作数据了,而netty所谓的异步是另起一个用户线程等待数据就绪并通过回调处理,并不是真正意义上的异步io
因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
ChannelFuture f = b.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else {
System.err.println("连接失败!");
}
}).sync();
并且,你还可以通过ChannelFuture 的 channel() 方法获取连接相关联的Channel 。
Channel channel = f.channel();
另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作编程同步的。
//bind()是异步的,但是,你可以通过 `sync()`方法将其变为同步。
ChannelFuture f = b.bind(port).sync();
本文参考:https://www.javadoop.com/post/netty-part-1
本文参考:https://github.com/Snailclimb/netty-practical-tutorial