BootStrap

Netty 中的 BootStrap 分为两种:一种是客户端的 BootStrap;一种是服务端的 ServerBootStrap

  • 客户端的 BootStrap

    初始化客户端,该 BootStrap 只有一个 EventLoopGroup,用于连接远程主机

  • 服务端的 ServerBootStrap

    用于绑定本地端口,一般存在两个 EventLoopGroup(一个用于包含单例的 ServerChannel,用于接收和分发请求;另一个是包含了所有创建的 Channel,处理服务器接受到的所有来自客户端的连接)。

启动流程

BootStrap 的启动流程(以 ServerBootStrap 为例):

具体的代码如下所示:

NioEventLoopGroup group = new NioEventLoopGroup(); // 创建一个 NioEventLoopGroup
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class) // 设置 Channel 的类型为 NioServerSocketChannel
.localAddress(new InetSocketAddress(9098)) // 监听本地的 9098 端口
.option(ChannelOption.TCP_NODELAY, true) // 设置启用 Nagle 算法
.option(ChannelOption.SO_BACKLOG, 1024) // 设置最大等待连接队列长度
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 对当前的 Channel 中的 pieline 添加一系列的 Handler
ch.pipeline().addLast(new ProtobufDecoder(null));
ch.pipeline().addLast(new ProtobufEncoder());
}
});

Channel

以客户端连接到服务端为例

客户端 Channel 的初始化过程

  1. 通过 BootStrap 实例对象设置对应的 Channel的类型,即上文的 bootStrap.channel(NioSocketChannel.class) 就是将当前的 BootStrapChannel 类型设置为 NioSocketChannel。这个过程只是指定了 Channel 的类型,同时设置了对应的 ChannelFactory,并没有真正实例化 Channel

  2. 通过调用 Bootstrapconnect()方法完成 Channel 的实例化。具体调用链(在 Boostrap):connect() ——> doResolveAndConnect() ——> initAndRegister() ——> channelFactory.newChannel() 完成 Channel 的初始化工作

  3. Channel 的初始化过程中,首先会触发 NioSocketChannel 的构造器,使用默认的 selector 来创建一个 NioSocketChannel,然后依次调用父类的方法进行一系列的初始化操作。

实例化的具体流程如下图所示:

服务端 Channel 的初始化过程

服务端的 Channel 初始化与客户端的 Channel 类似,不同的地方在于服务端的 Channel 是绑定到对应的主机和端口,而 客户端的 Channnel 是需要连接到服务器的 Channel。

客户端 Channel 的注册过程

在调用 BootStrapbind() 方法的过程中,会调用到 initRegister() 方法,在这个方法中会对 Channel 进行初始化;在调用 register() 方法之后,会再次调用 EventLoopGroup 对象的 register(Channel channel) 方法将 Channel 注册到对应的 EventLoopGroup 中。具体源代码如下所示:

// 移除了处理异常等不相关的代码
final ChannelFuture initAndRegister() {
Channel channel = null;
// 使用对应的 ChannelFactory 对象创建一个 Channel
channel = channelFactory.newChannel();
init(channel); // 将 Channel 注册到对应的 EventLoopGroup 中
ChannelFuture regFuture = config().group().register(channel); return regFuture;
}

调用 EventLoopGroupregister(Channel channel) 方法会调用 AbstractUnsaferegister(EventLoop eventLoop, ChannelPromise promise) 方法来进行注册。具体源代码如下所示:

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
// 省略一部分检查的代码
AbstractChannel.this.eventLoop = eventLoop; if (eventLoop.inEventLoop()) {
register0(promise);
}
// 省略一部分代码。。。
}

register0 源代码如下所示:

private void register0(ChannelPromise promise) {
// 省略一些无用的代码
doRegister();
// 省略一些无用的代码
}

由于当前使用的是 NioSocketChannel,因此会调用到 AbstractNioChanneldoRegister() 方法,具体的源代码如下所示:

protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
/*
javaChannel() :由于这里注册的 Channel 是 NioSocketChannel,因此会得到当前正要注册的 NioSocketChannel
通过调用 Channel 的 register() 方法,将这个 Channel 注册到对应的 Selector 中,这里的 selector 对应着 NioEventLoop,即分配到的 EventLoop 对象,到此 Channel 的注册完成
*/
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
// 省略一部分异常捕获的代码
}
}
}

总的流程图如下所示:

服务端 Channel 的注册过程

同样地,服务端的 Channel 的注册和客户端的也十分类似,不同的地方在于服务端的 Channel 会注册到“主” EventLoopGroup ,而客户端的 Channel 则只是注册到一个普通的 EventLoopGroup

客户端 Channel 的连接过程

服务端 Channel 接受连接的过程

服务端的 Channel 在启动时会首先创建一个 NioServerSocketChannel 并注册到 “主” EventLoopGroup,用于创建对应的处理对应的连接请求。处理连接的请求在初始化 NioServerSocketChannel 的时候就已经准备好了,初始化服务端的 Channel 的源代码如下所示:

// 该代码位于 ServerBootStrap
@Override
void init(Channel channel) {
// 获取当前 Channel 的 Pipeline
ChannelPipeline p = channel.pipeline(); // 这里的 childGroup 是 “从”—EvenLoopGroup,用于真正处理请求
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs); p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
// 将在 bootStrap 中添加的 ChannelHandler 对象添加到 Pipeline 的末尾
if (handler != null) {
pipeline.addLast(handler);
} ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
// ServerBootstrapAcceptor 中重写了 channelRead() 方法,每个连接的请求都会首先调用这个方法以读取请求的内容
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}

ServerBootstrapAcceptorchannelRead() 的源代码如下所示:

@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg; // childHandler 由 ServerBootStrap 对象进行指定
child.pipeline().addLast(childHandler); /*
注意这里的 childGroup 对应的是 “从”—EventLoopGroup,由于 “从”—EventLoopGroup 是处理请求的,
因此在这里得到的请求(即 msg)会为它新创建一个 Channel 注册到“从”—EventLoopGroup 的某个 EventLoop 中 这个请求转发到 “从”—EventLoopGroup 的工作是由 “主”—EventLoopGroup 来完成的
*/
childGroup.register(child).addListener(); // 省略部分异常检查代码。。。。
}

channelRead() 的工作已经介绍完了,现在问题是 “主”—EventLoopGroup 是如何处理请求的了。这个和使用 NIO 进行网络编程有关系,当绑定当前的一个 NioServerSocketChannel时,Netty 的底层会监听对应的事件。当 NioServerSocketChannel 的状态为 SelectionKey.OP_ACCEPT时,表示当前的 NioServerSocketChannel 是可以接收连接请求的,当一个请求到达时,便会执行对应的请求,NioServerSocketChannel 执行对应请求的源代码如下所示:

/*
因为每个 EventLoop 都是通过单个线程的方式来处理对应的任务的,同样地,NioServerSocketChannel 也是通过 EventLoop 来进行任务处理的 该方法位于 NioEventLoop 中,因为初始化 NioServerSocketChannel 时指定了 NioEventLoop 来处理每个任务
*/
@Override
protected void run() {
for (;;) {
// 省略一大段异常处理和其它不是很关键的代码
processSelectedKeys();
}
}

processSelectedKeys 源代码如下所示:

private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}

尽管存在一个判断条件,但实际上,由于当前处理的 Channel 是 NioServerSocketChannel,最终都会调用 processSelectedKey 方法进行进一步的处理。具体的源代码如下所示:

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
// 省略一大段异常检查的代码
int readyOps = k.readyOps();
// 这里就是 NioServerSocketChannel 在可以读取时要进行处理的代码块。当当前的 Channel 是可读的或者是可接收请求是则执行对应的逻辑
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
/*
这里的 Unsafe 对象由 NioServerSocketChannel 在实例化时创建,
具体由 AbstractNioMessageChannel 通过重写 newSafe() 方法来实现 这里的 Unsafe 类为 AbstractNioMessageChannel 的内部类
*/
unsafe.read();
}
}

unsafe.read()方法的源代码:

@Override
public void read() {
// 核心代码如下,已经省略大部分其它不太重要的代码
do {
/*
doReadMessages 由具体的子类来实现,在这里具体的子类为 NioServerSocketChannel 每次调用 doReadMessages 都将创建一个新的 SocketChannel,即一个新的连接放入 readBuf 列表中
*/
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
} allocHandle.incMessagesRead(localRead);
} while (continueReading(allocHandle)); int size = readBuf.size();
// 将读取到的数据进行相应的处理
for (int i = 0; i < size; i ++) {
/*
由于只有一个服务线程处理请求数据,因此就不会因为同时有多个请求进行访问而造成数据混乱
*/
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
} readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
}

doReadMessages() 方法源代码如下所示:

@Override
protected int doReadMessages(List<Object> buf) throws Exception {
// 根据当前的 NioServerSocketChannel 创建一个新的 SocketChannel
SocketChannel ch = SocketUtils.accept(javaChannel());
if (ch != null) {
// 可以看到,每次读取时都会为当前读取的数据段分配一个新的 NioSocketChannel 来进行相应的任务处理
buf.add(new NioSocketChannel(this, ch)); // 新分配的 NioSocketChnnel 的
return 1;
}
// 省略部分异常检测代码 return 0;
}

至此,NioServerSocketChannel 是如何分发请求到 NioSocketChannel 就已经非常清楚了

接收请求的具体流程如下所示:

ChannelPipeline

ChannelPipelineChannelHandler 链的容器,用于存储一系列的 ChannelHandler

每当一个新的 Channel 被创建了,都会建立一个新的 ChannelPipeline,并且这个 ChannelPipeline 会绑定到 Channel 上。具体对应关系如下图所示:

ChannelPipeline 是双向的,但是同一时刻只能有一个方向的任务进行,如下图所示:

当一个入站事件被触发时,将会按照 ChannelInboundHandler 的顺序进行对应的处理。Netty 一般会认为入站的开始位置为 ChannelPiepeline 的开始位置,及上图上 SocketChannelChannelPipeline 的开始位置,因此在添加对应的 ChannelHandler 时需要注意这一点。

ChannelPipeline 的初始化

初始化 ChannelPipeline 的源代码如下所示:

/*
这里的 Channel 是之前初始化的 NioServerSocketChannel
*/
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true); tail = new TailContext(this); // 上问 ChannelPipeline 示意图中的 head 节点,这是一个哑节点
head = new HeadContext(this); // Pipeline 中的尾节点,同样也是一个哑节点 // 组成一个双向链表,注意上文提到一个 Pipeline 可以是双向的,因此这里的数据结构采用双向链表
head.next = tail;
tail.prev = head;
}

即初始化 ChannelPipeline 时,会维护一个由 ChannelHandlerContext 组成的双向链表,这个链表的作用是对相应的 Channel 进行i相关的事件处理。

ChannelHandler

服务端的 ChannelHandler

由于服务端存在两种类型的 EventLoopGroup,一个用于接收和分发请求,一个用于真正处理请求,因此 ChannelHandler 也分为两种,一类是用于接收请求时使用,一类是用于处理请求时使用。

ServerBootStrap通过 handler(ChannelHandler handler) 方法来指定接收请求是要执行的 ChannelHandler,这里的ChannelHandler添加是发生在初始化 Channel 的过程中(注意是初始化 Channel 而不是实例化 Channel)。具体的源代码如下所示:

@Override
void init(Channel channel) { // 这里的 Channel 是实例化之后的 Channel,即 NioServerSocketChannel
ChannelPipeline p = channel.pipeline(); final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs); p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
final ChannelPipeline pipeline = ch.pipeline();
// 这里的 handler 是通过 ServerBootStrap 调用 handler(...) 来指定的
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));
}
});
}
});
}

初始化之后,服务端 NioServerSocketChannel 的 Pipeline 的底层 Handler 结构如下图所示:

当接收到一个新的客户端请求时,会调用 ServerBootstrapAcceptor.channelRead 方法,具体源代码如下图所示:

 public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 这里的 child 是分配到 workGroup 中的一个 Channel,不是服务端处理连接的 NioServerSocketChannel
final Channel child = (Channel) msg; // 这里的 chilHandler 是在 ServerBootStrap 对象中通过 childHandler() 方法设置的
child.pipeline().addLast(childHandler); // 省略一部分不重要的代码
}

EventLoop

EventLoop 的初始化

NioEvenLoopGroup有几个构造器,但是最终都是调用父类 MultithreadEventLoopGroup 的构造器

调用父类的 MultithreadEventLoopGroup 构造器初始化:

private static final int DEFAULT_EVENT_LOOP_THREADS;

static {
// 初始化 EventLoop 线程数,这里设置的线程数为处理器的数量 * 2
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
} // 确定 EventLoop 的线程数,再交给父类进行构造
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

再次调用父类的构造函数进行初始化:

protected MultithreadEventExecutorGroup(int nThreads,
Executor executor,
EventExecutorChooserFactory chooserFactory,
Object... args) {
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
} children = new EventExecutor[nThreads]; for (int i = 0; i < nThreads; i ++) {
children[i] = newChild(executor, args);
} chooser = chooserFactory.newChooser(children);
}

主要任务:

  • 创建一个大小为 nThreadsEventExecutor 数组

  • 通过调用 newChild 来初始化 children 数组的每个元素

  • 根据 nThreads 的大小,创建不同的 EventExecutorChooser,如果 nThreads 是 2 的整数幂,则使用 PowerOfTwoEventExecutorChooser,否则,使用 GenericEventExecutorChooser。它们的功能够一样,都是从 children 数组中选出一个合适的 EventExecutor 实例

    源代码如下:

    public EventExecutorChooser newChooser(EventExecutor[] executors) {
    if (isPowerOfTwo(executors.length)) {
    return new PowerOfTwoEventExecutorChooser(executors);
    } else {
    return new GenericEventExecutorChooser(executors);
    }
    }

总的初始化流程:

  • EventLoopGroup(实际上是 MultithreadEventExecutorGroup)内部维护了一个类型为 EventExecutor 的 children 数组,其大小为 nThreads,这样就创建了一个线程池
  • MultithreadEventLoopGroup 中会确定要选择的 EventLoop 线程数,默认为可用的处理器大小 * 2
  • MultithreadEventExecutorGroup 会调用 newChild 方法来初始化 children 数组的每个元素

具体的流程如下所示:

Netty 组件介绍的更多相关文章

  1. Netty组件介绍(转)

    http://www.tuicool.com/articles/mEJvYb 为了更好的理解和进一步深入Netty,我们先总体认识一下Netty用到的组件及它们在整个Netty架构中是怎么协调工作的. ...

  2. Netty快速入门(09)channel组件介绍

    书接上回,继续介绍组件. ChannelHandler组件介绍 ChannelHandler组件包含了业务处理核心逻辑,是由用户自定义的内容,开发人员百分之九十的代码都是ChannelHandler. ...

  3. 从netty-example分析Netty组件续

    上文我们从netty-example的Discard服务器端示例分析了netty的组件,今天我们从另一个简单的示例Echo客户端分析一下上个示例中没有出现的netty组件. 1. 服务端的连接处理,读 ...

  4. 开源免费且稳定实用的.NET PDF打印组件itextSharp(.NET组件介绍之八)

    在这个.NET组件的介绍系列中,受到了很多园友的支持,一些园友(如:数据之巅. [秦时明月]等等这些大神 )也给我提出了对应的建议,我正在努力去改正,有不足之处还望大家多多包涵.在传播一些简单的知识的 ...

  5. 免费开源的.NET多类型文件解压缩组件SharpZipLib(.NET组件介绍之七)

    前面介绍了六种.NET组件,其中有一种组件是写文件的压缩和解压,现在介绍另一种文件的解压缩组件SharpZipLib.在这个组件介绍系列中,只为简单的介绍组件的背景和简单的应用,读者在阅读时可以结合官 ...

  6. 免费高效实用的.NET操作Excel组件NPOI(.NET组件介绍之六)

    很多的软件项目几乎都包含着对文档的操作,前面已经介绍过两款操作文档的组件,现在介绍一款文档操作的组件NPOI. NPOI可以生成没有安装在您的服务器上的Microsoft Office套件的Excel ...

  7. 免费开源的DotNet任务调度组件Quartz.NET(.NET组件介绍之五)

    很多的软件项目中都会使用到定时任务.定时轮询数据库同步,定时邮件通知等功能..NET Framework具有“内置”定时器功能,通过System.Timers.Timer类.在使用Timer类需要面对 ...

  8. 免费开源的DotNet二维码操作组件ThoughtWorks.QRCode(.NET组件介绍之四)

    在生活中有一种东西几乎已经快要成为我们的另一个电子”身份证“,那就是二维码.无论是在软件开发的过程中,还是在普通用户的日常中,几乎都离不开二维码.二维码 (dimensional barcode) , ...

  9. 最好的.NET开源免费ZIP库DotNetZip(.NET组件介绍之三)

    在项目开发中,除了对数据的展示更多的就是对文件的相关操作,例如文件的创建和删除,以及文件的压缩和解压.文件压缩的好处有很多,主要就是在文件传输的方面,文件压缩的好处就不需要赘述,因为无论是开发者,还是 ...

  10. 高效而稳定的企业级.NET Office 组件Spire(.NET组件介绍之二)

    在项目开发中,尤其是企业的业务系统中,对文档的操作是非常多的,有时几乎给人一种错觉的是”这个系统似乎就是专门操作文档的“.毕竟现在的很多办公中大都是在PC端操作文档等软件,在这些庞大而繁重的业务中,单 ...

随机推荐

  1. u-boot启动流程

    U-Boot(Universal Bootloader)是一个通用的开源引导加载程序,通常用于嵌入式系统中,负责引导操作系统或加载 Linux 内核等任务.U-Boot的启动流程可以概括为以下几个关键 ...

  2. it 作形式主语:It's no good doing sth.

    It's no good doing sth. 这个 句型其实是一个省 略介词 in 的句型,完整形式是 It's no good in doing sth. 其中, good 是形容词,和介词 in ...

  3. fasthttp + `page partial gziped cache`: 页面输出服务性能提升20%

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 接上一篇:http 中使用 gzip 输出内容时,如何预先 ...

  4. poj2279

    Mr. Young's Picture Permutations Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 5841   ...

  5. Subtree 题解

    Subtree 题目大意 给定一颗树,你可以选出一些节点,你需要对于每个点求出在强制选这个点的情况下所有选择的点联通的方案数,对给定模数取模. 思路分析 对于这种求树上每一个点方案数的题目,首先考虑换 ...

  6. client-go实战之九:手写一个kubernetes的controller

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<client-go实战> ...

  7. My 2022

    很久以前好像是想过要写这么一个东西的.然而时间已至却毫无思路,故拖延至今. 很充实的一年.但似乎是唯一除了 whk/OI 相关完全不知还能写些什么的一年呢.(笑) 本来想写些别的什么,又忽而发现所有想 ...

  8. 7/10 luoguRound 10 庆典 div3T1

    #include<bits/stdc++.h> using namespace std; int m,n,x; int arr[100005]; int maxi = -1,maxn = ...

  9. Java比赛常用API总结

    1.栈和队列 1.1 栈的常用方法 //1.栈顶插入元素 push(element) //2.返回栈顶元素并弹出栈顶元素 pop() //3.返回栈顶元素但不弹出 peek() //4.清空栈 cle ...

  10. .NET的各种对象在内存中如何布局[博文汇总]

    在过去一段时间里,我陆陆续续写一些关于.NET对象类型布局的文章,其中包括值类型和引用类型的内存布局.字符串对象和数组的内存布局等,这里作一个简单的汇总. [1] 如何计算一个实例占用多少内存? 我们 ...