本文为个人学习笔记整理,仅供交流参考,非专业教学资料,内容请自行甄别
文章目录
概述
Netty核心组件:
- Bootstrap:Netty的启动入口类,分为客户端(ClientBootstrap)和服务端(ServerBootstrap)两种类型
- Channel:网络通信的核心抽象层,封装了底层Socket操作,提供统一的网络I/O接口
- EventLoop:采用Reactor线程模型,一个EventLoop可绑定多个Channel,实现多路复用
- 事件模型:
- 入站事件(Inbound):从网络层到业务层的处理流程(如数据读取)
- 出站事件(Outbound):从业务层到网络层的处理流程(如数据写入)
- ChannelHandler与Pipeline:
- ChannelHandler:处理网络与业务间的各种操作(如编解码、压缩等)
- Pipeline:采用责任链模式组织Handler,负责事件的流转处理
- ChannelFuture:为异步操作提供结果通知机制,是JUC Future的增强实现
一、EventLoopGroup&EventLoop
EventLoopGroup用于分配当前的channel由哪一个EventLoop进行管理当某个Channel分配给某一个EventLoop以后,在Channel的整个生命周期内,都是由该EventLoop处理。一个EventLoop可能对应多个Channel,这是多路复用思想在Netty中的体现。
EventLoopGroup负责去管理EventLoop,当有一个Channel诞生时,由EventLoopGroup负责将EventLoop和Channel绑定。在Channel的整个生命周期内,都由该EventLoop进行管理。
图片来源:图灵学院
而其他线程想要执行Channel的某些动作时,都要将Channel交给与之绑定的EventLoop本身进行相关的执行。以服务端的绑定操作为例,这里的bind
方法是由主线程去执行的,但是底层绑定操作应该由NioServerSocketChannel
执行。
在真正执行之前会进行判断:
- 如果当前线程是EventLoop中记录的线程,就由当前线程去执行
- 否则将其包装成Runnable任务,交给EventLoop 并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。
在上述的案例中,bind操作应该交给NioServerSocketChannel
对应的EventLoop去执行。
二、Channel&ChanneHandler
Channel可以理解成是对于Socket的一种封装,每次通信都会产生一个Channel。而Channel中还维护了一个pipeline。pipeline可以看做是一个双向链表,记录了该Channel的所有入站和出站事件。即:每一个channel都有自己的pipeline,负责将handler进行组装成责任链。(双向链表)
责任链中的每个元素称为ChanneHandler
,需要实现ChannelInboundHandler
或ChannelInboundHandler
。前者是入站处理器,处理从网络到业务部分的事件。后者是出站处理器,处理从业务部分到网络的事件。
如果不想实现接口中的所有方法,可以去继承对应的ChannelInboundHandlerAdapter
或ChannelOutboundHandlerAdapter
,这两个抽象类运用了接口适配器模式
,对接口中的方法进行了默认的实现,用户在使用时只需要自己选择需要重写的方法即可。
所有的入站和出站事件,在ChannelHandlerMask
中都有常量定义:
三、ChannelPipeline&ChannelHandlerContext
ChannelPipeline
的底层是一种类似于双向链表的实现,入站和出站事件,在责任链中的顺序是没有要求的,但是入站和入站事件之间,出站和出站事件之间,对于顺序有严格的要求,关系到事件的流转。
ChannelHandlerContext
是ChannelPipeline
中的上下文。每当有ChannelHandler
添加到ChannelPipeline
中时,都会创建ChannelHandlerContext
,而这个上下文,起到的作用和JDK的LinkedList的Node节点是相似的,用于组织责任链:
在写数据时,我们既可以通过ctx
去写,也可以通过channel().write();
去写,这两者有什么区别?
前者的写,只会写到与之相邻的下一个ChannelHandler
上,而后者的写,会经过该ChannelPipeline
后续所有的ChannelHandler
。
四、共享模式
在一般的情况下,不同Channel之间的Handler,是线程隔离的(在Handler没有static成员变量的场景下),每个Channel都有自己的pipeline。即使是某个Handler出现在了两个Channel中,实际上也是不同的实例。
图片来源:图灵学院
如果要求某个Handler中的属性共享,例如统计全局的信息,可以用到@ChannelHandler.Sharable
注解:
这样该实例在不同的Channel之间就可以共享。
五、Buffer的释放
在NIO编程中,需要将数据读取到缓冲区,或者写入到缓冲区,而在Netty中依旧存在Buffer的概念:
如果Buffer得不到及时的释放,可能会造成OOM的问题,在NIO中是进行的手动释放,Netty会自动进行Buffer的释放,因为Netty会在pipeline中安装两个Handle:
这两个Handle位于责任链的头部和尾部,其中会进行buffer的释放。但是如果在入站或出站的操作中,没有将数据向前(出站)/向后(入栈)传递,而是在某个handler中进行了拦截,导致数据没有发送到责任链的头部和尾部,则需要调用ReferenceCountUtil.release
方法手动地去进行资源的释放。
writeAndFlush
的底层就是这样实现的。
同时在针对入栈的场景,Netty提供了SimpleChannelInboundHandler
,在读取完成数据后,自动进行清理工作:
附录:Netty编程模型案例
服务端实现流程:
- 创建EventLoopGroup实例用于处理事件线程。
- 配置ServerBootstrap:
- 绑定EventLoopGroup
- 设置channel类型(NioServerSocketChannel)
- 配置监听IP和端口
- 添加handler处理链
- 绑定服务器并获取ChannelFuture
- 执行关闭操作
public class NettyServerDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("Netty服务器启动");
//产生EventLoopGroup的实例,需要线程去处理事件。
EventLoopGroup group = new NioEventLoopGroup();
try {
//服务端ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
//将EventLoopGroup绑定到ServerBootstrap上
serverBootstrap.group(group)
// 指定使用何种channel(NioServerSocketChannel)
.channel(NioServerSocketChannel.class)
//指定端口
.localAddress(new InetSocketAddress(8080))
//指定要经历哪些handler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyServerHandler());
}
});
//服务端ServerBootstrap绑定到服务器, 通过sync确保服务器在绑定端口后才继续执行后续代码,避免在未准备好时就处理连接。
// 拿到future。
ChannelFuture f = serverBootstrap.bind().sync();
//阻塞当前线程,直到服务端的ServerChannel被关闭
f.channel().closeFuture().sync();
} finally {
//关闭线程组
group.shutdownGracefully().sync();
}
}
}
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取客户端的信息
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//将消息转换为buffer
ByteBuf buf = (ByteBuf) msg;
System.out.println("接收到的客户端的消息" + buf.toString(StandardCharsets.UTF_8));
//回写到客户端
ctx.writeAndFlush(buf);
ctx.close();
}
/**
* 处理异常并关闭连接
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
客户端实现流程:
- 创建EventLoopGroup实例用于处理事件线程。
- 配置Bootstrap:
- 绑定EventLoopGroup1
- 设置channel类型(NioSocketChannel)
- 配置目标服务器IP和端口
- 添加handler处理链
- 连接服务器并获取ChannelFuture
- 执行关闭操作
public class NettyClientDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("Netty客户端启动");
//产生EventLoopGroup的实例,需要线程去处理事件。
EventLoopGroup group = new NioEventLoopGroup();
try {
//客户端Bootstrap
Bootstrap bootstrap = new Bootstrap();
//将EventLoopGroup的实例绑定到客户端
bootstrap.group(group)
// 指定使用何种channel(NioSocketChannel)
.channel(NioSocketChannel.class)
//与服务端建立连接
.remoteAddress(new InetSocketAddress("127.0.0.1",8080))
//指定要经历哪些handler
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler());
}
});
//连接服务器
ChannelFuture future = bootstrap.connect().sync();
//阻塞当前线程,直到客户端的Channel被关闭
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
public class NettyClientHandler extends SimpleChannelInboundHandler {
/**
* 读取服务端发送的数据
* @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
* belongs to
* @param msg the message to handle
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("客户端接收到服务端的" + msg.toString());
ctx.close();
}
/**
* 建立连接后,客户端向服务端发送数据
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer(
"向服务端发送数据", CharsetUtil.UTF_8));
}
}