1 回顾一下上一篇文章讲的Netty执行流程
详细可以参考:【Netty】模型篇一:Netty 线程模型架构 & 工作原理 解读
Netty 线程模型详细说明
- Netty抽象出两组 线程池:BossGroup 和 WorkerGroup
- BossGroup 专门负责接收客户端的连接
- WorkerGroup 专门负责网络的读写
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,NioEventLoopGroup 相当于一个
事件循环组
,这个组中含有多个事件循环
,每一个事件循环是 NioEventLoop - NioEventLoop 表示一个不断循环的执行处理任务的线程,
每个NioEventLoop 都有一个selector
, 用于监听绑定在其上的socket的网络通讯。 - NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
- 每个Boss Group 中的 NioEventLoop 循环执行的步骤:
- 1)轮询accept 事件
- 2)处理accept 事件,与client建立连接 , 生成
NioScocketChannel
,并将其注册 Worker Group 上的某个 NIOEventLoop 上的 selector
- 3)处理任务队列的任务,即 runAllTasks
- 每个 Worker Group 中的 NIOEventLoop 循环执行的步骤:
- 1)轮询 read/write 事件
- 2)处理 I/O 事件, 即 read/write 事件,在对应的 NioScocketChannel 上处理
- 3)处理任务队列的任务 , 即 runAllTasks
- 每个Worker NIOEventLoop 处理业务时,会使用 pipeline(管道)。pipline中包含了 channel,即通过pipline可以获取到对应的 channel,并且pipline维护了很多的 handler(处理器)来对我们的数据进行一系列的处理。
- handler(处理器) 有Netty内置的,我们也可以自己定义。
2 Netty 快速入门案例
下面我们通过一个案例来进一步深入理解上一节讲解的Netty线程模型的工作原理,并对其中涉及到的Netty核心组件进行详细分析
案例要求
- Netty 服务器在 6668 端口监听,浏览器发出请求
http://localhost:6668/
; - 服务器可以回复消息给客户端 "Hello! 我是服务器 5 ",并对特定请求资源进行过滤;
- 目的:Netty 可以做Http服务开发,并且理解Handler实例和客户端及其请求的关系。
2.1 服务端代码
2.1.1 代码实现思路详解
步骤一:创建两个线程组 BossGroup 和 WorkerGroup,它们的类型都是 NioEventLoopGroup
- BossGroup 只处理连接请求;
- WorkerGroup处理客户端业务。
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
步骤二:创建服务器端启动对象 ServerBootstrap ,并进行参数配置:
- 设置 BossGroup 和 WorkerGroup;
- 设置使用使用 NioSocketChannel 作为服务器的通道实现。
- 设置保持活动连接;
- 给 WorkerGroup 的 EventLoop 对应的 Pipline 设置 Handler,Handler可以是Netty内置的,也可以自己实现。
// 2.创建服务器端启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
// 使用链式编程进行设置
bootstrap.group(bossGroup,workerGroup)// 设置两个线程组
.channel(NioServerSocketChannel.class)// 使用 NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128)// 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true)// 设置保持活动连接
// 给 WorkerGroup 的 EventLoop 对应的 管道 设置处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
// 向 pipline 设置 处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 通过 channel 可以拿到 pipline,也可以通过 pipline 拿到 channel
socketChannel.pipeline()
.addLast(new NettyServerHandler());// 向 pipline 添加 处理器
}
});
步骤三:实现Handler对象
- 如果想自定义Handler,则需要继承Netty规定好的某个
HandlerAdapter
- 这里先重写三个常用方法:
- channelRead():读取客户端的消息
- channelReadComplete():消息读取完毕,向客户端回复消息
- exceptionCaught():处理异常,一般需要关闭通道
/*
说明:
1.自定义Handler,需要继承Netty规定好的某个HandlerAdapter
2.这时我们自定义的Handler,才能称为一个Handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/*
作用:读取客户端的消息
ChannelHandlerContext ctx:上下文对象,含有 管道pipline、通道channel、地址等
Object msg:就是客户端发送的数据 默认 Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx = "+ctx);
// 将 msg 转成 ByteBuffer
// ByteBuf 是 Netty 提供的,不是 NIO 中的
ByteBuf buffer = (ByteBuf) msg;
System.out.println("客户端发送的消息是:"+buffer.toString(CharsetUtil.UTF_8));
System.out.println("客户端的地址是:"+ctx.channel().remoteAddress());
}
// 数据读取完毕,向客户端回复消息
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// writeAndFlush 是 write + flush
// 将数据写入到缓存,并刷新
// 一般我们会对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端...",CharsetUtil.UTF_8));
}
// 处理异常,一般需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
步骤四:绑定端口并且同步,启动服务器,生成并返回一个 ChannelFuture 对象
ChannelFuture cf = bootstrap.bind(6668).sync();
步骤五:设置对关闭通道事件进行监听
// 对关闭通道事件进行监听
channelFuture.channel().closeFuture().sync();
2.1.2 最终代码
public class NettyServer {
public static void main(String[] args) {
// 1.创建 BossGroup 和 WorkerGroup
/*
说明:
1.创建两个线程组 BossGroup 和 WorkerGroup
2.bossGroup 只处理连接请求,workerGroup 处理客户端业务
3.两个都是无限循环
4.BossGroup 和 WorkerGroup 还有的子线程(NioEventLoop)的个数
默认是 cpu核数 * 2
*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 2.创建服务器端启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
// 使用链式编程进行设置
bootstrap.group(bossGroup,workerGroup)// 设置两个线程组
.channel(NioServerSocketChannel.class)// 使用 NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128)// 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true)// 设置保持活动连接
// 给 WorkerGroup 的 EventLoop 对应的 管道 设置处理器
.childHandler(new ChannelInitializer<SocketChannel>() {// 创建一个通道测试对象(匿名对象)
// 向 pipline 设置 处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 通过 channel 可以拿到 pipline,也可以通过 pipline 拿到 channel
socketChannel.pipeline()
.addLast(new NettyServerHandler());// 向 pipline 添加 处理器
}
});
System.out.println("服务器就绪...");
// 3.绑定端口并且同步,生成一个 ChannelFuture 对象
// 这里服务器就已经启动服务器了
ChannelFuture cf = bootstrap.bind(6668).sync();
// 4.对关闭通道事件进行监听
cf.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
我们可以看到上面为 pipline 设置了一个handler对象,因此我们来实现一下 handler对象
/*
说明:
1.自定义Handler,需要继承Netty规定好的某个HandlerAdapter
2.这时我们自定义的Handler,才能称为一个Handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/*
作用:读取客户端的消息
ChannelHandlerContext ctx:上下文对象,含有 管道pipline、通道channel、地址等
Object msg:就是客户端发送的数据 默认 Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx = "+ctx);
// 将 msg 转成 ByteBuffer
// ByteBuf 是 Netty 提供的,不是 NIO 中的
ByteBuf buffer = (ByteBuf) msg;
System.out.println("客户端发送的消息是:"+buffer.toString(CharsetUtil.UTF_8));
System.out.println("客户端的地址是:"+ctx.channel().remoteAddress());
}
// 数据读取完毕,向客户端回复消息
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// writeAndFlush 是 write + flush
// 将数据写入到缓存,并刷新
// 一般我们会对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端...",CharsetUtil.UTF_8));
}
// 处理异常,一般需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
2.2 客户端代码
2.2.1 代码实现思路详解
步骤一:创建一个线程组 group ,它的类型是 NioEventLoopGroup。
// 客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
步骤二:创建客户端启动对象 Bootstrap ,并进行参数配置:
- 设置 group;
- 设置使用使用 NioSocketChannel 作为客户端通道的实现类;
- 给 WorkerGroup 的 EventLoop 对应的 Pipline 设置 Handler,Handler可以是Netty内置的,也可以自己实现。
Bootstrap bootstrap = new Bootstrap();
// 设置相关参数,链式编程
bootstrap.group(group)// 设置线程组
.channel(NioSocketChannel.class)// 设置客户端通道的实现类
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler());// 加自己的处理器
}
});
步骤三:实现Handler对象
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
// 当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client ctx = "+ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,服务端...", CharsetUtil.UTF_8));
}
// 当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
System.out.println("服务器回复的消息:"+buffer.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址:"+ctx.channel().remoteAddress());
}
// 发生异常时处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
步骤四:绑定 IP 和 端口 并且同步,启动客户端,生成并返回一个 ChannelFuture 对象
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
步骤五:设置对关闭通道事件进行监听
// 对关闭通道事件进行监听
channelFuture.channel().closeFuture().sync();
2.2.2 最终代码
public class NettyClient {
public static void main(String[] args) {
// 客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建客户端启动对象
// 注意:客户端使用的是 Bootstrap 而不是 ServerBootStrap
Bootstrap bootstrap = new Bootstrap();
// 设置相关参数,链式编程
bootstrap.group(group)// 设置线程组
.channel(NioSocketChannel.class)// 设置客户端通道的实现类
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler());// 加自己的处理器
}
});
System.out.println("客户端就绪...");
// 启动客户端连接服务端
// 关于 ChannelFuture 涉及到 netty 的异步模式
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
// 对关闭通道事件进行监听
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
这里也需要为 pipline 设置了一个handler对象,因此我们来实现一下 handler对象
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
// 当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client ctx = "+ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,服务端...", CharsetUtil.UTF_8));
}
// 当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
System.out.println("服务器回复的消息:"+buffer.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址:"+ctx.channel().remoteAddress());
}
// 发生异常时处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
3 代码分析
3.1 代码总结
由于客户端和服务端代码类似,所以我们重点分析服务端的代码。
在服务端创建了两个 线程池:BossGroup 和 WorkerGroup ,BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写。
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,NioEventLoopGroup 相当于一个
事件循环组
,这个组中含有多个事件循环 NioEventLoop
,每一个事件循环是 NioEventLoop - NioEventLoop 表示一个不断循环的执行处理任务的线程,
每个NioEventLoop 都有一个selector
, 用于监听绑定在其上的socket的网络通讯。
每个Boss Group 中的 NioEventLoop 循环执行的步骤:
- 1)轮询accept 事件
- 2)处理accept 事件,与client建立连接 , 生成
NioScocketChannel
,并将其注册 Worker Group 上的某个 NIOEventLoop 上的 selector
- 3)处理任务队列的任务,即 runAllTasks
每个 Worker Group 中的 NIOEventLoop 循环执行的步骤:
- 1)轮询 read/write 事件
- 2)处理 I/O 事件, 即 read/write 事件,在对应的 NioScocketChannel 上处理
- 3)处理任务队列的任务 , 即 runAllTasks
每个Worker NIOEventLoop 处理业务时,会使用 pipeline(管道)。pipline中包含了 channel,即通过pipline可以获取到对应的 channel,并且pipline维护了很多的 handler(处理器)来对我们的数据进行一系列的处理。handler(处理器) 可以是Netty内置的,我们也可以自己定义。
3.2 涉及到的Netty组件总结
Netty核心组件详解我们另写一篇文章分析,这里只简要说明用到了哪些组件。
- 在创建线程组的时候用到了
NioEventLoopGroup类
; - 在创建服务端启动对象时用到了
ServerBootstrap 类
;在创建客户端启动对象时用到了Bootstrap 类
; - 在为启动对象配置参数时我们用到了
NioServerSocketChannel类
、ChannelOption类
、ChannelInitializer类
;ChannelInitializer类继承了ChannelInboundHandlerAdapter类
; - 在自定义Handler时,我们继承了
ChannelInboundHandlerAdapter
类;在重写该类的方法中涉及到了ChannelHandlerContext 类
、ByteBuf 类
、Unpooled类
; - 在绑定端口并且同步,启动服务器,生成并返回一个
ChannelFuture类
的对象。
对于这些组件的用法,大家可以参考下一篇文章:【Netty】模型篇三:Netty核心组件讲解