用户程序进行IO的读写,基本上会用到系统调用read&write
- read 把数据从内核缓冲区复制到进程缓冲区
- write把 数据从进程缓冲区复制到内核缓冲区,它们不等价于数据在内核缓冲区和磁盘之间的交换。
- (1)客户端请求:Linux通过网卡,读取客户断的请求数据,将数据读取到内核缓冲区。
- (2)获取请求数据:服务器从内核缓冲区读取数据到Java进程缓冲区。
- (3)服务器端业务处理:Java服务端在自己的用户空间中,处理客户端的请求。
- (4)服务器端返回数据:Java服务端已构建好的响应,从用户缓冲区写入系统缓冲区。
- (5)发送给客户端:Linux内核通过网络 I/O ,将内核缓冲区中的数据,写入网卡,网卡通过底层的通讯协议,会将数据发送给目标客户端。
- 同步阻塞IO, BIO(Block-IO):数据的读取写入必须阻塞在一个线程内等待其完成。
这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。实际上线程在等待水壶烧开的时间段什么都没有做。 保持一个连接,不能做其他事。 - 同步非阻塞IO,non-blocking-IO,
同时支持阻塞与非阻塞模式。同步非阻塞 IO 的定义为,如果还拿烧开水来说,NIO的做法是叫 一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。 可以保持多个连接,不能做其他事。 - 异步非阻塞I/O模型: AIO
异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。 可以保持多个连接,可以做其他事。
以socket.read()为例子:
- 传统的BIO里面socket.read(),如果 TCP RecvBuffe r里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
- 对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
- 最新的 AIO(Async I/O) 里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
- IO 面向流的意思是,必须用一个持久化来接受。流是一种数据存储的方式,数据流必须有文件来接;而NIO 使用缓冲区,可以将数据存储的缓冲区中,然后选择行读或者一个一个字段,效率跟高;
- NIO 由于会出现轮询所有水壶的操作,因此需要一个多路选择器。
- 同步: 发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生。
异步: 发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发。
- 阻塞:
- 传统的IO流都是阻塞式的。也就是说,当一个线程调用read()或者write()方法时,该线程将被阻塞,直到有一些数据读取或者被写入,在此期间,该线程不能执行其他任何任务。
- 在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量的客户端时,性能急剧下降。
- 非阻塞:
- JavaNIO 是非阻塞式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程会去执行其他任务。
- 线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
- 因此 NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
可以将是否同步与是否阻塞理解为横向维度与纵向维度
缓冲区 Buffer- Buffer 是一个对象。它包含一些要写入或者读出的数据。在面向流的 I/O 中,可以将数据写入或者将数据直接读到Stream 对象中。
- 在NIO中,所有的数据都是用缓冲区处理。这也就本文上面谈到的 IO 是面向流的,NIO 是面向缓冲区的。
缓冲区实质是一个数组,通常它是一个字节数组( ByteBuffer),也可以使用其他类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的 结构化访问以及维护读写位置(limit) 等信息。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean)都对应一种缓冲区,具体如下:
- ByteBuffer:字节缓冲区
- CharBuffer: 字符缓冲区
- ShortBuffer:短整型缓冲区
- IntBuffer:整型缓冲区
- LongBuffer: 长整型缓冲区
- FloatBuffer:浮点型缓冲区
- DoubleBuffer:双精度浮点型缓冲区
- Channel 是一个通道,可以通过它读取和写入数据,他就像自来水管一样,网络数据通过 Channel 读取和写入。
- 通道和流不同之处在于通道是双向的,流只是在一个方向移动,而且通道可以用于读,写或者同时用于读写。
因为Channel是全双工的,所以它比流更好地映射底层操作系统的API,特别是在UNIX网络编程中,底层操作系统的通道都是全双工的,同时支持读和写。
Channel 有四种实现:
- FileChannel: 是从文件中读取数据。
- DatagramChannel: 从 UDP 网络中读取或者写入数据。
- SocketChannel :从 TCP 网络中读取或者写入数据。
- ServerSocketChannel:允许你监听来自TCP的连接,就像服务器一样。每一个连接都会有一个SocketChannel产生。
Selector 选择器可以监听多个Channel(比如 read、write、accep、connect),实现一个线程管理多个Channel,节省线程切换上下文的资源消耗。
- Selector 只能管理非阻塞的通道,FileChannel 是阻塞的,无法管理。
关键对象
- Selector:选择器对象,通道注册、通道监听对象和 Selector 相关。
- SelectorKey:通道监听关键字,通过它来监听通道状态。
监听注册
- 监听注册在 Selector: socketChannel.register(selector, SelectionKey.OP_READ);
监听的事件有
- OP_ACCEPT: 接收就绪,serviceSocketChannel 使用的
- OP_READ: 读取就绪,socketChannel使用
- OP_WRITE: 写入就绪,socketChannel使用
- OP_CONNECT: 连接就绪,socketChannel使用
定义: Java NIO 成功的应用在了各种分布式、即时通信和中间件 Java 系统中,充分的证明了基于 NIO 构建的通信基础,是一种高效,且扩展性很强的通信架构。例如:Dubbo(服务框架),就默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
java NIO的工作原理:
- 由一个专门的线程来处理所有的 IO 事件,并负责分发。
- 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
- 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。
代码演示(注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。)
BIO
-
传统IO,即BIO,同步阻塞处理的模型如下:
{ 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()....//写数据 } } } } -
上面模拟之所以使用多线程,主要原因在于 socket.accept()、socket.read()、socket.write() 三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质: 1. 利用多核。 2. 当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。
-
线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
-
这个模型最本质的问题在于,严重依赖于线程。但线程是很 ”贵” 的资源,主要表现在:
- 线程的创建和销毁成本很高,在 Linux 这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
- 线程本身占用较大内存,像 Java 的线程栈,一般至少分配 512K~1M 的空间,如果系统中的线程数过千,恐怕整个JVM 的内存都会被吃掉一半。
- 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。
- 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或 CPU 核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
NIO
-
BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能”傻等”。
-
如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在 Selector 上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。
-
NIO 的特点:应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。
-
NIO的优点: 每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
-
NIO的缺点:需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。
-
总之,NIO模型在高并发场景下,也是不可用的。
一般 Web 服务器不使用这种 IO 模型。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。java的实际开发中,也不会涉及这种IO模型。 -
再次说明,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一种模型,叫做IO多路复用模型( IO multiplexing )。
如何避免同步非阻塞NIO模型中轮询等待的问题呢?这就是IO多路复用模型。
-
IO多路复用模型,就是通过一种新的系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核kernel 能够通知程序进行相应的IO系统调用。
-
目前支持 IO 多路复用的系统调用,有 select,epoll 等等。
- select 系统调用,是目前几乎在所有的操作系统上都有支持,具有良好跨平台特性。
- epoll 是在linux 2.6内核中提出的,是select系统调用的linux增强版本。
-
IO 多路复用模型的基本原理就是 select/epoll 系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。
-
因此,好处也就显而易见了——通过一次select/epoll系统调用,就查询到到可以读写的一个甚至是成百上千的网络连接。
-
代码如下:
interface ChannelHandler{ void channelReadable(Channel channel); void channelWritable(Channel channel); } class Channel{ Socket socket; Event event;//读,写或者连接 } //IO线程主循环: class IoThread extends Thread{ public void run(){ Channel channel; while(channel=Selector.select()){//选择就绪的事件和对应的连接 if(channel.event==accept){ registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器 } if(channel.event==write){ getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件 } if(channel.event==read){ getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件 } } } MaphandlerMap;//所有channel的对应事件处理器 } 上面代码为简单的,reactor 模式: 注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。
多路复用IO的特点:
- IO多路复用模型,建立在操作系统 kernel 内核能够提供的多路分离系统调用select/epoll 基础之上的。多路复用 IO 需要用到两个系统调用(system call), 一个select/epoll查询调用,一个是IO的读取调用。
- 和 NIO 模型相似,多路复用 IO 需要轮询。负责 select/epoll 查询调用的线程,需要不断的进行 select/epoll 轮询,查找出可以进行 IO 操作的连接。
- 多路复用 IO 模型与前面的NIO模型,是有关系的。对于每一个可以查询的 socket,一般都设置成为 non-blocking 模型。只是这一点,对于用户程序是透明的(不感知)。
多路复用IO的优点:
- 用 select/epoll 的优势在于,它可以同时处理成千上万个 connection。
- 与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。
- Java的NIO(new IO)技术,使用的就是 IO多路复用模型。在linux系统上,使用的是epoll系统调用。
多路复用 IO 的缺点:
- 本质上,select/epoll 系统调用,属于同步 IO,也是阻塞 IO。都需要在读写事件就绪后,自己负责进行读写,也就是说这个读写过程是阻塞的。
本文所说的AIO特指Java环境下的AIO。
- AIO是java中IO模型的一种,作为 NIO的改进和增强随JDK1.7版本更新被集成在JDK的nio包中,因此AIO也被称作是NIO2.0。
- 区别于传统的BIO(Blocking IO,同步阻塞式模型,JDK1.4之前就存在于JDK中,NIO于JDK1.4版本发布更新)的阻塞式读写,AIO提供了从建立连接到读、写的全异步操作。
- AIO 可用于异步的文件读写和网络通信。
异步 IO 则是采用 “订阅-通知”模式 : 即应用程序向操作系统注册 IO 监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数:
代码示例-
首先以Server端为例,需要创建一个AsynchronousServerSocketChannel 示例并绑定监听端口,接着开始监听客户端连接:
public class SimpleAIOServer { public static void main(String[] args) { try { final int port = 5555; //首先打开一个ServerSocket通道并获取AsynchronousServerSocketChannel实例: AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(); //绑定需要监听的端口到serverSocketChannel: serverSocketChannel.bind(new InetSocketAddress(port)); //实现一个CompletionHandler回调接口handler, //之后需要在handler的实现中处理连接请求和监听下一个连接、数据收发,以及通信异常。 CompletionHandlerhandler = new CompletionHandler () { @Override public void completed(final AsynchronousSocketChannel result, final Object attachment) { // 继续监听下一个连接请求 serverSocketChannel.accept(attachment, this); try { System.out.println("接受了一个连接:" + result.getRemoteAddress() .toString()); // 给客户端发送数据并等待发送完成 result.write(ByteBuffer.wrap("From Server:Hello i am server".getBytes())) .get(); ByteBuffer readBuffer = ByteBuffer.allocate(128); // 阻塞等待客户端接收数据 // result 表示当前接受的客户端的连接会话,与客户端的通信都需要通过该连接会话进行 result.read(readBuffer) .get(); System.out.println(new String(readBuffer.array())); } catch (IOException | InterruptedException | ExecutionException e) { e.printStackTrace(); } } @Override public void failed(final Throwable exc, final Object attachment) { System.out.println("出错了:" + exc.getMessage()); } }; serverSocketChannel.accept(null, handler); // 由于serverSocketChannel.accept(null, handler);是一个异步方法,调用会直接返回, // 为了让子线程能够有时间处理监听客户端的连接会话, // 这里通过让主线程休眠一段时间(当然实际开发一般不会这么做)以确保应用程序不会立即退出。 TimeUnit.MINUTES.sleep(Integer.MAX_VALUE); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } -
客户端:Client
public class SimpleAIOClient { public static void main(String[] args) { try { // 打开一个SocketChannel通道并获取AsynchronousSocketChannel实例 AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); // 连接到服务器并处理连接结果 client.connect(new InetSocketAddress("127.0.0.1", 5555), null, new CompletionHandler() { @Override public void completed(final Void result, final Void attachment) { System.out.println("成功连接到服务器!"); try { // 给服务器发送信息并等待发送完成 client.write(ByteBuffer.wrap("From client:Hello i am client".getBytes())) .get(); ByteBuffer readBuffer = ByteBuffer.allocate(128); // 阻塞等待接收服务端数据 client.read(readBuffer) .get(); System.out.println(new String(readBuffer.array())); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } @Override public void failed(final Throwable exc, final Void attachment) { exc.printStackTrace(); } }); TimeUnit.MINUTES.sleep(Integer.MAX_VALUE); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }



