1. Netty认识
Netty 是由 JBOSS 提供一个异步的、 基于事件驱动的网络编程框架
。
Netty 可以帮助你快速、 简单的开发出一 个网络应用, 相当于简化和流程化了 NIO 的开发过程。 作为当前最流行的 NIO 框架, Netty 在互联网领域、 大数据分布式计算领域、 游戏行业、 通信行业等获得了广泛的应用, 知名的 Elasticsearch 、 Dubbo 框架内部都采用了 Netty。
1.1 为什么使用Netty
NIO缺点
- NIO 的类库和 API 繁杂,使用麻烦。你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等.
- 可靠性不强,开发工作量和难度都非常大
- NIO 的 Bug。例如 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%
Netty优点
- 对各种传输协议提供统一的 API
- 高度可定制的线程模型 —— 单线程、一个或多个线程池
- 更好的吞吐量,更低的等待延迟
- 更少的资源消耗
- 最小化不必要的内存拷贝
2. 线程模型
2.1 单线程模型
2.2 线程池模型
2.3 Netty 模型
Netty 抽象出两组线程池, BossGroup 专门负责接收客 户端连接,WorkerGroup 专门负责网络读写操作。NioEventLoop 表示一个不断循环执行处理任务的线程, 每个 NioEventLoop 都有一个 selector, 用于监听绑定在其上的 socket 网络通道。 NioEventLoop 内部采用串行化设计, 从消息的读取 > 解码 > 处理 > 编码 > 发送
, 始终由 IO 线 程 NioEventLoop 负责。
3. Netty核心组件
3.1 ChannelHandler 及其实现类
ChannelHandler 接口定义了许多事件处理的方法, 我们可以通过重写这些方法去实现具体的业务逻辑
public void channelActive(ChannelHandlerContext ctx) // 通道就绪事件
public void channelRead(ChannelHandlerContext ctx, Object msg) // 通道读取数据事件
public void channelReadComplete(ChannelHandlerContext ctx) // 数据读取完毕事件
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) //通道发生异常事件
3.2 ChannelPipeline
ChannelPipeline 是一个 Handler 的集合
, 它负责处理和拦截 inbound 或者 outbound 的事件和操作, 相当于一个贯穿 Netty 的链。
ChannelPipeline addFirst(ChannelHandler... handlers) // 把一个业务处理类(handler) 添加到链中的第一 个位置
ChannelPipeline addLast(ChannelHandler... handlers) // 把一个业务处理类(handler) 添加到链中的最后 一个位置
3.3 ChannelHandlerContext
这是事件处理器上下文对象
,Pipeline链中的实际处理节点。每个处理节点ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的pipeline和Channel的信息,方便对ChannelHandler进行调用。
常用方法如下所示
ChannelFuture close() // 关闭通道
ChannelOutboundInvoker flush() // 刷新
ChannelFuture writeAndFlush(Object msg)//将数据写到 ChannelPipeline 中,当前ChannelHandler的下一个 ChannelHandler 开始处理(出站)
3.4 ChannelFuture
表示 Channel 中异步 I/O 操作的结果
, 在 Netty 中所有的 I/O 操作都是异步的, I/O 的调用会直接返回, 调用者并不能立刻获得结果, 但是可以通过 ChannelFuture 来获取 I/O 操作 的处理状态。
常用方法如下所示:
Channel channel() // 返回当前正在进行 IO 操作的通道
ChannelFuture sync() // 等待异步操作执行完毕
3.5 EventLoopGroup 和其实现类 NioEventLoopGroup
EventLoopGroup 是一组 EventLoop 的抽象
, Netty 为了更好的利用多核 CPU 资源, 一般会有多个 EventLoop 同时工作, 每个 EventLoop 维护着一个 Selector 实例。 EventLoopGroup 提供 next 接口, 可以从组里面按照一定规则获取其中一个 EventLoop 来处理任务。 在 Netty 服务器端编程中, 我们一般都需要提供两个EventLoopGroup, 例如: BossEventLoopGroup 和 WorkerEventLoopGroup
public NioEventLoopGroup() // 构造方法
public Future<?> shutdownGracefully() // 断开连接, 关闭线程
3.6 ServerBootstrap 和 Bootstrap
ServerBootstrap 是 Netty 中的服务器端启动助手,通过它可以完成服务器端的各种配置; Bootstrap 是 Netty 中的客户端启动助手, 通过它可以完成客户端的各种配置。
常用方法如下 所示:
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) // 该方法用于 服务器端,用来设置两个EventLoop
public B group(EventLoopGroup group) // 该方法用于客户端,用来设置一个 EventLoop
public B channel(Class<? extends C> channelClass) // 该方法用来设置一个服务器端的通道实现
public <T> B option(ChannelOption<T> option, T value) // 用来给 ServerChannel 添加配置
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) // 用来给接收到的通道添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler) // 该方法用来设置业务处理类(自定义的handler)
public ChannelFuture bind(int inetPort) // 该方法用于服务器端, 用来设置占用的端口号
public ChannelFuture connect(String inetHost, int inetPort) // 该方法用于客户端, 用来连接服务器端
4. Netty版案例实现
需求:使用 netty 客户端给服务端发送数据,服务端接收消息打印
- 引入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.52.Final</version>
</dependency>
- 服务端
package com.study.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
// 接收客户端请求,打印在控制台
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//1.创建2个线程池对象
//bossGroup 负责接收用户连接
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
//workGroup 负责处理用户的io读写操作
NioEventLoopGroup workGroup = new NioEventLoopGroup();
//2.创建启动引导类
ServerBootstrap serverBootstrap = new ServerBootstrap();
//3.设置启动引导类
//添加到组中,两个线程池,第一个位置的线程池就负责接收,第二个参数就负责读写
serverBootstrap.group(bossGroup,workGroup)
//给我们当前设置一个通道类型
.channel(NioServerSocketChannel.class)
//绑定一个初始化监听
.childHandler(new ChannelInitializer<NioSocketChannel>() {
//事件监听Channel通道
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
//获取pipeLine
ChannelPipeline pipeline = nioSocketChannel.pipeline();
//绑定编码
pipeline.addFirst(new StringEncoder());
pipeline.addLast(new StringDecoder());
//绑定我们的业务逻辑
pipeline.addLast(new SimpleChannelInboundHandler<String>() {
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
//获取入栈信息,打印客户端传递的数据
System.out.println(msg);
}
});
}
});
//4.启动引导类绑定端口
ChannelFuture future = serverBootstrap.bind(9999).sync();
//5.关闭通道
future.channel().closeFuture().sync();
}
}
在编写 Netty 程序时,一开始都会生成 NioEventLoopGroup 的两个实例,分别是 bossGroup 和 workerGroup,也可以称为 parentGroup 和 childGroup,为什么创建这两个实例,作用是什么?
-
可以这么理解,bossGroup 和workerGroup 是两个线程池, 它们默认线程数为 CPU 核心数乘以 2,bossGroup 用于接收客户端传过来的请求,接收到请求后将后续操作交由 workerGroup 处理。
-
接下来我们生成了一个服务启动辅助类的实例 bootstrap,boostrap 用来为 Netty 程序的启动组装配置一些必须要组件,例如上面的创建的两个线程组。
-
channel 方法用于指定服务器端监听套接字通道NioServerSocketChannel,其内部管理了一个 Java NIO 中的ServerSocketChannel实例。
-
channelHandler 方法用于设置业务职责链,责任链是我们下面要编写的,责任链具体是什么,它其实就是由一个个的 ChannelHandler 串联而成,形成的链式结构。正是这一个个的 ChannelHandler 帮我们完成了要处理的事情。
-
ChannelInitializer 继承 ChannelInboundHandlerAdapter,用于初始化 Channel 的 ChannelPipeline。通过initChannel 方法参数 sc 得到 ChannelPipeline 的一个实例。
-
当一个新的连接被接受时, 一个新的 Channel 将被创建,同时它会被自动地分配到它专属的 ChannelPipeline通过 addLast 方法将一个一个的 ChannelHandler 添加到责任链上并给它们取个名称(不取也可以,Netty 会给它个默认名称),这样就形成了链式结构。在请求进来或者响应出去时都会经过链上这些 ChannelHandler 的处理。
-
最后再向链上加入我们自定义的 ChannelHandler 组件,处理自定义的业务逻辑
-
接着我们调用了 bootstrap 的 bind 方法将服务绑定到 8080 端口上,bind 方法内部会执行端口绑定等一系列操,使得前面的配置都各就各位各司其职,sync 方法用于阻塞当前 Thread,一直到端口绑定操作完成。最后是应用程序将会阻塞等待直到服务器的 Channel 关闭。
- 客户端
package com.study.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
//客户端给服务器发送数据
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//1.创建连接池对象
NioEventLoopGroup group = new NioEventLoopGroup();
//2.创建客户端的启动引导类 BootStrap
Bootstrap bootstrap = new Bootstrap();
//3.配置启动引导类
bootstrap.group(group)
//设置通道为Nio
.channel(NioSocketChannel.class)
//设置Channel初始化监听
.handler(new ChannelInitializer<Channel>() {
//当前该方法监听channel是否初始化
protected void initChannel(Channel channel) throws Exception {
//设置编码
channel.pipeline().addLast(new StringEncoder());
}
});
//4.使用启动引导类连接服务器 , 获取一个channel
Channel channel = bootstrap.connect("127.0.0.1", 9999).channel();
//5.循环写数据给服务器
while (true) {
//给服务器写数据
channel.writeAndFlush("hello server .. this is client ...");
Thread.sleep(2000);
}
}
}
使用Netty之后,一方面Netty对NIO封装得如此完美,写出来的代码非常优雅,另外一方面,使用Netty之后,网络通信这块的性能问题几乎不用操心