在上篇《Java IO(2)阻塞式输入输出(BIO)》的末尾谈到了什么是阻塞式输入输出,通过Socket编程对其有了大致了解。现在再重新回顾梳理一下,对于只有一个“客户端”和一个“服务器端”来讲,服务器端需要阻塞式接收客户端的请求,这里的阻塞式表示服务器端的应用代码会被挂起直到客户端有请求过来,在高并发的应用场景有多个客户端发起连接下非阻塞式IO(NIO)是不二之选(且只需要在服务器端使用1个线程来管理,并不需要多个线程来处理多个连接)。在现实情况下,Tomcat、Jetty等很多Web服务器均使用了NIO技术。

  接下来对于非阻塞式输入输出(NIO)的学习以及理解首先从它的三个基础概念讲起。

Channel(通道)

  在NIO中,你需要忘掉“流”这个概念,取而代之的是“通道”。举例在网络应用程序中有多个客户端连接,此时数据传输的概念并不是“流”而“通道”,通道与流最大的不同就是,通道是双向的,而流是单向的(例如InputStream、OutputStream)。

Buffer(缓冲区)

  在NIO中并不是简单的将流的概念替换为了通道,与通道搭配的是缓冲区。在BIO的字节流中并不会使用到缓冲区,而是直接操作文件通过字节方式直接读取,而NIO则不同,它会将通道中的数据读入缓存区,或者将缓存区的数据写入通道。

Selector(选择器)

  如果使用NIO的应用程序中只有一个Channel,选择器则是可以不需要的,而如果有多个Channel,换言之有多个连接时,此时通过选择器,在服务器端的应用程序中就只需要1个线程对多个连接进行管理。

  当然从最开始就说到Channel是双向的,所以在最终图的示例为下图所示:

  下面再重新回到这三个概念,详细解释它们是如何协同工作的。

Channel & Buffer

  通常情况下Channel会和Buffer配合使用,但可以不使用Channel。首先需要明确的是,应用程序不管是从文件(包括网络或者其他什么地方)中读取数据,还是写入数据到文件(包括网络或者其他什么地方)都需要Buffer。

1. 直接将数据写入Buffer,应用程序从Buffer中获取数据

 ByteBuffer buffer = ByteBuffer.allocate(1024);
byte b = 121;
buffer.put(b);
buffer.flip(); //读写转换,由“写模式”转换为“读模式”
System.out.println((char)buffer.get());

  第1行,分配一个1KB大小的Buffer缓冲区,ByteBuffer.allcoate返回HeapByteBuffer实例。

  第3行,向Buffer中写入一个字节。

  第4行,Buffer由“写模式”转换为“读模式”。

  第5行,ByteBuffer.get方法读取Buffer中的数据,并且position索引+1。 在上面的代码中有一个重点——flip方法,这个方法的存在是由于Buffer兼顾了读和写的操作,在ByteBuffer的实现中有三个重要的成员变量需要注意: capacity——Buffer容量 position——索引位置 limit——读时表示最大容量,即limit = capacity;写时表示最后一个数据所在的索引位置。 用图例来说明上面代码的执行过程。

  从上图可以清晰的看到Buffer内部是如何进行读写操作的,其中调用flip方法是很关键且重要的一个步骤,试想如果不调用flip进行读写转换,此时position、limit、capacity的索引位置将会如下图所示。

  此时进行读的操作将会得到一个错误数据(0)。 尽管在讲这个小标题“直接将数据写入Buffer,应用程序从Buffer中获取数据”,但实际上已经简要介绍了Buffer的内部实现原理。

  通过上面的例子可以看到,Channel和Buffer并不一定要在一起,单独使用Buffer也是可以的,但要使用Chnnel那就必须得配合Buffer。

2. 从文件中读取数据写入Buffer,应用程序从Buffer中获取数据

  此时的数据来源是文件,开头提过在NIO中忘掉“流”,记住“通道”。在NIO中可以通过传统的流获取通道。例如从输入流FileInputSteram中调用getChannel,或者从输出流FileOutputStream中调用getChannel,当然还有兼顾输入和输出的RandomAccessFile类从中调用getChannel。

  BIO中首先获取流,NIO中首先获取通道。

 RandomAccessFile file = new RandomAccessFile("/Users/yulinfeng/Documents/Coding/Idea/simplenio/src/main/java/com/demo/test.json", "rw");
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array()));

  看到这段NIO读取文件数据的代码,心中默写传统的BIO是如何读取文件数据的。

 InputStream in = new FileInputStream("/Users/yulinfeng/Documents/Coding/Idea/simplenio/src/main/java/com/demo/test.json");
byte[] bytes = new byte[1024];
in.read(bytes);
System.out.println(new String(bytes));

  展开代码可以看到,基本上如出一辙,在NIO中就是多了Buffer这个媒介来读取数据。

  回到NIO读取文件数据的代码。 第1行,获取文件流。 第2行,获取Channel通道。 第3-6行,创建Buffer缓冲区,并将数据读取从通道读取到缓冲区。 同样还是用图例来说明上面代码的执行过程。

  最后调用ByteBuffer.array方法返回缓冲区中的值,此时并未移动position的数组下标。这个例子结合图例我相信能很清楚地看到NIO是如何从文件中读取数据的,下面这个例子将输出数据到文件。

3. 从应用程序中将数据输出到文件中

  前面都是应用程序从Buffer中获取数据并且用图例的方式了解了它的内部运行原理。本例将把数据通过Buffer写到文件中,当然得记住还需要通过Channel才能写入文件。

 RandomAccessFile file = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\out\\test.json", "rw");
FileChannel channel = file.getChannel();
ByteBuffer buffer = Charset.forName("utf-8").encode("{\"name\": \"Kevin\"}"); //这里会自动进行读写转换,第1个例子需要手动调用flip方法进行读写模式的转换

  通过上面的例子很容易想到,首先需要通道,那么就利用可读可写的RandomAccessFile获取通道;其次需要缓冲区;最后将缓冲区的数据写入到通道中即可。这段代码其实可以把重点放到是如何从缓冲区写到管道的。

  第1-2行,通过可读可写的RandomAccessFile类获取Channel通道。(要是只需要写文件,也可以通过FileOutputStream.getChannel获得)

  第3行,将字符串{“name”: “Kevin”}通过UTF-8编码写入Buffer缓冲区,NIO会对自动对其进行读写模式的转换,不需要手动调用flip方法。

  第4行,将Buffer中的数据写入通道。

4. 从一个文件读数据,再写到另一个文件

  NIO不易掌握,需要反复练习,所以本文会给出多个例子反复操练并领会NIO的设计哲学。

  这个例子有两种实现方式,第一种基于上面的例子就能拼凑出来,第二种则需要掌握一个新的API——transferFrom / transferTo

4.1通过上面的知识读文件再写文件

 RandomAccessFile readFile = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\inout\\from.json", "rw");
FileChannel readChannel = readFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
readChannel.read(buffer);
buffer.flip(); //读写转换
RandomAccessFile writeFile = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\inout\\to.json", "rw");
FileChannel writeChannel = writeFile.getChannel();
writeChannel.write(buffer);

  经过上面的几个例子写出这个示例应该没什么问题,需要注意的是第x行的buffer.flip方法是读写转换,这在上面有提到过。

4.2 通过新的API——transferFrom读文件并写文件

 RandomAccessFile fromFile = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\inout\\from.json", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\inout\\to.json", "rw");
FileChannel toChannel = toFile.getChannel(); toChannel.transferFrom(fromChannel, 0, fromChannel.size());

  通过transferFrom就能将一个通道直接输出到另一个通道而不需要缓冲区做中转。

5. Socket网络应用程序是如何使用NIO的

  前面的例子全是有关本地文件的读写操作,在一个应用程序中有可能免不了通过网络来传输数据,传统的Socket编程利用的是BIO,也就是阻塞式输入输出。而NIO同样也可应用到Socket网络编程中。下面两个例子均是1个客户端对应1个服务器端。此时并不能很好的体会BIO和NIO的区别,若多个客户端对应1个服务器端,此时NIO的优点便很快显现,不过要实现多个客户端对应1个服务器端则需要Selector(选择器),由于现在还并未详细认识它所以将“多个客户端对应1个服务器端”放置在后面提及。

5.1 阻塞式网络编程(BIO Socket)

  BIO Socket是我取的名字,意思是利用传统的阻塞式IO来进行Socket编程,本文虽主讲NIO,但也需要了解并熟练掌握BIO。故,在此先使用传统的IO来进行Socket编程以便能对下文的NIO Socket有一个类比。在本例中使用UDP协议传输数据。

 /**
* BIO客户端
* Created by Kevin on 2017/12/18.
*/
public class Client {
public static void main(String[] args) throws Exception{
String data = "this is Client.";
DatagramSocket socket = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(data.getBytes(), data.getBytes().length, InetAddress.getByName("127.0.0.1"), 8989);
socket.send(packet);
}
}
 /**
* 服务器端
* BIO Created by Kevin on 2017/12/18.
*/
public class Server {
public static void main(String[] args) throws Exception{
DatagramSocket socket = new DatagramSocket(8989);
byte[] data = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, data.length);
socket.receive(packet); //服务器端在未收到数据时,会在此处被阻塞挂起
System.out.println(new String(packet.getData()));
}
}

  这是我们比较熟悉的Socket编程,其中有特点的就是在服务器端的第x行代码,此处若未收到来自客户端的数据,服务器端将会被阻塞。

5.2 非阻塞式网络编程(NIO Socket)

  在通常情况下,对于网络编程用的比较多的还是阻塞式。非阻塞式在应用程序中并不是特别常见,但它在Tomcat等Web服务器中却很常见。这是因为对于非阻塞式的网络编程其最大的优点或者说是最大的使用场景就是面对多个客户端时良好的性能表现。

  此处我们还是在单一的客户端场景下使用非阻塞式网络编程(多个客户端就会使用到Selector选择器,下文会展开)。同样在本例中使用UDP协议传输数据。

 /**
* NIO客户端
* Created by Kevin on 2017/12/18.
*/
public class Client {
public static void main(String[] args) throws Exception{
DatagramChannel channel = DatagramChannel.open(); //类似读取本地文件,首先都需要建立一个通道
ByteBuffer buffer = Charset.forName("utf-8").encode("this is Client."); //其次建立一个缓冲区
channel.send(buffer, new InetSocketAddress("127.0.0.1", 8989));
}
}
 /**
*NIO 服务器端
* Created by Kevin on 2017/12/18.
*/
public class Server {
public static void main(String[] args) throws Exception{
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress("127.0.0.1", 8989));
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.receive(buffer); //服务器端没有收到来自客户端的数据,会在这里和BIO Socket一样被阻塞
System.out.println(new String(buffer.array()));
}
}

  对于NIO Socket的服务器端第10行可能会感到疑惑,既然是非阻塞的那么为什么在这个地方还是被阻塞了呢?在未收到客户端的数据时为什么还是被阻塞挂起了呢?这就需要用开头提到的这是1个客户端对应1个服务器端的场景,BIO和NIO并无明显区别,对于BIO或许更有优势,因为它的API相对来说更简单一些。而如果是多个客户端,如果使用NIO,服务器端会利用Selector(选择器)来选择准备好了的数据,而不会想此例一样一直等待一个客户端传输数据。接下来就是对Selector选择器的进一步认识。

Selector

  看到这里对于NIO似乎还只有一个认识,API变得负责了,莫名其妙地从“流”的概念转换为了“通道”“+“缓冲区”,并且似乎和BIO并无多大区别。要我说,最大的区别和改进莫过于彻底理解NIO中的Selector(选择器)。 在《Java IO(2)阻塞式输入输出(BIO)》一文的末尾提到了在服务器端利用线程来处理数据以便使得程序能拥有更大的吞吐量,这种利用新开一个线程来处理接收到的数据不失为一种常用的计策。但是,在程序中,我个人认为还是要谨慎使用多线程,毕竟线程的上下文切换是有一定的开销的,况且线程如果过多还有可能造成Java虚拟机的栈溢出。Selector选择器的出现就可以使用1个线程来管理。

  上面的示例程序都只有一个通道,也就是说同时只会读取或写入一个文件,如果现在有多个客户端,此时也就有多个通道,Selector选择器将会选择已经准备好了的通道读取数据。

  要使用Selector选择器,免不了大致会经过以下几个流程:创建Selector选择器;将Channel通道修改为非阻塞模式(只有Socket才能修改为非阻塞模式,FileChannel不能修改),并将通道注册至Selector;Selector调用select方法对通道进行选择。

 /**
* NIO 客户端,此处只有一个客户端连接
* Created by Kevin on 2017/12/24.
*/
public class Client {
public static void main(String[] args) throws Exception{
DatagramChannel channel = DatagramChannel.open();
ByteBuffer buffer = Charset.forName("utf-8").encode("this is Client.");
channel.send(buffer, new InetSocketAddress("127.0.0.1", 8989));
}
}

  如上注释所说,此处的示例仍然是只有一个客户端连接,对于服务器端的连接下面将会使用Selector选择器,重要部分在注释中已说明。

 /**
* NIO 服务器端
* Created by Kevin on 2017/12/23.
*/
public class Server {
public static void main(String[] args) throws Exception{
Selector selector = Selector.open(); //Selector选择器
DatagramChannel channel = DatagramChannel.open(); //Channel通道
channel.configureBlocking(false);
channel.bind(new InetSocketAddress("127.0.0.1", 8989));
channel.register(selector, SelectionKey.OP_READ); //此通道注册在Selector时关注是否可读
while (true) {
selector.select(); //如果没有一个注册到此Selector上的通道就绪,则阻塞;反之,只要有一个通道就绪则不会被阻塞。selectNow方法不论是否有通道就绪,都不会阻塞。
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); //选择就绪的通道
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) { //收到客户端数据
receive(key);
}
if (key.isWritable()) { //服务器端通道准备好向客户端发送数据
send(key);
}
}
}
} /**
* 服务器端收到客户端数据,并做处理
* @param key
*/
private static void receive(SelectionKey key) throws Exception{
DatagramChannel channel = (DatagramChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
channel.receive(byteBuffer);
System.out.println(new String(byteBuffer.array()));
}
/**
* 服务器端通道已准备好向客户端发送数据
* @param key
*/
private static void send(SelectionKey key) { }
}

  对于使用Selector选择器,可以使得服务器端只使用1个线程来管理多个连接,尽管在上面的例子没有给出示例代码,但这种场景在Web应用中可以说是必然的,因为对于客户端(浏览器)一定是很多的,而服务器就只有一个,此时正是NIO场景的最大使用,当然上面的例子也可以看到JDK原生NIO编程相比于BIO是略微有点复杂的,市面上也有很多优秀的第三方NIO框架——Netty、Mina均是对NIO的再次封装,这在以后也会提到,此篇关于NIO的了解暂到此处,以后将会在对此有更深刻的理解时再次讲解。下篇将介绍——AIO(异步输入输出)。

这是一个能给程序员加buff的公众号 

Java IO(3)非阻塞式输入输出(NIO)的更多相关文章

  1. Java IO(2)阻塞式输入输出(BIO)的字节流与字符流

    在上文中<Java IO(1)基础知识——字节与字符>了解到了什么是字节和字符,主要是为了对Java IO中有关字节流和字符流有一个更好的了解. 本文所述的输出输出指的是Java中传统的I ...

  2. Java IO(2)阻塞式输入输出(BIO)

    在上文中<Java IO(1)基础知识——字节与字符>了解到了什么是字节和字符,主要是为了对Java IO中有关字节流和字符流有一个更好的了解. 本文所述的输出输出指的是Java中传统的I ...

  3. 并发式IO的解决方案:多路非阻塞式IO、多路复用、异步IO

    在Linux应用编程中的并发式IO的三种解决方案是: (1) 多路非阻塞式IO (2) 多路复用 (3) 异步IO 以下代码将以操作鼠标和键盘为实例来演示. 1. 多路非阻塞式IO 多路非阻塞式IO访 ...

  4. Java基础——NIO(二)非阻塞式网络通信与NIO2新增类库

    一.NIO非阻塞式网络通信 1.阻塞与非阻塞的概念  传统的 IO 流都是阻塞式的.也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在 ...

  5. Linux NIO 系列(03) 非阻塞式 IO

    目录 一.非阻塞式 IO 附:非阻塞式 IO 编程 Linux NIO 系列(03) 非阻塞式 IO Netty 系列目录(https://www.cnblogs.com/binarylei/p/10 ...

  6. 阻塞式和非阻塞式IO

    有很多人把阻塞认为是同步,把非阻塞认为是异步:个人认为这样是不准确的,当然从思想上可以这样类比,但方式是完全不同的,下面说说在JAVA里面阻塞IO和非阻塞IO的区别 在JDK1.4中引入了一个NIO的 ...

  7. Socket-IO 系列(三)基于 NIO 的同步非阻塞式编程

    Socket-IO 系列(三)基于 NIO 的同步非阻塞式编程 缓冲区(Buffer) 用于存储数据 通道(Channel) 用于传输数据 多路复用器(Selector) 用于轮询 Channel 状 ...

  8. 4.NIO的非阻塞式网络通信

    /*阻塞 和 非阻塞 是对于 网络通信而言的*/ /*原先IO通信在进行一些读写操作 或者 等待 客户机连接 这种,是阻塞的,必须要等到有数据被处理,当前线程才被释放*/ /*NIO 通信 是将这个阻 ...

  9. 基于NIO写的阻塞式和非阻塞式的客户端服务端

    由于功能太过简单,就不过多阐述了,直接上阻塞式代码: package com.lql.nio; import org.junit.Test; import java.io.IOException; i ...

随机推荐

  1. Java多线程Lock

    JDK5以后为代码的同步提供了更加灵活的Lock+Condition模式,并且一个Lock可以绑定多个Condition对象 1.把原来的使用synchronized修饰或者封装的代码块用lock.l ...

  2. 微信公众平台快速开发框架 For Core 2.0 beta –JCSoft.WX.Core 5.2.0 beta发布

    写在前面 最近比较忙,都没有好好维护博客,今天拿个半成品来交代吧. 记不清上次关于微信公众号快速开发框架(简称JCWX)的更新是什么时候了,自从更新到支持.Net Framework 4.0以后基本上 ...

  3. 浅谈web移动端适配问题

    一.布局方案 目前在解决移动端页面适配问题方案选择上,目前用得比较多是百分比布局,弹性布局flex,rem布局,本文将重点跟大家探讨rem布局. 二.viewport 在介绍rem布局之前,首先跟大家 ...

  4. 15.javaweb XML详解教程

    一.XML语言简介 1,  作用:用于描述和保存现实中具有某种关系的数据,还可以作为软件配置文件,和描述程序模块之间的关系 2,  语法: 首先 先看一个XML文件的组成部分 关于文档声明 Versi ...

  5. Maven仓库-Nexus环境搭建及简单介绍

    1.    环境搭建 1.1  下载 http://www.sonatype.org/nexus/ NEXUS OSS [OSS = Open Source Software,开源软件——免费] NE ...

  6. 网站出现service unavailable的解决方法

    特别提示:本文的教程仅适合采用windows服务器的IIS组件上操作,service unavailable是许多网站会经常遇到的问题,希望对大家有用. 昨天一小段时间网站出现了service una ...

  7. WPF 完美截图 <一>

    最近比较懒,一直没继续,此处省略一万字,下面开始正题. 简单介绍下截图的思路: 核心是利用 public CroppedBitmap(BitmapSource source, Int32Rect so ...

  8. 通过游戏认识 --- JQuery与原生JS的差异

      前言 jQuery是一个快速.简洁的JavaScript框架,是继Prototype之后又一个优秀的JavaScript代码库( 或JavaScript框架).jQuery设计的宗旨是“write ...

  9. vimgdb安装以及使用

    vimgdb安装 vim-7.3.tar.bz2http://www.vim.org/sources.phpvimgdb-for-vim7.3 (this patch) https://github. ...

  10. python3学习笔记(3)

    一.内置函数补充1.callable()检测传递的参数是否可以被调用.def f1() pass可以被调用f2 = 123不可以被调用2.chr()和ord()chr()将ascii码转换成字符,or ...