Netty源码分析(一)


Netty源码分析(一)

简述

从IO到NIO体现了java对于流操作的一个变迁,在到构建Netty,这一个系列文章主要讲述的就是Netty框架,第一篇文章从NIO出发;
主要分为:
Socket编程、IO模型、Netty简介、Netty示例,下面就开始今天的学习吧!

Socket编程

Socket最早是在4.3BSD UNIX中内置的’Berkeley Socket’演化而来,主要是用于实现进程间的通信;

Socket通信模型

Socket通信模型

Socket通信模型如上图所示,Socket是作为应用层与传输层之间协议,对应用层来说只要按照Socket设计的协议进行调用就可以进行通信了;

常用的Socket类型有两种:“Stream Sockets”(串流式 Sockets)和"Datagram Sockets"(讯息式 Sockets),Stream Sockets底层使用的是TCP协议,Datagram Sockets底层使用的是UDP协议;

CP 会在传输层对将上层送来的过大数据分割成多个 TCP 段(TCP segments),而 UDP 本身不会,UDP 是信息导向的(message oriented),若 UDP 信息过大时(整体数据包长度超过 MTU),则会由 host 或 router 在 IP 层对数据包进行分割,将一个 IP packet 分割成多个 IP fragments。IP fragmention 的缺点是,到达端的系统需要做 IP 数据包的重组,将多个 fragments 重组合并为原本的 IP 数据包,同时也会增加数据包遗失的可能性。如将一个 IP packet 分割成多个 IP fragments,只要其中一个 IP fragment 遗失了,到达端就会无法顺利重组 IP 数据包,因而造成数据包的遗失,若是高可靠度的应用,则上层协议需重送整个 packet 的数据。

简单示例

HttpServer01.kt

开启8880端口,然后返回字符串"socket",通过这个程序可以看到影响程序响应的因素是在于对响应的一个处理速度,示例中的程序是采用new Thread的方式,但是在还有很多改进的空间:使用线程池;

这种I/O模型是标准的select/poll模型,有以下这些缺点:

  1. 每次调用需要将数据从用户态拷贝到内核态
  2. select需要遍历内核态传递进来的全部fd(file descriptor)是指向一个打开的文件或I/O设备的数字标识符
  3. select支持的文件描述符只有1024个

NIO模型

IO模型

IO复用

Netty模型

Netty整体流程

下面根据这个整体流程先写一个简单示例;

Netty基本组件

NioEventLoop组件

NioEventLoop同时处理客户端连接读写客户端发送过来的数据

Channel组件

ByteBuf

Pipeline

使用逻辑链,来解析数据

Channel Handler

执行过程

服务端执行流程

  1. 创建服务端Channel
    bind():用户代码入口 -> initAndRegister():初始化并注册 -> newChannel():创建服务端Channel

  2. 初始化服务端Channel

    NioServerSocketChannel.class初始化方法:
    1. newSocket()通过JDK来创建原生Channel
    2. NioServerSockerChannelConfig配置TCP参数
    3. AbstractNioChannel
    3.1 configureBlocking 设置阻塞/非阻塞模式
    3.2 AbstractChannel创建id/unsafe/pipeline

添加 ServerBootstrapAcceptor -> ServerBootstrapAcceptor.init()

添加Handler,

  1. 注册selector

在创建和注册channel完成以后,会调用EventLoopGroup接口的实现类进行注册;

整体流程:

  1. bind()作为入口
  2. AbstractBootstrap#initAndRegister作为实现方法
  3. 通过EventLoopGroup的子类,NioEventLoopGroup的父类MultithreadEventLoopGroup实现的register()实现

register()执行的时候是通过MultithreadEventExecutorGroup#next()方法获取事件处理器,next()方法是通过DefaultEventExecutorChooserFactory#newChooser来进行选择,选择EventExecutor[] executors数组中的元素时,对于偶数和奇数的处理方式不同;

register()方法EventLoopGroup#register(Channel channel)接口定义的将传入的channel注册到EvetLoop的方法;

public ChannelFuture register(Channel channel, ChannelPromise promise) {
    channel.unsafe().register(this, promise);
    return promise;
}

EmbeddedEventLoop

EventLoopGroup通过调用channel.register方法来进行处理;

  1. 端口绑定
    1. AbstractBootstrap#doBind
    2. AbstractBootstrap#doBind0
    3. AbstractUnsafe#bind
    4. NioSocketChannel#doBind
    5. NioSocketChannel#doBind0
    6. SocketUtils#bind
    7. 事件广播

'端口绑定’这个功能分为两个动作:1-操作端口绑定、2-绑定事件进行广播

上述1~6的步骤都是将Netty将设置的端口通过JDK底层方法进行绑定,在绑定完成后是通过后续事件广播功能将这一事件广播出去


//端口绑定完成以后isActive()返回true
if (!wasActive && isActive()) {
    invokeLater(new Runnable() {
        @Override
        public void run() {
            //广播事件 @TODO
            pipeline.fireChannelActive();
        }
    });
}

pipeline.fireChannelActive()方法会调用到HeadContext#channelActive

小结

NioEventLoop组件

  1. 默认情况下,Netty服务端会启动多少线程?什么时候启动?
  2. Netty如何解决JDK空轮询问题?
  3. Netty如何保证异步串行无锁化?

NioEventLoop创建过程

创建核心线程数

在我们的示例代码中可以看到

val bossGroup = NioEventLoopGroup(1)
val workerGroup = NioEventLoopGroup()

NioEventLoopGroup有两种构造方法,一种传参,一种不传参数,进入构造方法可以看到:

//设置线程数量
public NioEventLoopGroup(int nThreads) {
    this(nThreads, (Executor) null);
}

//不设置线程数量时
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
    super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

    private static final int DEFAULT_EVENT_LOOP_THREADS;

//设置线程数量为配置参数值或默认当前核心数*2
static {
    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);
    }
}

创建线程执行器

NioEventLoop启动过程

NioEventLoop执行逻辑

  1. 业务逻辑:对数据包进行拆包 -> 数据类型处理器

参考资料

Socket通信模型
Beej’s Guide to Network Programming简体中文
Netty中的策略模式


  TOC