Netty简介

简单介绍netty

Posted by gregorius on September 10, 2021

导言

大家都知道在不管用什么语言进行网络编程时都会用到socket这个基础组件,我们以java为例,socket的三个基本方法accept,read,write都是同步阻塞的,就是它们之间在调用时需要相互等待,也就是说如果我们的程序用的这些基本的东西,不去考虑其他方式去处理的化那么这样的程序肯定是低效的。如今计算机硬件飞速发展,多核CPU已成大众配置,如果我们用这样的方式写代码完全发挥不了多核CPU的优势,其次,这样的程序能处理的连接和请求是相当有限的,这样的程序对于我们日常生活中对于狂暴流量的追求也是格格不入的。

作为一个freshman来说写这样的程序情有可原,毕竟初来乍到有些东西还不了解,但是对于工作了3-5年的人来说,那么多线程的开发知识是你成为老鸟的必备知识。然而大家都知道多线程的知识体系繁而复杂,如果没有艰苦卓绝的努力和良好的资质是很难驾驭的,多线程的程序不光是要考虑线程变量的同步,同时还要兼顾性能等各方面的问题,稍有不慎就会出现死锁和OOM的问题,而且这些问题都是灾难性的,所以多线程是你升级为架构师的必经之路。

对于工作5年以上的人来说你可能要考虑一些分布式的问题了,如今互联网如此发达,我们日常生活活动基本都是在网上进行,所以分布式编程知识是一个架构师基本必备的知识,也是未来的趋势。分布式的技术的基础框架也离不开基本的网络编程,但是为了支撑超大流量和超大计算能力,我们的代码不是再是简单的BIO的编程模式,在介绍netty之前我们首先来了解一下网络编程必备的同步和异步阻塞模式。

BIO

首先我们回顾一下经典BIO编程模型

10824258-eab2b2a29071e762

以上模式完全是单线程的,没有任何的价值,最多作为我们入门学习的范例使用。

NIO

10824258-d956969585b74bf3

NIO的经典编码(伪代码)

{
 ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池

 ServerSocket serverSocket = new ServerSocket();
 serverSocket.bind(8088);
 while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
 Socket socket = serverSocket.accept();
 executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}

class ConnectIOnHandler extends Thread{
    private Socket socket;
    public ConnectIOnHandler(Socket socket){
       this.socket = socket;
    }
    public void run(){
      while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
          String someThing = socket.read()....//读取数据
          if(someThing!=null){
             ......//处理数据
             socket.write()....//写数据
          }

      }
    }
}

以上代码请求并发比较小的需求BIO是完全可以胜任的,但是如果请求过多,那么线程过多将是灾难性的,首先线程会占用过多的资源,OOM是个大问题,如果没有OOM,其次线程之前频繁进行上下文切换对于CPU的损耗也是相当大,程序完全消耗在线程的上下文切换上了,真正干实事的时间是相当少的。

从上图可以看出NIO是同步非阻塞的,但是我们可以看到,虽然是非阻塞模式,程序需要自己去轮询访问内核是否有数据返回给我们,内核对于程序来说是一个黑盒,虽然对比BIO来说已经有了相当大的改进,但是对于超大流量的处理还是相去甚远的。

IO多路复用

10824258-57307f54f0f712c2

多路复用实现了一个线程处理多个 I/O 句柄的操作。多路指的是多个数据通道,复用指的是使用一个或多个固定线程来处理每一个 Socket。select、poll、epoll 都是 I/O 多路复用的具体实现,线程一次 select 调用可以获取内核态中多个数据通道的数据状态。多路复用解决了同步阻塞 I/O 和同步非阻塞 I/O 的问题,是一种非常高效的 I/O 模型。

信号驱动 I/O

10824258-447fcc5eb1b6b445

信号驱动 I/O 并不常用,它是一种半异步的 I/O 模型。在使用信号驱动 I/O 时,当数据准备就绪后,内核通过发送一个 SIGIO 信号通知应用进程,应用进程就可以开始读取数据了。

异步 I/O(AIO)

10824258-29b174c8b32deb3f

异步 I/O 最重要的一点是从内核缓冲区拷贝数据到用户态缓冲区的过程也是由系统异步完成,应用进程只需要在指定的数组中引用数据即可。不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。

在 I/O 多路复用的场景下,当有数据处于就绪状态后,需要一个事件分发器(Event Dispather),它负责将读写事件分发给对应的读写事件处理器(Event Handler)。事件分发器有两种设计模式:Reactor 和 Proactor,Reactor 采用同步 I/O, Proactor 采用异步 I/O。Reactor有分为单Reactor和主从Reactor模式。

  • 单Reactor 10824258-abac1391eec56490

  • 主从Reactor 10824258-e3e7f8a77bef265b

在 JDK 1.4 投入使用之前,只有 BIO 一种模式。开发过程相对简单。新来一个连接就会创建一个新的线程处理。随着请求并发度的提升,BIO 很快遇到了性能瓶颈。JDK 1.4 以后开始引入了 NIO 技术,支持 select 和 poll;JDK 1.5 支持了 epoll;JDK 1.7 发布了 NIO2,支持 AIO 模型。Java 在网络领域取得了长足的进步。

既然JDK如此成熟了,那么为什么我们在实际的使用过程中大家都选择的是netty呢?

下面我们就来说说netty的过人之处:

  • 易用性。 我们使用 JDK NIO 编程需要了解很多复杂的概念,比如 Channels、Selectors、Sockets、Buffers 等,编码复杂程度令人发指。相反,Netty 在 NIO 基础上进行了更高层次的封装,屏蔽了 NIO 的复杂性;Netty 封装了更加人性化的 API,统一的 API(阻塞/非阻塞) 大大降低了开发者的上手难度;与此同时,Netty 提供了很多开箱即用的工具,例如常用的行解码器、长度域解码器等,而这些在 JDK NIO 中都需要你自己实现。

  • 稳定性。 Netty 更加可靠稳定,修复和完善了 JDK NIO 较多已知问题,例如臭名昭著的 select 空转导致 CPU 消耗 100%,TCP 断线重连,keep-alive 检测等问题。

  • 可扩展性。 Netty 的可扩展性在很多地方都有体现,这里我主要列举其中的两点:一个是可定制化的线程模型,用户可以通过启动的配置参数选择 Reactor 线程模型;另一个是可扩展的事件驱动模型,将框架层和业务层的关注点分离。大部分情况下,开发者只需要关注 ChannelHandler 的业务逻辑实现。

除了以上基本的改进之外,netty作为一个工业级的生产力工具,它同时暗含了很多“黑科技”,其创作者可谓是呕心沥血,让我们认识到什么是匠人精神,那就是把技术做到极致,做到完美,做到无可挑剔的地方,其编程技巧之高也是让我们非常敬佩的。

  • netty设计了精妙的管道pipeline,它就像一个流水线一样源源不断的处理输入输出数据,和我们目前的流行的大数据处理框架的设计一样,可谓英雄所见略同,要想处理大数据必须采用管道的方式进行设计。

  • netty设计高效的内存管理系统,其借鉴jemalloc的设计思想,利用伙伴算法讲内存分配做到极致,内存分为大,中,小三类,大内存直接内核分配,对于中小内存,netty有自己内存管理机制,大家都知道java负责我们的内存管理,netty为了实现内存管理利用了堆外内存,内存分配是用多线程实现的,线程同时利用缓存机制来重复利用回收的内存,其内存使用多种数据结构多内存进行分级管理,包括满二叉树,数组加链表等。对于内存的回收netty设计了轻量级回收工具Recycler 对象池,可以大大减少jvm内存管理的压力,同时其设计了自己的bytebuf内存读写工具,充分改进了jdk bytebuffer的不足与缺陷,做到内存读写相互不冲突,同时可以做到扩容。对于网络工具来说,零拷贝是所有工具的标配,netty利用CompositeByteBuf独辟蹊径实现零拷贝技术,同时Netty 还使用了 FileRegion 实现文件传输的零拷贝。

  • 在细节方面,netty使用SelectedSelectionKeySet 来对jdk selector的数据结构进行脑洞大开的优化,同时嫌弃ThreadLocal的hashmap的key频繁冲突引起的低效而自创了FastThreadLocal,其利用数组和AtomicInteger 时间换空间实现了O(1)性能,同时其自制的HashedWheelTimer时间轮工具简单而高效,其关键思想是利用环形数组队列管理的链式槽来管理任务,但是对比kafka的时间轮来说虽然没有实现分级,但是netty的任务一般都是短时任务,所以完全是适用的,简单而高效。除了造轮子之外,但是有些好的东西还是可以直接拿过来用的,这点是完全值得我们学习的,不是所有的轮子都需要造,如果自己造的没有人家好还不如不造,所以netty利用jctools工具来实现 Mpsc Queue(多生产者队列),其消费是单一线程的。jctools的代码晦涩难懂,原因是其利用字节填充的方式实现了伪内存共享问题。

在学习一门技术的时候,大部分人都只是停留在会使用的层面,并不知道该技术到底能够解决什么问题,相比同领域的其他技术有什么优缺点。我们刚开始不可能一下看清楚问题的本质,需要不断在学习中思考,积累实践经验,然后慢慢总结自己的见解。一名优秀的技术人可以从技术原理中去了解问题本质,然后找到问题的解决防范,也让结果更有说服力。学会从优秀的开源项目中挖掘技术原理对我们是非常有帮助的,起码在面对问题的时候可以让我们思路更加开阔,处理问题更加得心应手。

从技术的角度来说,我们一定要培养自己多维度的思考习惯,而不是停留在表面,这样永远都进步不了。一个方案、一个问题、一个功能都可能需要考虑到多种因素,如果我们能够把方方面面都考虑得非常细致,那么也会让自己做事更有技术深度、更具备全面性。在工作中,我们经常会得到别人大量的信息,看别人的观点和学习别人的方案,吸收值得学习的地方,再总结出自己的独特的思考。用多个维度去看待问题,有时候别人的观点并不一定是对的。