Netty

作者: Java及SpringBoot | 来源:发表于2018-09-10 10:42 被阅读24次

    一、网络编程基础原理

    1 网络编程(Socket)概念

    首先注意,Socket不是Java中独有的概念,而是一个语言无关标准。任何可以实现网络编程的编程语言都有Socket。

    1.1 什么是Socket

    网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

    建立网络通信连接至少要一个端口号。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

    Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

    1.2 Socket连接步骤

    根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。【如果包含数据交互+断开连接,那么一共是五个步骤】

    (1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。

    (2)客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

    (3)连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

    clip_image002.gif

    1.3 Java中的Socket

    在java.net包是网络编程的基础类库。其中ServerSocket和Socket是网络编程的基础类型。ServerSocket是服务端应用类型。Socket是建立连接的类型。当连接建立成功后,服务器和客户端都会有一个Socket对象示例,可以通过这个Socket对象示例,完成会话的所有操作。

    对于一个完整的网络连接来说,Socket是平等的,没有服务器客户端分级情况。

    2 什么是同步和异步

    同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。

    以银行取款为例:

    同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);

    异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);

    3 什么是阻塞和非阻塞

    阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。

    以银行取款为例:

    阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);

    非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器通知可读写时再继续进行读写,不断循环直到读写完成)

    4 BIO编程

    Blocking IO: 同步阻塞的编程方式。

    BIO编程方式通常是在JDK1.4版本之前常用的编程方式。编程实现过程为:首先在服务端启动一个ServerSocket来监听网络请求,客户端启动Socket发起网络请求,默认情况下ServerSocket回建立一个线程来处理此请求,如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝。

    且建立好的连接,在通讯过程中,是同步的。在并发处理效率上比较低。大致结构如下:

    clip_image003.gif

    同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

    BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

    使用线程池机制改善后的BIO模型图如下:

    clip_image004.gif

    5 NIO编程

    Unblocking IO(New IO): 同步非阻塞的编程方式。

    NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题,NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

    NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。

    在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题。

    clip_image005.gif

    同步非阻塞,服务器实现模式为一个请求一个通道,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

    NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程复杂,JDK1.4开始支持。

    Buffer:ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。

    Channel:SocketChannel,ServerSocketChannel。

    Selector:Selector,AbstractSelector

    SelectionKey:OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT

    6 AIO编程

    Asynchronous IO: 异步非阻塞的编程方式

    与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:

    AsynchronousSocketChannel

    AsynchronousServerSocketChannel

    AsynchronousFileChannel

    AsynchronousDatagramChannel

    异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

    AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

    clip_image007.gif

    二、Netty

    1 简介

    Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

    也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。

    “快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议的实现经验,这些协议包括FTP,SMTP,HTTP,各种二进制,文本协议,并经过相当精心设计的项目,最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。

    Netty从4.x版本开始,需要使用JDK1.6及以上版本提供基础支撑。

    在设计上:针对多种传输类型的统一接口 - 阻塞和非阻塞;简单但更强大的线程模型;真正的无连接的数据报套接字支持;链接逻辑支持复用;

    在性能上:比核心 Java API 更好的吞吐量,较低的延时;资源消耗更少,这个得益于共享池和重用;减少内存拷贝

    在健壮性上:消除由于慢,快,或重载连接产生的 OutOfMemoryError;消除经常发现在 NIO 在高速网络中的应用中的不公平的读/写比

    在安全上:完整的 SSL / TLS 和 StartTLS 的支持

    且已得到大量商业应用的真实验证,如:Hadoop项目的Avro(RPC框架)、Dubbo、Dubbox等RPC框架。

    Netty的官网是:http://netty.io

    有三方提供的中文翻译Netty用户手册(官网提供源信息):http://ifeve.com/netty5-user-guide/

    2 Netty架构

    Netty 采用了比较典型的三层网络架构进行设计,逻辑架构图如下所示:


    image
    1. 第一层,Reactor 通信调度层,它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父 类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe以及其衍生出的各种内部类等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据 读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事 件、写事件等等,将这些事件触发到 PipeLine 中,由 PipeLine 充当的职责链来 进行后续的处理。
    2. 第二层,职责链 PipeLine,它负责事件在职责链中的有序传播,同时负责动态的 编排职责链,职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向 后/向前传播事件,不同的应用的 Handler 节点的功能也不同,通常情况下,往往 会开发编解码 Hanlder 用于消息的编解码,它可以将外部的协议消息转换成内部 的 POJO 对象,这样上层业务侧只需要关心处理业务逻辑即可,不需要感知底层 的协议差异和线程模型差异,实现了架构层面的分层隔离。
    3. 第三层,业务逻辑处理层。可以分为两类:
    4. 纯粹的业务逻辑 处理,例如订单处理。
    5. 应用层协议管理,例如HTTP协议、FTP协议等。

    接下来,我从影响通信性能的三个方面(I/O模型、线程调度模型、序列化方式)来谈谈Netty的架构。

    3 线程模型

    clip_image011.gif

    Netty中支持单线程模型,多线程模型,主从多线程模型。

    在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。

    为了尽可能提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

    3.1 Reactor单线程模型

    在ServerBootstrap调用方法group的时候,传递的参数是同一个线程组,且在构造线程组的时候,构造参数为1,这种开发方式,就是一个单线程模型。

    个人机开发测试使用。不推荐。

    3.2 Reactor多线程模型

    在ServerBootstrap调用方法group的时候,传递的参数是两个不同的线程组。负责监听的acceptor线程组,线程数为1,也就是构造参数为1。负责处理客户端任务的线程组,线程数大于1,也就是构造参数大于1。这种开发方式,就是多线程模型。

    长连接,且客户端数量较少,连接持续时间较长情况下使用。如:企业内部交流应用。

    3.3 Reactor主从多线程模型

    在ServerBootstrap调用方法group的时候,传递的参数是两个不同的线程组。负责监听的acceptor线程组,线程数大于1,也就是构造参数大于1。负责处理客户端任务的线程组,线程数大于1,也就是构造参数大于1。这种开发方式,就是主从多线程模型。

    长连接,客户端数量相对较多,连接持续时间比较长的情况下使用。如:对外提供服务的相册服务器。

    4 基础程序演示

    4.1 入门案例

    TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

    出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

    粘包情况有两种:

    • 粘在一起的包都是完整的数据包;
    • 粘在一起的包有不完整的包。

    解决粘连包的方法大致分为如下三种:

    1. 发送方开启TCP_NODELAY;
    2. 接收方简化或者优化流程尽可能快的接收数据;
    3. 认为强制分包每次只读一个完整的包。

    对于以上三种方式,第一种会加重网络负担,第二种治标不治本,第三种算比较合理的。
    第三种又可以分两种方式:

    • 每次都只读取一个完整的包,不如不足一个完整的包,就等下次再接收,如果缓冲区有N个包要接受,那么需要分N次才能接收完成;
    • 有多少接收多少,将接収的数据缓存在一个临时的缓存中,交由后续的专门解码的线程/进程处理。

    以上两种分包方式,如果强制关闭程序,数据会存在丢失,第一种数据丢失在接收缓冲区;第二种丢失在程序自身缓存。

    Netty自带的几种粘连包解决方案:

    1. DelimiterBasedFrameDecoder
      1. FixedLengthFrameDecoder
      2. LengthFieldBasedFrameDecoder

    4.2 拆包粘包问题解决

    netty使用tcp/ip协议传输数据。而tcp/ip协议是类似水流一样的数据传输方式。多次访问的时候有可能出现数据粘包的问题,解决这种问题的方式如下:

    4.2.1 定长数据流

    客户端和服务器,提前协调好,每个消息长度固定。(如:长度10)。如果客户端或服务器写出的数据不足10,则使用空白字符补足(如:使用空格)。

    4.2.2 特殊结束符

    客户端和服务器,协商定义一个特殊的分隔符号,分隔符号长度自定义。如:‘#’、‘_’、‘AA@’。在通讯的时候,只要没有发送分隔符号,则代表一条数据没有结束。

    4.2.3 协议

    相对最成熟的数据传递方式。有服务器的开发者提供一个固定格式的协议标准。客户端和服务器发送数据和接受数据的时候,都依据协议制定和解析消息。

    4.3 序列化对象

    JBoss Marshalling序列化

    Java是面向对象的开发语言。传递的数据如果是Java对象,应该是最方便且可靠。

    影响序列化性能的关键因素总结如下:

    • 序列化后的码流大小(网络带宽占用);
    • 序列化&反序列化的性能(CPU资源占用);
    • 并发调用的性能表现:稳定性、线性增长、偶现的时延毛刺等;

    Netty默认提供了对Google Protobuf的支持,通过扩展Netty的编解码接口,用户可以实现其它的高性能序列化框架,例如Thrift的压缩二进制编解码框架。

    不同的应用场景对序列化框架的需求也不同,对于高性能应用场景Netty默认提供了Google的Protobuf二进制序列化框架,如果用户对其它二进制序列化框架有需求,也可以基于Netty提供的编解码框架扩展实现。

    4.4 定时断线重连

    客户端断线重连机制。

    客户端数量多,且需要传递的数据量级较大。可以周期性的发送数据的时候,使用。要求对数据的即时性不高的时候,才可使用。

    优点: 可以使用数据缓存。不是每条数据进行一次数据交互。可以定时回收资源,对资源利用率高。相对来说,即时性可以通过其他方式保证。如: 120秒自动断线。数据变化1000次请求服务器一次。300秒中自动发送不足1000次的变化数据。

    对于长连接的程序断网重连几乎是程序的标配。断网重连具体可以分为两类:

    1. CONNECT失败,需要重连;

      实现ChannelFutureListener 用来启动时监测是否连接成功,不成功的话重试

      实现ChannelFutureListener 用来启动时监测是否连接成功,不成功的话重试
      private void doConnect() {
          Bootstrap b = ...;
          b.connect().addListener((ChannelFuture f) -> {
              if (!f.isSuccess()) {
                  long nextRetryDelay = nextRetryDelay(...);
                  f.channel().eventLoop().schedule(nextRetryDelay, ..., () -> {
                      doConnect();
                  }); // or you can give up at some point by just doing nothing.
              }
          });
      }
      或者
      public class ConnectionListener implements ChannelFutureListener {
        private Client client;
        public ConnectionListener(Client client) {
          this.client = client;
        }
        @Override
        public void operationComplete(ChannelFuture channelFuture) throws Exception {
          if (!channelFuture.isSuccess()) {
            System.out.println("Reconnect");
            //因为是建立网络连接所以可以共用EventLoop
            final EventLoop loop = channelFuture.channel().eventLoop();
            loop.schedule(new Runnable() {
              @Override
              public void run() {
                client.createBootstrap(new Bootstrap(), loop);
              }
            }, 1L, TimeUnit.SECONDS);
          }
        }
      }
      
    2. 程序运行过程中断网、远程强制关闭连接、收到错误包必须重连;

      public Bootstrap createBootstrap(Bootstrap bootstrap, EventLoopGroup eventLoop) {
           if (bootstrap != null) {
             final MyInboundHandler handler = new MyInboundHandler(this);
             bootstrap.group(eventLoop);
             bootstrap.channel(NioSocketChannel.class);
             bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
             bootstrap.handler(new ChannelInitializer<SocketChannel>() {
               @Override
               protected void initChannel(SocketChannel socketChannel) throws Exception {
                 socketChannel.pipeline().addLast(handler);
               }
             });
             bootstrap.remoteAddress("localhost", 8888);
             bootstrap.connect().addListener(new ConnectionListener(this));
           }
           return bootstrap;
         }
      
      public class MyInboundHandler extends SimpleChannelInboundHandler {
         private Client client;
         public MyInboundHandler(Client client) {
           this.client = client;
         }
         @Override
         public void channelInactive(ChannelHandlerContext ctx) throws Exception {
           final EventLoop eventLoop = ctx.channel().eventLoop();
           eventLoop.schedule(new Runnable() {
             @Override
             public void run() {
               client.createBootstrap(new Bootstrap(), eventLoop);
             }
           }, 1L, TimeUnit.SECONDS);
           super.channelInactive(ctx);
         }
       }
      

    4.5 心跳监测

    使用定时发送消息的方式,实现硬件检测,达到心态检测的目的。

    心跳监测是用于检测电脑硬件和软件信息的一种技术。如:CPU使用率,磁盘使用率,内存使用率,进程情况,线程情况等。

    Netty提供的心跳检测机制分为三种:

    • 读空闲,链路持续时间t没有读取到任何消息;
    • 写空闲,链路持续时间t没有发送任何消息;
    • 读写空闲,链路持续时间t没有接收或者发送任何消息。

    心跳检测机制分为三个层面:

    • TCP层面的心跳检测,即TCP的Keep-Alive机制,它的作用域是整个TCP协议栈
    • 协议层的心跳检测,主要存在于长连接协议中。例如SMPP协议;
    • 应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。
    4.5.1 sigar

    需要下载一个zip压缩包。内部包含若干sigar需要的操作系统文件。sigar插件是通过JVM访问操作系统,读取计算机硬件的一个插件库。读取计算机硬件过程中,必须由操作系统提供硬件信息。硬件信息是通过操作系统提供的。zip压缩包中是sigar编写的操作系统文件,如:windows中的动态链接库文件。

    解压需要的操作系统文件,将操作系统文件赋值到${Java_home}/bin目录中。

    4.6 HTTP协议处理

    使用Netty服务开发。实现HTTP协议处理逻辑。

    5 流数据的传输处理

    在基于流的传输里比如TCP/IP,接收到的数据会先被存储到一个socket接收缓冲里。不幸的是,基于流的传输并不是一个数据包队列,而是一个字节队列。即使你发送了2个独立的数据包,操作系统也不会作为2个消息处理而仅仅是作为一连串的字节而言。因此这是不能保证你远程写入的数据就会准确地读取。所以一个接收方不管他是客户端还是服务端,都应该把接收到的数据整理成一个或者多个更有意思并且能够让程序的业务逻辑更好理解的数据。

    在处理流数据粘包拆包时,可以使用下述处理方式:

    使用定长数据处理,如:每个完整请求数据长度为8字节等。(FixedLengthFrameDecoder)

    使用特殊分隔符的方式处理,如:每个完整请求数据末尾使用’\0’作为数据结束标记。(DelimiterBasedFrameDecoder)

    使用自定义协议方式处理,如:http协议格式等。

    使用POJO来替代传递的流数据,如:每个完整的请求数据都是一个RequestMessage对象,在Java语言中,使用POJO更符合语种特性,推荐使用。

    客户端和服务端之间连接断开机制

    TCP连接的建立需要三个分节(三次握手),终止则需要四个分节。

    1. 某个应用进程首先调用close,称该端执行主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕
    2. 接收到这个FIN分节的对端执行被动关闭(passive close) 。这个FIN由TCP确认。他的接收也作为一个文件结束符传递给接收端应用程序,因为FIN的接收意味着接收端应用程序在相应连接上再无额外数据可以收取;
    3. 一段时间后,接收到这个文件结束符的应用程序调用close管理他的套接字。这导致他的TCP也发送一个FIN。
    4. 接收这个最终FIN的原发送端TCP(执行主动关闭的那一端)确认这个FIN。

    既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。但是某些情况下步骤1的FIN随数据一起发送;另外,步骤2和步骤3发送的分节都处在执行被动关闭的那一端,有可能合并成一个分节发送。


    image

    TCP关闭连接时对应的状态图如下:


    image

    对于大量短连接的情况下,经常出现卡在FIN_WAIT2和TIMEWAIT状态的连接,等待系统回收,但是操作系统底层回收的时间频率很长,导致SOCKET被耗尽。解决方案如下:

    Linux平台:

        #!/bin/sh
    
        echo "Now,config system parameters..."
    
        echo "#config for MWGATE" >> /etc/sysctl.conf
        #当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击
        echo 'net.ipv4.tcp_syncookies = 1' >> /etc/sysctl.conf
        #允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
        echo 'net.ipv4.tcp_tw_reuse = 1' >> /etc/sysctl.conf
        #TIM_WAIT_2也就是FIN_SYN_2的等待时间
        echo 'net.ipv4.tcp_fin_timeout = 30' >> /etc/sysctl.conf
        #表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
        echo 'net.ipv4.tcp_tw_recycle = 1' >>/etc/sysctl.conf
        FILEMAX=$(cat /proc/sys/fs/file-max)
        PORTRANGE="net.ipv4.ip_local_port_range = 1024 $FILEMAX"
        echo $PORTRANGE >> /etc/sysctl.conf
        echo "#end config for MWGATE" >> /etc/sysctl.conf
    
        echo '#config for MWGATE' >> /etc/security/limits.conf
        SOFTLIMIT="* soft nofile $FILEMAX"
        HARDLIMIT="* hard nofile $FILEMAX"
        echo "$SOFTLIMIT" >> /etc/security/limits.conf
        echo "$HARDLIMIT" >> /etc/security/limits.conf
        echo '#end config for MWGATE' >> /etc/security/limits.conf
    
        echo "#config for MWGATE" >> /etc/pam.d/login
        echo 'session required /lib/security/pam_limits.so' >> /etc/pam.d/login
        echo "#end config for MWGATE" >> /etc/pam.d/login
    
        sysctl -p
    
        echo "#config for MWGATE" >> /etc/profile
        echo 'ulimit -S -c unlimited > /dev/null 2>&1' >> /etc/profile
        echo "#end config for MWGATE" >> /etc/profile
    
        echo 'core-%p-%t' >> /proc/sys/kernel/core_pattern
    
        source /etc/profile
    
        echo "Config is OK..."
    

    Windows平台:

        Windows Registry Editor Version 5.00
        [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
        "MaxUserPort"=dword:0000fffe
        "TCPTimedWaitDelay"=dword:00000005
    

    TCP状态图

    TCP状态转化图

    TCP/IP半关闭

    从上述讲的TCP关闭的四个分节可以看出,被动关闭执行方,发送FIN分节的前提是TCP套接字对应应用程序调用close产生的。如果服务端有数据发送给客户端那么可能存在服务端在接受到FIN之后,需要将数据发送到客户端才能发送FIN字节。这种处于业务考虑的情形通常称为半关闭。

    半关闭可能导致大量socket处于CLOSE_WAIT状态

    谁负责关闭连接合理

    连接关闭触发的条件通常分为如下几种:

    1. 数据发送完成(发送到对端并且收到响应),关闭连接;
    2. 通信过程中产生异常;
    3. 特殊指令强制要求关闭连接;

    对于第一种,通常关闭时机是,数据发送完成方发起(客户端触发居多);
    对于第二种,异常产生方触发(例如残包、错误数据等)发起。但是此种情况可能也导致压根无法发送FIN。
    对于第三种,通常是用于运维等。由命令发起方产生。

    Future-Listener机制

    Future在netty中位于io.netty.concurrent包中,其依赖关系如下:


    image

    在并发编程中,我们通常会用到一组非阻塞的模型:Promise,Future,Callback。其中的Future表示一个可能还没有实际完成的异步任务的结果,针对这个结果添加Callback以便在执行任务成功或者失败后做出响应的操作。
    而经由Promise交给执行者,任务执行者通过Promise可以标记任务完成或者失败。以上这套模型是很多异步非阻塞框架的基础。
    具体的理解可参见JDK的FutureTask和Callable。JDK的实现版本,在获取最终结果的时候,不得不做一些阻塞的方法等待最终结果的到来。
    Netty的Future机制是JDK机制的一个子版本,它支持给Future添加Listener,以方便EventLoop在任务调度完成之后调用。

    Future提供了一套高效便捷的非阻塞并行操作管理方案。其基本思想很简单,所谓Future,指的是一类占位符对象,用于指代某些尚未完成的计算的结果。一般来说,由Future指代的计算都是并行执行的,计算完毕后可另行获取相关计算结果。以这种方式组织并行任务,便可以写出高效、异步、非阻塞的并行代码。

    默认情况下,future和promise并不采用一般的阻塞操作,而是依赖回调进行非阻塞操作。为了在语法和概念层面更加简明扼要地使用这些回调。当然,future仍然支持阻塞操作——必要时,可以阻塞(sync、await、awaitUninterruptibly)等待future(不过并不鼓励这样做)。

    Future

    所谓Future,是一种用于指代某个尚未就绪的值的对象。而这个值,往往是某个计算过程的结果:

    • 若该计算过程尚未完成,我们就说该Future未就位;
    • 若该计算过程正常结束,或中途抛出异常,我们就说该Future已就位。

    Future的就位分为两种情况:

    • 当Future带着某个值就位时,我们就说该Future携带计算结果成功就位。
    • 当Future因对应计算过程抛出异常而就绪,我们就说这个Future因该异常而失败。

    Future的一个重要属性在于它只能被赋值一次。一旦给定了某个值或某个异常,future对象就变成了不可变对象——无法再被改写。

    Callbacks(回调函数)

    Callback用于对计算的最终结果Future做一些后续的处理,以便我们能够用它来做一些有用的事。我们经常对计算结果感兴趣而不仅仅是它的副作用。

    Promises

    如果说futures是为了一个还没有存在的结果,而当成一种只读占位符的对象类型去创建,那么Promise就被认为是一个可写的,可以实现一个Future的单一赋值容器。这就是说,promise通过这种success方法可以成功去实现一个带有值的future。相反的,因为一个失败的promise通过failure方法就会实现一个带有异常的future。

    Future和Promise的区别

    Promise与Future的区别在于,Future是Promise的一个只读的视图,也就是说Future没有设置任务结果的方法,只能获取任务执行结果或者为Future添加回调函数。

    流量整形

    流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。Netty的流量整形有两个作用:

    • 防止由于上下游网元性能不均衡导致下游网元被压垮,业务流程中断;
    • 防止由于通信模块接收消息过快,后端业务线程处理不及时导致的“撑死”问题。

    流量整形的原理示意图如下:

    image

    流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。流量整形与流量监管的主要区别在于,流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。

    Netty支持两种流量整形模式:

    • 全局流量整形:全局流量整形的作用范围是进程级的,无论你创建了多少个Channel,它的作用域针对所有的Channel。用户可以通过参数设置:报文的接收速率、报文的发送速率、整形周期。[GlobalChannelTrafficShapingHandler]
    • 链路级流量整形:单链路流量整形与全局流量整形的最大区别就是它以单个链路为作用域,可以对不同的链路设置不同的整形策略。[ChannelTrafficShapingHandler针对于每个channel]

    优雅停机

    Netty的优雅停机三部曲:

    1. 不再接收新消息
    2. 退出前的预处理操作
    3. 资源的释放操作
    image

    Java的优雅停机通常通过注册JDK的ShutdownHook来实现,当系统接收到退出指令后,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行。

    通常优雅退出需要有超时控制机制,例如30S,如果到达超时时间仍然没有完成退出前的资源回收等操作,则由停机脚本直接调用kill -9 pid,强制退出。

    在实际项目中,Netty作为高性能的异步NIO通信框架,往往用作基础通信框架负责各种协议的接入、解析和调度等,例如在RPC和分布式服务框架中,往往会使用Netty作为内部私有协议的基础通信框架。
    当应用进程优雅退出时,作为通信框架的Netty也需要优雅退出,主要原因如下:

    • 尽快的释放NIO线程、句柄等资源;
    • 如果使用flush做批量消息发送,需要将积攒在发送队列中的待发送消息发送完成;
    • 正在write或者read的消息,需要继续处理;
    • 设置在NioEventLoop线程调度器中的定时任务,需要执行或者清理

    Netty架构剖析之安全性

    Netty面临的安全挑战:

    • 对第三方开放
    • 作为应用层协议的基础通信框架
    image

    安全威胁场景分析:

    • 对第三方开放的通信框架:如果使用Netty做RPC框架或者私有协议栈,RPC框架面向非授信的第三方开放,例如将内部的一些能力通过服务对外开放出去,此时就需要进行安全认证,如果开放的是公网IP,对于安全性要求非常高的一些服务,例如在线支付、订购等,需要通过SSL/TLS进行通信。
    • 应用层协议的安全性:作为高性能、异步事件驱动的NIO框架,Netty非常适合构建上层的应用层协议。由于绝大多数应用层协议都是公有的,这意味着底层的Netty需要向上层提供通信层的安全传输功能。

    SSL/TLS

    Netty安全传输特性:

    • 支持SSL V2和V3
    • 支持TLS
    • 支持SSL单向认证、双向认证和第三方CA认证。

    Netty通过SslHandler提供了对SSL的支持,它支持的SSL协议类型包括:SSL V2、SSL V3和TLS。

    • 单向认证:单向认证,即客户端只验证服务端的合法性,服务端不验证客户端。
    • 双向认证:与单向认证不同的是服务端也需要对客户端进行安全认证。这就意味着客户端的自签名证书也需要导入到服务端的数字证书仓库中。
    • CA认证:基于自签名的SSL双向认证,只要客户端或者服务端修改了密钥和证书,就需要重新进行签名和证书交换,这种调试和维护工作量是非常大的。因此,在实际的商用系统中往往会使用第三方CA证书颁发机构进行签名和验证。我们的浏览器就保存了几个常用的CA_ROOT。每次连接到网站时只要这个网站的证书是经过这些CA_ROOT签名过的。就可以通过验证了。

    可扩展的安全特性

    通过Netty的扩展特性,可以自定义安全策略:

    • IP地址黑名单机制
    • 接入认证
    • 敏感信息加密或者过滤机制

    IP地址黑名单是比较常用的弱安全保护策略,它的特点就是服务端在与客户端通信的过程中,对客户端的IP地址进行校验,如果发现对方IP在黑名单列表中,则拒绝与其通信,关闭链路。

    接入认证策略非常多,通常是较强的安全认证策略,例如基于用户名+密码的认证,认证内容往往采用加密的方式,例如Base64+AES等。

    Netty架构剖析之扩展性

    通过Netty的扩展特性,可以自定义安全策略:

    • 线程模型可扩展
    • 序列化方式可扩展
    • 上层协议栈可扩展
    • 提供大量的网络事件切面,方便用户功能扩展

    Netty的架构可扩展性设计理念如下:

    • 判断扩展点,事先预留相关扩展接口,给用户二次定制和扩展使用;
    • 主要功能点都基于接口编程,方便用户定制和扩展。

    数据安全性之滑动窗口协议

    我们假设一个场景,客户端每次请求服务端必须得到服务端的一个响应,由于TCP的数据发送和数据接收是异步的,就存在必须存在一个等待响应的过程。该过程根据实现方式不同可以分为一下几类(部分是错误案例):

    • 每次发送一个数据包,然后进入休眠(sleep)或者阻塞(await)状态,直到响应回来或者超时,整个调用链结束。此场景是典型的一问一答的场景,效率极其低下;
    • 读写分离,写模块只负责写,读模块则负责接收响应,然后做后续的处理。此种场景能尽可能的利用带宽进行读写。但是此场景不坐控速操作可能导致大量报文丢失或者重复发送。
    • 实现类似于Windowed Protocol。此窗口是以上两种方案的折中版,即允许一定数量的批量发送,又能保证数据的完整性。

    相关文章

      网友评论

          本文标题:Netty

          本文链接:https://www.haomeiwen.com/subject/lpojgftx.html