启动服务(上)服务端:NioServerSocketChannel 是什么时候激活的

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

本文会从请求处理的角度分析 Netty 源码,包含以下 7 个过程:启动服务、构建连接、接收数据、业务处理、发送数据、断开连接、关闭服务。

Netty 服务端启动,最主要的工作就是绑定端口、构建连接。

1. 主线分析

我们先回顾一下 Server 启动的代码:

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup) // 配置参数Server
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() { ... }); ChannelFuture f = b.bind(8888).sync(); // 启动服务,绑定端口
f.channel().closeFuture().sync(); // 关闭服务

1.1 主线

ServerBootstrap 启动的核心方法是 bind。服务器在启动时主要工作如下:

  1. 初始化:最主要的是初始化并配置 ServerSocketChannel,包括 TCP 参数、Handler 的配置。
  2. 注册:包括注册到 NioEventLoop 和将 Channel 注册到 Selector 两部分工作。需要注意,ServerSocketChannel 注册完成后,不会立刻注册感兴趣的事件来启动任务。
  3. 绑定端口:只有注册完成后才会启动端口绑定,并在端口绑定完成后注册 OP_ACCEPT 启动任务。
  4. 启动任务:在 Selector 上注册 OP_ACCEPT 事件,开始构建连接。

ServerBootstrap#bind 主要的执行过程如下:

ServerBootstrap#bind
-> initAndRegister
-> ServerBootstrap#init # 初始化
-> AbstractUnsafe#register # 注册
-> doBind0
-> AbstractUnsafe#bind # 绑定端口
-> AbstractUnsafe#beginRead # 启动任务,通过pipeline.fireChannelActive()

以上的初始化启动工作,分别是在 main thread 和 boss thread 两个线程上执行。

  1. main thread 线程

    • 创建 Selector。

    • 创建 ServerSocketChannel,并初始化。

    • 将 Channel 注册到 EventLoopGroup。

  2. boss thread 线程

    • 将 Channel 注册到 EventLoopGroup 对应的 Selector。
    • 绑定地址启动。
    • Channel 将 OP_ACCEPT 事件注册到 Selector。

注意:Netty 为了避免上下文切换,采用了一种局部串行化的执行方式。也就是将任务提交到 Channel 注册的 EventLoopGroup 上执行。对于 ServerSocketChannel 而言就是 EventLoop。

1.2 知识点

(1)启动服务的本质

  • Selector:Selector selector = SelectorProviderImpl.openSelector();
  • ServerSocketChannel:ServerSocketChannel serverSocketChannel = provider.openServerSocketChannel();
  • 注册 Channel 到 Selector:selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
  • 绑定端口:javaChannel().bind(localAddress, config.getBacklog());
  • 注册感兴趣事件到 Selector:javaChannel().bind(localAddress, config.getBacklog());

(2)Selector 创建

每个 NioEventLoop 对应一个自己的 Selector,在创建 NioEventLoop 时会创建 Selector。一旦有 Channel 注册到 NioEventLoop 上,就会通过调用 startThread 方法启动该线程。

(3)感兴趣事件注册时机

  • Channel 注册时都是 0:selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this) 。
  • 感兴趣事件注册:当 pipeline.fireChannelActive() 时,调用 beginRead 注册感兴趣的事件。对于 NioServerSocketChannel 而言,就是注册 OP_ACCEPT 事件,NioSocketChannel 则是 OP_READ 事件。

2. 源码分析

图1:服务端启动流程

  • 初始化:init() 方法,对应第 1~6 步。先通过反射创建 ServerSocketChannel,再通过 init 进行初始化。初始化主要是配置 ServerSocketChannel 的 TCP 参数、附加属性、自定义 Handler、以及构建连接的 ServerBootstrapAcceptor。
  • 注册:register() 方法,对应第 7~10 步。包含到 EventLoop 和 Selector 的注册。其中 doRegister 调用 javaChannal.register() 注册到 Selector。
  • 绑定端口:doBind0() 方法,对应第 11~15 步。绑定端口后,触发 pipeline#fireChannelActive 开始接收连接。其中 doBind 调用 javaChannal.bind() 绑定端口。
  • 启动任务:beginRead() 方法。通过 pipeline#fireChannelActive 方法调用 beginRead,将 OP_ACCEPT 注册到 Selector 上,此时服务端已经启动成功,可以接收客户端的连接。其中 beginRead 调用 selectionKey.interestOps 注册感兴趣的事件。
private ChannelFuture doBind(final SocketAddress localAddress) {
// 1. 初始化和注册。initAndRegister是异步执行
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
} // 2. 绑定端口:注意doBind0必须在initAndRegister执行完成后
// doBind0是同步执行,也就是只有javaChannel.bind之后,才会注册OP_ACCEPT事件
if (regFuture.isDone()) {
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}

2.1 初始化

Server 看需要配置的参数有:

  1. 线程池:服务端通常会构建两个线程池。bossGroup 负责接收连接,childGroup 负责处理网络 IO 以及系统 Task。
  2. TCP 参数和其它附加配置。
  3. 自定义 Handler。如日志输出等。
ServerBootstrap#initAndRegister
-> ChannelFactory#newChannel # 通过反射创建连接
-> init
-> setChannelOptions # TCP参数设置
-> setAttributes # Channel附加属性设置
-> ChannelPipeline#addLast(handler) # ServerChannel配置的Handler
-> ChannelPipeline#addLast(ServerBootstrapAcceptor) # 接收连接的Handler

说明: initAndRegister 先通过反射创建 ServerSocketChannel,再通过 init 进行初始化。初始化主要是配置 ServerSocketChannel 的 TCP 参数、附加属性、自定义 Handler、以及接收连接的 Handler。

void init(Channel channel) throws Exception {
// 1. 配置 TCP 参数和附加属性
setChannelOptions(channel, options0().entrySet().toArray(newOptionArray(0)), logger);
setAttributes(channel, attrs0().entrySet().toArray(newAttrArray(0)));
... // 2. 配置Handler,其中ServerBootstrapAcceptor为接收客户端的Handler
pipeline.addLast(config.handler());
pipeline.addLast(new ServerBootstrapAcceptor(
channel, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}

说明:ServerBootstrapAcceptor 是专门用于接收客户端请求的 Handler。pipeline.addLast 添加 ChannelInitializer 时,ChannelInitializer 执行 initChannel 方法后,会将 ChannelInitializer 自身从 pipeline 中移除。Hander 执行过程如下:

思考1:ServerSocketChannel 创建为什么要用 provider.openServerSocketChannel()?

private static ServerSocketChannel newSocket(SelectorProvider provider) {
// ServerSocketChannel.open() 每秒创建 5000 个连接时性能会下将 1%
// https://github.com/netty/netty/issues/2308
return provider.openServerSocketChannel();
}

2.2 注册

ServerBootstrap#initAndRegister
-> EventLoopGroup#register
-> SingleThreadEventLoop#register
-> AbstractUnsafe#register √

说明: 其实 Channel 注册包含以下几件事:

  1. 绑定线程:将 Channel 注册到 EventLoop。

  2. 绑定 Selector:将 Channel 注册到 EventLoop 对应的 Selector。

  3. 激活 Channel:如果 Channel 已经是 Active 状态,注册感兴趣的事件到 Selector 上,启动任务。需要注意的是,对于 ServerSocket 而言,此时不可能是 Active 状态,只有 bind 成功后才会注册 OP_ACCEPT 来接收客户端连接。

config().group().register(channel)

注意:这里的 config().group() 是 bossGroup。Channel 是如何注册到 NioEventLoopGroup 是的详见:https://www.cnblogs.com/binarylei/p/10135712.html

2.3 绑定端口

ServerBootstrap#doBind0
-> AbstractChannel#bind
-> AbstractUnsafe#bind √
-> NioServerSocketChannel#doBind
-> DefaultChannelPipeline#fireChannelActive
-> AbstractUnsafe#beginRead √

说明: Channel 绑定端口成功后会通过 pipeline#fireChannelActive 方法调用 beginRead,将 OP_ACCEPT 注册到 Selector 上,此时服务端已经启动成功,可以接收客户端的连接。

// AbstractUnsafe#bind
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
doBind(localAddress); // javaChannel.bind()
pipeline.fireChannelActive(); // beginRead()
}

NioServerSocketChannel#doBind 则会调用最底层的 javaChannel.bind() 绑定端口。

2.4 激活 Channel

pipeline#fireChannelActive[tail...head] -> head#readIfIsAutoRead
-> channel#read -> pipeline#read[head...tail] -> head#read
-> AbstractUnsafe#beginRead
-> AbstractNioChannel#doBeginRead

说明:pipeline#fireChannelActive 最终会调用 doBeginRead 方法,将 OP_ACCEPT 注册到 Selector 上,从而启动服务。

// HeadContext#readIfIsAutoRead
private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) {
channel.read();
}
}

说明: DefaultChannelConfig 中所有的 Channel 的 autoRead 属性都默认是 1,也就是 true。channel.read 通过 pipeline 从 tail -> head 传播,调用 head.read 方法时最终调用 unsafe.beginRead()。

unsafe.beginRead 直接调用 doBeginRead 方法激活 Channel。事实上,也就是将感兴趣的事件注册到 Selector 上。对于 NioServerSocketChannel 而言,就是注册 OP_ACCEPT 事件,NioSocketChannel 则是 OP_READ 事件。

@Override
protected void doBeginRead() throws Exception {
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}

思考1:channel.read()、pipeline.read()、ctx.read()、unsafe.read() 区别?

  • channel.read():直接调用 pipeline.read()。
  • pipeline.read():调用 tail.read(),从 head 或 tail 经历全部的 Handler。实际上,最后的 head.read() 调用 unsafe.beginRead(),这个方法会注册 OP_ACCEPT 或 OP_READ 事件从而激活 Channel。
  • ctx.read():从当前 ctx 开始之后的全部的 Handler。如果发送数据,需要使用 ctx.write 而不是 ctx.channel().write。
  • unsafe.read():最底层的 API。和 unsafe.beginRead() 不同,unsafe#read 会真正从 socket revbuf 读取数据。

每天用心记录一点点。内容也许不重要,但习惯很重要!

Netty 源码(一)服务端启动的更多相关文章

  1. Netty源码解析---服务端启动

    Netty源码解析---服务端启动 一个简单的服务端代码: public class SimpleServer { public static void main(String[] args) { N ...

  2. Netty源码解析 -- 服务端启动过程

    本文通过阅读Netty源码,解析Netty服务端启动过程. 源码分析基于Netty 4.1 Netty是一个高性能的网络通信框架,支持NIO,OIO等多种IO模式.通常,我们都是使用NIO模式,该系列 ...

  3. Netty 源码学习——服务端流程分析

    在上一篇我们已经介绍了客户端的流程分析,我们已经对启动已经大体上有了一定的认识,现在我们继续看对服务端的流程来看一看到底有什么区别. 服务端代码 public class NioServer { pr ...

  4. zookeeper源码之服务端

    zookeeper服务端主要包括一下几个模块:     1.启动模块. 2.核心执行模块 3.数据管理模块. 启动模块 读取配置文件,启动程序.详见:zookeeper源码之服务端启动模块. 核心执行 ...

  5. Netty源码—一、server启动(1)

    Netty作为一个Java生态中的网络组件有着举足轻重的位置,各种开源中间件都使用Netty进行网络通信,比如Dubbo.RocketMQ.可以说Netty是对Java NIO的封装,比如ByteBu ...

  6. zookeeper源码之服务端数据库管理中心

    负责管理ZooKeeper整个数据.主要管理树结构数据.session数据.持久化管理. 类图 ZKDatabase ZooKeeper数据管理门户类,底层通过DataTree来管理树结构,通过Fil ...

  7. Netty源码分析之客户端启动过程

    一.先来看一下客户端示例代码. public class NettyClientTest { public void connect(int port, String host) throws Exc ...

  8. kbengine mmo源码(完整服务端源码+资源+完整客户端源码)

      本项目作为kbengine服务端引擎的客户端演示而写 更新kbengine插件库(https://github.com/kbengine/kbengine_unity3d_plugins):    ...

  9. Nacos源码系列—服务端那些事儿

    点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友. 项目源码地址:公众号回复 nacos,即可免费获取源码 前言 在上节课中,我们讲解了客户端注册服 ...

  10. Netty源码—二、server启动(2)

    我们在使用Netty的时候的初始化代码一般如下 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGro ...

随机推荐

  1. 分布式tensorflow

    分布式Tensorflow Tensorflow的一个特色就是分布式计算.分布式Tensorflow是由高性能的gRPC框架作为底层技术来支持的.这是一个通信框架gRPC(google remote ...

  2. Django之模板Template

    模板介绍 作为Web框架,Django提供了模板,可以很便利的动态生成HTML 模版系统致力于表达外观,而不是程序逻辑 模板的设计实现了业务逻辑(view)与显示内容(template)的分离,一个视 ...

  3. 生产案例、Linux出现假死,怎么回事?

    1.什么是假死 所谓假死,就是能ping通,但是ssh不上去:任何其他操作也都没反应,包括上面部署的nginx也打不开页面. 2.假死其实很难出现一次 作为一个多任务操作系统,要把系统忙死,忙到ssh ...

  4. ldap复制

    1.使用yum命令安装openldap,openldap-servers,openldap-clients $ yum install openldap $ yum install openldap- ...

  5. 34. CentOS-6.3安装配置Apache2.2.6

    安装说明 安装环境:CentOS-6.3安装方式:源码编译安装 软件:httpd-2.2.6.tar.gz  | pcre-8.32.tar.gz | apr-1.4.6.tar.gz | apr-u ...

  6. jpa-入门测试

    package com.atguigu.jpa.test; import java.util.Date;import java.util.List; import javax.persistence. ...

  7. linux文件格式转换:<U+FEFF> character showing up in files. How to remove them?

    You can easily remove them using vim, here are the steps: 1) In your terminal, open the file using v ...

  8. Haskell语言学习笔记(44)Lens(2)

    自定义 Lens 和 Isos -- Some of the examples in this chapter require a few GHC extensions: -- TemplateHas ...

  9. maven GroupId 和ArtifactId通常填什么

    GroupID是项目组织唯一的标识符,实际对应JAVA的包的结构,是main目录里java的目录结构.ArtifactID就是项目的唯一的标识符,实际对应项目的名称,就是项目根目录的名称.

  10. python-条件和循环

    条件 Demo1: if i < 10: print('i<10') elif i == 0: print('i=0 ') else: print('...') Demo1说明了以下几点: ...