netty为了向使用者用户屏蔽NIO通信的底层细节,在和用户交互的边界做了封装,目的就是减少用户开发工作量降低开发难度。ServerBootstrap是Socket服务端的启动辅助类,用户通过ServerBootstrap可以方便地创建Netty的服务端。时序图如下:
下面我们对Netty服务端创建的关键步骤和原理进行讲解。
步骤1:创建ServerBootstrap实例。ServerBootstrap是Netty服务器的启动辅助类,它提供了一系列的方法用于设置服务端启动相关的参数。底层通过门面模式对各种能力进行抽象和封装,尽量不要用户过多地跟底层APi打交道,以降低用户开发难度。
我们在创建ServerBootstrap实例时,会惊讶地方法ServerBootstrap只有一个无参的构造函数,作为启动辅助类这让人不可思议,因为它需要与多个其他组件或者类交互。ServerBootstrap构造函数没有参数的根本原因是因为它的参数太多了,而且未来也可能会发生变化,为了解决这个问题,就需要引入Builder模式。《Effective Java》第二版第二条建议遇到多个构造器参数时要考虑用构建器。
步骤2:设置并绑定Reactor线程池。Netty的Reactor线程池是EventLoopGroup,它实际就是EventLoop的数组。EventLoop的职责是处理注册到本线程多路复用器Selector上的Channel,Selector的轮询操作由绑定的EventLoop线程run方法驱动,在一个循环体内循环执行。值得说明的是,EventLoop的职责不仅仅是处理网络I/O事件,用户自定义的Task和定时任务Task也统一由EventLoop负责处理,这样线程模型就实现了统一。从调度层面看,也不存在从EventLoop线程中在启动其他类型的线程用于异步执行另外的任务,这样就避免了多线程并发操作和锁竞争,提升了I/O线程的处理和调度性能。
步骤3:设置并绑定服务端Channel。作为NIO服务端,需要创建ServerSocketChannel,Netty对原生的NIO类库进行了封装,对应实现是NioServerSocketChannel。对于用户而言,不需要关心服务端Channel的底层实现细节和工作原理,只需要指定具体使用哪种服务端Channel即可。因此,Netty的ServerBootstrap方法提供了channel方法用于指定服务端Channel的类型。Netty通过工厂类,利用反射创建NioServerSocketChannel对象。由于服务端监听端口往往只需要在系统启动时才会调用,因此反射对性能的影响并不大。相关代码如下:
public ServerBootstrap channel(Class<? extends ServerChannel> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new ServerBootstrapChannelFactory<ServerChannel>(channelClass));
}
步骤4:链路建立的时候创建并初始化ChannelPipeline。ChannelPipeline并不是NIO服务端必需的,它本质就是一个负责处理网络事件的职责链,负责管理和执行ChannelHandler。网络事件以事件流的形式在ChannelPipeline中流转,由ChannelPipeline根据ChannelHandler的执行策略调度ChannelHandler的执行。典型的网络事件如下:
(1)链路注册;
(2)链路激活
(3)链路断开;
(4)接收到请求消息;
(5)请求消息接收并处理完毕
(6)发送应答消息
(7)链路发生异常
(8)发生用户自定义事件。
步骤5:初始化ChannelPipeline完成之后,添加并设置ChannelHandler。ChannelHandler是Netty提供给用户定制和扩展的关键接口。利用ChannelHandler用户可以完成大多数的功能定制,例如消息编解码、心跳、安全认证、TSL/SSL认证、流量控制和流量整形等。Netty同时也提供了大量的系统ChannelHandler供用户使用,比较实用的系统ChannelHandler总结如下。
(1)系统编解码框架——ByteToBessageCodec;
(2)通用基于长度的半包解码器——LengthFieldBasedFrameDecoder;
(3)码流日志打印Handler——LoggingHandler;
(4)SSL安全认证Handler——SslHandler;
(5)链路空闲检测Handler——IdleStateHandler;
(6)流量整形Handler——ChannelTrafficShapingHandler;
(7)Base64编解码——Base64Decoder和Base64Encoder。
创建和添加ChannelHandler的代码示例如下:
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new ServerBootstrapAcceptor(currentChildHandler, currentChildOptions,
currentChildAttrs));
}
});
步骤6:绑定并启动监听端口。在绑定监听端口之前系统会做一系列的初始化和检测工作,完成之后,会启动监听端口,并将ServerSocketChannel注册到Selector上监听客户端连接,相关代码如下(在NioServerSocketChannel中):
protected void doBind(SocketAddress localAddress) throws Exception {
this.javaChannel().socket().bind(localAddress, this.config.getBacklog());
}
步骤7:Selector轮询。由Reactor线程NioEventLoop负责调度和执行Selector轮询操作,选择准备就绪的Channel集合。
步骤8:当轮询到准备就绪的Channel之后,就由Reactor线程NioEventLoop执行CHannelPipeline的相应方法,最终调度并执行Channelhandler,接口如下:
步骤9:执行Netty系统ChannelHandler和用户添加定制的ChannelHandler。ChannelPipeline根据网络事件的类型,调度并执行ChannelHandler,代码如下:
Netty服务端创建源码分析
首先通过构造函数创建ServerBootstrap实例,随后,通常会创建两个EventLoopGroup(并不是必须要创建两个不同的EventLoopGroup,也可以只创建一个并共享),代码如下:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
NioEventLoopGroup实际就是Reactor线程池,负责调度和执行客户端的接入,网络读写事件的处理,用户自定义任务和定时任务的执行。通过ServerBootstrap的group方法将两个EventLoopGroup实例传入,代码如下:
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if(childGroup == null) {
throw new NullPointerException("childGroup");
} else if(this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
} else {
this.childGroup = childGroup;
return this;
}
}
其中父NioEventLoopGroup被传入了父类(AbstracpBootstrap)的构造函数中,代码如下。
public B group(EventLoopGroup group) {
if(group == null) {
throw new NullPointerException("group");
} else if(this.group != null) {
throw new IllegalStateException("group set already");
} else {
this.group = group;
return this;
}
}
该方法会被客户端和服务端重用,用于设置工作I/O线程,执行和调度网络事件的读写。
线程组和线程类型设置完成后,需要设置服务端Channel用于端口监听和客户端链路接入。Netty通过Channel工厂类来创建不同类型的Channel,对于服务端,需要创建NioServerSocketChannel。所以,通过指定Channel类型的方式创建Channel工厂。
ServerBootstrapChannelFactory是ServerBootstrap的内部静态类,职责是根据Channel的类型通过反射创建Channel的实例,服务端需要创建的是NioServerSocketChannel实例,代码如下:
public T newChannel(EventLoop eventLoop, EventLoopGroup childGroup) {
try {
Constructor<? extends T> constructor = this.clazz.getConstructor(new Class[]{EventLoop.class, EventLoopGroup.class});
return (ServerChannel)constructor.newInstance(new Object[]{eventLoop, childGroup});
} catch (Throwable var4) {
throw new ChannelException("Unable to create Channel from class " + this.clazz, var4);
}
}
指定NioServerSocketChannel后,需要设置TCP的一些参数,作为服务端,主要是要设置TCP的backlog参数,底层C的对应接口定义如下。
int listren(int fd,int backlog);
backlog指定了内核为此套接口排队的最大连接个数,对于给定的监听套接口,内核要维护两个队列:未连接队列和已连接队列,根据TCP三次握手过程中三个分节来分隔这两个队列。服务器处于listen状态时,收到客户端syn分节(connect)时在未完成队列中创建一个新的条目,然后用三次握手的第二个分节即服务器的syn响应客户端,此条目在第三个分节到达前(客户端对服务器syn的 ack)一直保留在未完成连接队列中,如果三次握手完成,该条目将从未完成的连接队列搬到已完成连接队列尾部。当进程调用Accept时,从已完成队列中的头部取出一个条目给进程,当已完成队列为空时进程将睡眠,直到有条目在已完成连接队列中才唤醒。backlog被规定为两个队列总和的最大值,大多数实现默认值是5,但在高并发Web服务器中此值显然不够,Lighttpd中此值达到128*8.需要设置此值更大一些的原因是未完成连接队列的长度可能因为客户端syn的到达及等待三次握手第三个分节的到达延时而增大。Netty默认的backlog为100,当然,用户可以修改默认值,这需要根据实际场景和网络状况进行灵活配置。
服务端启动最后一步,就是绑定本地端口,启动服务,下面我们看下这部分代码:
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();NO.1
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
final ChannelPromise promise;
if (regFuture.isDone()) {NO.2
promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
promise = new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE);
regFuture.addListener(new ChannelFutureListener() {
@Override NO.3
public void operationComplete(ChannelFuture future) throws Exception {
doBind0(regFuture, channel, localAddress, promise);
}
});
}
return promise;
}
先看下NO.1。首先创建Channel,createChanel由子类ServerBootstrap实现,创建新的NioServerSocketChannel。它有两个参数:参数1是从父类的NIO线程池中顺序获取一个NioEventLoop,它就是服务端用于监听和接收客户端连接的Reactor线程;参数2是所谓的workerGroup线程池,它就是处理I/O读写的Reactor线程组。
final ChannelFuture initAndRegister() {
Channel channel;
try {
channel = createChannel();
} catch (Throwable t) {
return VoidChannel.INSTANCE.newFailedFuture(t);
}
try {
init(channel);
} catch (Throwable t) {
channel.unsafe().closeForcibly();
return channel.newFailedFuture(t);
}
NioServerSocketChannel创建成功后,对它进行初始化,初始化工作主要有以下三点。
(1)设置Socket参数和NioServerSocketChannel的附加属性,代码如下:
void init(Channel channel) throws Exception {
final Map<ChannelOption<?>, Object> options = options();
synchronized (options) {
channel.config().setOptions(options);
}
final Map<AttributeKey<?>, Object> attrs = attrs();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
(2)将AbstractBootstrap的Handler添加到NioServerSocketChannel的ChannelPipeline中,代码如下。
ChannelPipeline p = channel.pipeline();
if (handler() != null) {
p.addLast(handler());
}
(3)将用于服务端注册的HandlerServerBootstrapAcceptor添加到ChannelPipeline中,代码如下:
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new ServerBootstrapAcceptor(currentChildHandler, currentChildOptions,
currentChildAttrs));
}
});
到此,Netty服务端监听的相关资源已经初始化完毕,就剩下最后一步——注册NioServerSocketChannel到Reactor线程的多路复用器上,然后轮训客户端连接事件。在分析代码前,我们先看下目前NioServerSocketChannel的ChannelPipeline的组成。
客户端接入源码分析
if (selectedKeys != null) {
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
由于Channel的Attachment是NioServerSocketChannel,所以执行processSelectedKey方法,根据就绪的操作位,执行不同的操作。此处,由于监听的是连接操作,所以执行unsafe.read()方法。由于不同的Channel执行不同的操作,所以NioUnsafe被设计成接口,由不同的Channel内部的NioUnsafe实现类负责具体实现。我们发现read()方法的实现有两个,分别是NioByteUnsafe和NioMessageUnsafe。对于NioServerSocketChannel,它使用的是NioMessageUnsafe。
final ChannelConfig config = config();
final int maxMessagesPerRead = config.getMaxMessagesPerRead();
final boolean autoRead = config.isAutoRead();
final ChannelPipeline pipeline = pipeline();
boolean closed = false;
Throwable exception = null;
try {
for (;;) {
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
if (readBuf.size() >= maxMessagesPerRead | !autoRead) {
break;
}
}
对doReadMessages方法进行分析,发现它实际就是接收新的客户端连接并创建NioSocketChannel,代码如下:
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = javaChannel().accept();
try {
if (ch != null) {
buf.add(new NioSocketChannel(this, childEventLoopGroup().next(), ch));
return 1;
}
接收到新的客户端连接后,触发ChannelPipeline的ChannelRead方法,代码如下:
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
pipeline.fireChannelRead(readBuf.get(i));
}
执行headChannelHandlerContext的fireChannelRead方法,事件在ChannelPipeline中传递,执行ServerBootstrapAcceptor的channelRead方法,代码如下:
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
for (Entry<ChannelOption<?>, Object> e: childOptions) {
try {
if (!child.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
logger.warn("Unknown channel option: " + e);
}
} catch (Throwable t) {
logger.warn("Failed to set a channel option: " + child, t);
}
}
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
child.unsafe().register(child.newPromise());
}
该方法主要分为如下三个步骤。
第一步:将启动时传入的childHandler加入到客户端SocketChannel的ChannelPipeline中
第二步:设置客户端SocketChannel的TCP参数;
第三步:注册SocketChannel到多路复用器。
以上三个步骤执行完成以后,下面我们展开看下NioSocketChannel的register方法,NioSocketChannel的注册方法与ServerSocketCannel的一致,也是将Channel注册到Reactor线程的多路复用器上。由于注册的操作位是0,所以此时NioSocketChannel还不能读取客户端发送的消息,那么什么时候监听操作位为OP_READ呢,继续看代码。
执行完注册操作后,紧接着会触发ChannelReadComplete事件。我们继续分析ChannelReadComplete在ChannelPipeline中的处理流程:Netty的Header和Tail本身不关注ChannelReadComplete事件就直接透传,执行完ChannelReadComplete后,接着执行Pipeline的read()方法,最终执行HeadHandler的read()方法。
HeadHandler read()方法用来将网络操作位修改为读操作。创建NioSocketChannel的时候已经将AbstractNioChannel的readInterestOp设置为OP_READ,这样,执行selectionKey.insterestOps(interestOps|readInterestOp)操作时就会把操作位设置为OP_READ。
到此,新接入的客户端连接处理完成,可以进行网络读写等I/O操作。