BIO NIO AIO

BIO NIO AIO

BIO

传统的同步阻塞式I/O模型。通常由一个Acceptor线程负责监听客户端的连接,接受到客户端的连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回给客户端。线程销毁,也就是典型的一请求一应答通信模型。BIO读写是面向流操作的。BIO流是单向的,一个流必须是InputStream或OutputString的子类。

最大的问题就在于缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问量呈1:1的正比关系,由于线程是java虚拟机非常宝贵的系统资源。当线程膨胀之后,系统的性能就会下降,最后导致系统发生线程堆栈溢出,创建新线程失败的问题。

伪异步I/O编程

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化:后端通过一个线程池来处理多个客户端的请求接入。通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

使用线程池和任务队列可以实现伪异步I/O通信框架,当有新的客户端接入时,将客户端的Socket封装成一个Task(该任务实现Runnable接口)投递到后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理。本质还是异步阻塞I/O。

弊端是:但对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到:1. 有数据可读 2. 可用数据读取完毕 3. 发生空指针或者I/O异常。这就意味着当对方发送请求或者应答消息比较缓慢时,读取输入流一方的通信线程将被长时间阻塞。也就是如果对方要60s才能将数据发送完成,那么读取一方I/O线程就会被同步阻塞60s。

学习过TCP/IP的人都知道,但消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP window size不断减小,直到0,双方都处于Keep-alive状态,消息发送方将不能再向TCP缓冲区写入消息,这是如果采用的时同步阻塞I/O,write操作将被无限期阻塞,直到TCP window size大于0或者发送I/O异常。

NIO

官方叫法是New I/O。而被大多数人接受的更准确叫法是非阻塞IO(Non-block I/O)。NIO提供了SocketChannelServerSocketChannel两种不同的套接字通道实现。两种新增的通道都支持阻塞和非阻塞两种模式。一般来说:低负载、低并发的应用程序可以选择同步阻塞I/O来降低编程复杂度。高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。

NIO类库简介

①缓冲区Buffer

缓冲区实质是一个数组,提供对数据的结构化访问以及维护读写位置等。所有数据都是用缓冲区处理,任何时候访问NIO中的数据,都是通过缓冲区进行操作。常见缓冲区类有7个:ByteBuffer(最常用)、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。

②通道Channel

Channel是一个全双工的双向通道,可以读写操作同时进行,能更好的映射底层操作系统的API,因为Unix底层操作系统通道都是全双工的。

Channel可以分为两大类:用于网络多写的SelectableChannel和用于文件操作的FileChannel。ServerSocketChannel和SocketChannel都是SelectableChannel的子类。

③多路复用器Selector

多路复用器提供选择以及就绪的任务的能力。Selector会不断地轮询注册在其上地Channel,如果某个Channel上面发生读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。JDK使用epoll代替了传统的select实现。

NIO编程优缺点

缺点:NIO编程难度比BIO大很大,编码复杂。

优点:

  • 客户端发起的连接操作都是异步的,通过在多路复用器注册OP_CONNECT等待后续结果。

  • SocketChannel的读写操作都是异步的。

  • 线程模型的优化,一个Selector线程可以同时处理成千上万个客户端连接。

浅谈TCP粘包/半包

如果发送区TCP缓冲区满,会导致写半包。此时需要注册监听器听写操作位,循环写,直到整包消息写入TCP缓冲区。

那么什么是粘包/半包呢?

假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:

第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

img

第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

img

第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。此时packet1为半包,packet2为粘包。

img

此时packet2为半包,packet1为粘包。

img

那么如何解决这种问题呢?

我们知道UDP是没有半包粘包的问题的,因为它有边界协议,消息是有格式的。所以我们应用层的解决方案也是在这一思路上展开,解决半包粘包的问题其实就是定义消息边界的问题。

应用层解决方案

长度边界

应用层在发送消息的时候指定每个消息的固定长度,比如固定每个消息为1K,那么当发送时消息不满1K时,用固定的字符串填充,当接收方读取消息的时候,每次也截取1k长度的流作为一个消息l来解析。这种方式的问题在于应用层不能发送超过1K大小的数据,所以使用这种方式的前提知道了消息大小会在哪个范围之内,如果不能确定消息的大小范围不太适合用这种方式,这样会导致大的消息发出去会有问题,小的消息又需要大量的数据填充,不划算。

符号边界

应用层在发送消息前和发送消息后标记一个特殊的标记符,比如&符号,当接收方读取消息时,根据&符号的流码来截取消息的开始和结尾。这种方式的问题在于发送的消息内容里面本身就包含用于切分消息的特殊符号,所以在定义消息切分符时候尽量用特殊的符号组合。

组合边界

这种方式先是定义一个Header+Body格式,Header消息头里面定义了一个开始标记+一个内容的长度,这个内容长度就是Body的实际长度,Body里面是消息内容,当接收方接收到数据流时,先根据消息头里的特殊标记来区分消息的开始,获取到消息头里面的内容长度描述时,再根据内容长度描述来截取Body部分。

至于Netty的处理方式,之后的博客会细谈

AIO

JDK1.7(NIO 2.0)引入了新的异步通道的概念,并提供了异步文件通道异步套接字通道的实现,是真正的异步IO(因此NIO2.0也称作异步非阻塞IO,而NIO 1.0称作非阻塞IO)。其中异步套接字通道是真正的异步非阻塞IO,对应于Unix网络编程中的事件驱动IO(AIO)。它不需要通过多路复用器Selector对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。主要类有AsynchronousSocketChannelCompletionHandler(异步操作回调通知接口)。

四种IO模型对比

img

posted @ 2022-01-13 00:43  会编程的老六  阅读(84)  评论(0编辑  收藏  举报