java I/O体系总结(五)netty架构浅析
简介
netty是使用java编写的高性能IO框架,旨在为高并发场景提供支持。netty可提供多种IO模型的支持,如OIO,NIO等。一般来说,非阻塞IO更适合于大规模高并发场景,我们使用netty主要也因为其封装了原生NIO,规避了其中复杂易出错的细节,更加易用、通用。
从示例讲起
netty既然是以java NIO为基础构建的(当然添加了大量特性),那就不能不了解java NIO的处理方式。NIO实现非阻塞的关键在于Selector(选择器)以及通道。下面先复习一下nio的示例,然后再对比netty。
java nio 示例
public void start() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
Selector selector = Selector.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
// ①将服务器的channel注册到选择器
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
try {
//阻塞,至少一个连接到来时才会继续
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
// 连接进入
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
// ② 服务器接受连接,创建客户端的channel,然后注册到选择器(Selector)
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// ③ 客户端的channel
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int count = sc.read(byteBuffer);
if (count < 0) {
key.cancel();
sc.close();
} else {
byteBuffer.flip(); //切换到读模式
String msg = Charset.forName("UTF-8").decode(byteBuffer).toString();
System.out.println("received from: " + msg);
sc.write(ByteBuffer.wrap(msg.getBytes(CharsetUtil.UTF_8)));
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
示例很简单,就是该服务器接受来自客户端的连接,并打印客户端的信息。解释如下:
- selector为选择器,即可以把要关注的事件注册到这里来。待该事件发生时,可给与通知。如将表示与服务器相连的serverSocketChannel注册,等有新连接过来后(accept事件),会通知该channel。
- ①处即为向选择器注册服务器channel及需关注的accept事件。
- ②处为向选择器注册接收的客户端channel,及关注的read事件
- ③处为客户端channel的read事件,处理read事件
- 从上面我们看出,selector注册了两种channel。一种是服务器channel,一种是客户端channel。前者只有一个,后者却很多,来一个请求便创建一个。且后者是前者在②处创建出来的。这两种channel有种父子关系的特征,后面netty就是用了这种概念表示。
下面看看netty的示例。学之前以为netty的非阻塞是以nio为基础创建的,应该差不多。看过来发现,果然,一点也不一样。
public class NettyServer {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
final ByteBuf buf = Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8"));
try {
//服务端的引导类
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置线程组
serverBootstrap.group(group)
// 设置非阻塞channel
.channel(NioServerSocketChannel.class)
// 设置绑定本地的端口
.localAddress(new InetSocketAddress(8080))
// 设置
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
String text = CharsetUtil.UTF_8.decode(byteBuf.nioBuffer()).toString();
System.out.println("接受到的消息:" + text);
}
});
}
});
ChannelFuture f = serverBootstrap.bind().sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
解释一下
- netty和java原生nio实现方式相当不一样,它将nio和oio的实现方式做了统一,所以,上面非阻塞式的代码,只需改动一点即可实现oio的方式。
- 从概念上来讲,Bootstrap(引导器)的说法在java nio上是没有的,它相当于一个用于集成引导配置的容器。有ServerBootstrap(用于服务器)和BootStrap(用于客户端)。
- EventLoopGroup和EventLoop很重要。EventLoopGroup用于管理多个EventLoop,而EventLoop关联一个线程。同时EventLoop又充当选择器(Selector)的角色。用于选取已注册的准备好的事件。
- 还有一个点是childHandler,用于设置处理接收而来的客户端channel。而handler,则用与设置服务器Channel。
netty流程浅析
你可以以理解java nio的方式理解netty。ServerBootstrap作为服务端的引导类,作用为串联配置,启动服务器。EventLoop是netty中的重要部件,有java nio中的选择器的功能,可以选择就绪的channel,且自身关联一个Thread。看下图
这图是从《netty实战》中找的,可以简单概括出EventLoopGroup、EventLoop以及Channel的关系。
EventLoopGroup可在创建时指定EventLoop的个数,如图中为3个。同时,EventLoopGroup负责为每个新创建的Channel(客户端Channel)分配一个EventLoop。一般采用顺序循环的方式分配。如此,客户端连接一多,每个EventLoop就会负责多个Channel。EventLoop本身还关联着一个Thread。负责处理Channel的读或写等事件。每个Channel的整个生命周期的事件均由其关联的EventLoop的线程处理,这样可避免多线程环境下数据同步等问题。
对比java nio的选择器模型,可以发现一些相似之处。这里的selector同样负责多个channel的事件处理。
当channel的某个事件准备好后,就可以根据业务需要处理这些数据了(或读或写等)。netty的处理流程对应的是一个处理链。ChannelPipeline。处理链上可添加若干个单个处理逻辑:ChannelHandler。这种处理方式使得处理逻辑简单清晰(如可将处理编解码的handler和序列化以及处理业务逻辑的代码分离开)。并且当需要改变处理流程时(如出站数据需要进行加密),只需动态添加(或移除)一个ChannelHandler即可。
图中直观显示了ChannelPipeline和ChannelHandler的关系。上面示例中,设置childHandler即可设置一个ChannelHandler。
启动流程
ServerBootstrap作为server端的引导器,是串联整个流程的关键。前面也说过,netty的引导器分2种,服务端的(ServerBootStrap)和客户端的(Bootstrap)。其类继承关系如图
可见两者均继承了AbstractBootstrap,这里只分析ServerBootStrap。
ServerBootStrap的group方法用于设置EventLoopGroup。上面示例中类似于这样设置的。
group(new NioEventLoopGroup())
看其源码
@Override
public ServerBootstrap group(EventLoopGroup group) {
return group(group, group);
}
以及
/**
* Set the {@link EventLoopGroup} for the parent (acceptor) and the child (client). These
* {@link EventLoopGroup}'s are used to handle all the events and IO for {@link ServerChannel} and
* {@link Channel}'s.
*/
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (childGroup == null) {
throw new NullPointerException("childGroup");
}
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
这里需解释下parentGroup和childGroup的含义。parentGroup用于处理ServerSocketChannel对应的事件(也就是accept()事件),而childGroup用于处理客户端channel的读写等的事件。前面提过这两种channel有一种父子对应的关系,所以netty就这样做的命名。
从源码可以看出,如果只设置一个group,则parentGroup和childGroup共用一个group。
目前来说,一般在引导器中主动设置两个EventLoopGroup,即
EventLoopGroup parentGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(parentGroup, workerGroup);
看一下NioEventLoopGroup
类的构造器方法
public NioEventLoopGroup() {
this(0);
}
public NioEventLoopGroup(int nThreads) {
this(nThreads, (Executor) null);
}
可知,传递的数字参数为线程数,跟踪代码知道,若不设线程数(无参),则最终为核心数的2倍。
protected MultithreadEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, chooserFactory, args);
}
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
ServerBootstrap最关键的方法是bind
方法。也是开启netty服务器的方法。其具体实现在父类AbstractBootstrap
。
调用链为 doBind()-> initAndRegister()->init()。init()依靠子类实现。这也是模板方法的应用。看看init()方法。
void init(Channel channel) throws Exception {
// 设置属性option及attr,略过
// 获取pipeline
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
// 设置属性,略过
// 添加处理逻辑
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}
init(channel)的参数channel要说明一下。其来源于NioServerSocketChannel,经反射得到的。
serverBootstrap.group(group).channel(NioServerSocketChannel.class)
也即这个channel是与服务器相关联的channel,这些代码为设置服务端channel的pipeline和handler。看看ServerBootstrapAcceptor
。
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
//省略其他字段及方法
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger);
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
try {
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
}
ServerBootstrapAcceptor
继承了ChannelInboundHandlerAdapter
。用于负责接收客户端的连接。当连接过来后注册到childGroup
中。
handler与childHandler的区别在于前者处理服务端handler,如接收新客户端连接;后者处理客户端连接,如客户端读写等事件。
文本为简要介绍netty流程,后续尝试逐步分析。若有问题还请指正。