qiuyadong's Homepage

Netty

2019-06-15

Netty出来之后,由于对Nio的良好封装,被应用于各大高性能框架中!有必要深入研究!

前言

通过对Netty知识的系统总结,画出如下总结图:

接下来将以自己的理解阐述上述内容。

IO演进之路

Java1.4之前,java对IO的支持仅为BIO

  • BIO的问题
    • 没有数据缓存区,IO性能存在问题
    • 没有c++的Channel,只有输入输出流
    • 同步阻塞的方式,导致通讯线程被长时间阻塞
    • 支持的字符集有限,硬件可移植性不好

因为这些问题,高性能服务器开发被C++和C长期占据。

Linux网络IO简介

Linux对所有操作看做对文件的操作,使用fd(file descriptor)表示。对socket的读写类似,对socketfd的操作,它对网络编程提供5中IO模型如下:

  • 阻塞IO模型

从进程空间调用recvfrom,直到数据包到达复制到进程缓存区,或者返回错误,这期间一直处于等待状态。

  • 非阻塞IO模型

当recvfrom从应用层到内核的时候,如果缓冲区没数据返回EWOULDBLOCK错误,非阻塞IO会轮询这个状态,看内核有无数据。这期间可以做其他操作。

  • IO复用模型

Linux采用select/poll顺序扫描fd是否有数据就绪,支持的fd数量有限。之后提出epoll事件驱动代替顺序扫描,提高性能。有fd就绪,立即回调rollback。

  • 信号驱动IO模型

开启套接口信号驱动IO功能,并系统调用sigaction执行一个信号处理函数。当数据准备就绪,为该进程生成SIGIO信号,回调通知应用程序调用recvfrom读取数据,并通知主循环函数处理数据。

  • 异步IO

告知内核启动某个操作,并让内核在整个操作完成后通知我们。与信号驱动主要区别是:信号驱动由内核通知我们何时可以开始一个io操作。异步io由内核通知我们io操作何时已经完成。一个是通知下楼去取快递,一个是快递送货上门。

这里我们谈Netty,就要谈NIO,那NIO核心库多路复用器Selector就是基于epoll的多路复用技术的实现。

IO多路复用技术

IO编程中,需要多个客户接入请求时,可以利用多线程或者io多路复用技术进行处理。io多路复用技术通过把多个io阻塞复用到同一个select的阻塞上,从而使的系统在单线程情况下支撑多个客户请求,系统不用创建新的额外线程,也不用维护这些线程的运行,降低维护工作量,节约系统资源,那么它的主要应用场景:

  • 服务器同时处理多个处于监听状态或者多个连接状态的套接字;
  • 服务器需要同时处理多种网络协议的套接字;

目前支持IO多路复用的系统有:select,pselect,poll,epoll。这里epoll最好用。

Java的IO演进

java1.4之前是bio,java1.4提出了nio。java1.7提出了NIO2.0 其实就是aio。

各种IO

传统BIO

网络编程模型是Client/Server模型,服务器提供位置信息IP+port,客户端通过连接操作向地址发送连接请求,完成三次握手建立连接,双方开始通讯。

针对BIO原理,提出了使用独立的Acceptor线程负责监听客户请求,有客户连接后为每个客户提供一个新线程进行链路处理,处理完成后返回应答给客户端,销毁线程。

该模型最大的问题:随着并发访问量的持续增加,系统的性能急剧下降。直到不能提供服务。线程是虚拟机宝贵的系统资源,导致大量的浪费。

为了改进这种问题,又演进了通过线程池或者消息队列实现1个或多个线程处理N个客户端的模型,底层依然是同步阻塞IO,所以叫做伪异步。

伪异步

伪异步采用线程池实现,避免了为每个请求创建一个独立线程造成的资源耗尽问题,由于底层还是同步阻塞,没有根本解决问题。

到底是什么底层原因呢?

socket的read方法。

当一方请求或者应答比较慢、或者网络传输较慢时,读取输入流的通信线程一方被长时间阻塞。如果对方要60s发送完成,读取一方的IO也将要被阻塞60s,再次期间,其他接入消息只能在消息队列中排队。

outputstream的write方法写输出流时,它将被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。当消息接受放处理缓慢时,将不能来得及从Tcp缓存区读取数据,将导致Tcp windows不断减小,直到为0,双方处于keep-alive状态,发送方不能写入消息,这时候write被无线阻塞,直到windows大于0或者异常。

由于在生产环境无法保证生产环境网络状态和对端的应用程序足够快,如果模型应用程序依赖对方的处理速度,可靠性将非常差。

说到底:简单的优化无法根本解决同步IO导致的通信线程阻塞问题。只要有通信应答时间过长将会导致级联故障。如果有故障服务器阻塞了所有可用的线程,那么,阻塞队列过长,导致拒绝新客户连接,旧客户的请求也无法得到相应。

这个问题NIO将其解决。

NIO

New IO 或者通俗的NON-Block IO。与ServerSocket/Socket对应的ServerSocketChannel/SocketChannel。

  • 基本类库
    • 缓存区BUffer 对数据的读取/写入都是针对缓存区的,缓冲区就是一个数组,除了对数组的访问还有读写位置的访问。方便网络访问。
    • 通道Channel 通过它来读取或者写入数据。通道与流的不同之处,通道是双向的,在读、写的同时读写。通道是全双工的。
    • 多路复用技术Selector Selector会不断的轮询注册在Channel,如果某个Channel上面有新的TCP的连接、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。 一个多路复用器Selcetor可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它没有最大连接句柄1024/2048的限制。只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
  • 服务器端
      1. 打开ServerSocketChannel,用于监听客户端的连接,绑定监听端口,设置连接为非阻塞
      1. 创建Selector线程,创建多路复用器并启动线程
      1. 将ServerSocketChannel注册到多路复用器的Selector上,监听Accept事件
      1. 无限轮询准备就绪的Key,如果是客户端接入请求,完成TCP三次握手,建立物理链路,设置客户端链路为非阻塞
      1. 把新接入的客户端注册到selector上,监听读操作,用于读取客户端发送的网络信息
      1. 异步读取客户端请求消息到缓冲区,对buffer进行编解码,如果半包消息指针reset继续读取后续保温,将解码成功的消息封装成Task,投递到业务线程池进行业务逻辑编排
    • 7.将结果转换成buffer,调用SocketChannel的异步writer接口,将消息异步发送到客户端。
  • 客户端
    • 1.打开SocketChannel绑定客户端本地地址,设置SocketChannel为非阻塞模式,同时设置客户端TCP参数
    • 2.异步连接服务端,如果连接成功,则直接注册读状态到多路复用器中,如果当前没有连接成功向多路复用器注册connetc状态为,监听服务器的tcp ACK应答。
    • 3.创建多路复用器并启动线程,无限轮询就绪的key,接受到connect事件进行处理,连接成功将读注册到多路复用器,与服务器类似!…

AIO

NIO2.0引入了新的异步通道的概念,并提高异步文件通道和异步套接字通道的实现。异步通道提高两种方式获取操作结果。

通过Future类来表示异步操作的结果

在执行异步操作的时候传入一个channels

CompletionHandler接口的实现类作为操作完成的回调。

它是真正的异步非阻塞IO,它对unix网络编程中的事件驱动io aio,它不需要多路复用器对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。



Comments