NIO是Jdk中非常重要的一个组成部分,基于它的Netty开源框架可以很方便的开发高性能、高可靠性的网络服务器和客户端程序。本文将就其核心基础类型Channel, Buffer, Selector进行详细介绍,之后将介绍内存映射文件,Scatter/Gatter等扩展知识,最后将对Linux的5种IO模型进行剖析。

基础概念

Java NIO(non-blocking IO非阻塞IO)是jdk1.4后提供的新IO API,为所有基础类型都提供类缓存支持。其基础类型Channel定义了一个新的I/O接口,支持锁和访问内存映射文件,提供多路非阻塞式的高伸缩性网络I/O,其与传统IO的区别如下所示。

  1. Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  2. Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  3. Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

核心理解

NIO中的SocketChannel等网络Channel充分利用了操作系统内核对象的调度,只使用少量的几个线程来处理和客户端的所有通信,消除了无谓的线程上下文切换,最大限度的提高了网络通信的性能。可以看到,NIO主要关注文件IO和基于传输层的网络传输IO。对于.NET程序员来说,Windows的完成端口模型和NIO有一些相似之处,具体的IO模型分析请见本文最后部分的五种IO模型

使用场景

对于比较时髦的物联网公司,需要收集设备的数据,通常需要自定义相关协议并基于传输层通信。

Channel

Channel通道和传统IO中的Stream流类似,但它是双向的,而流式单向的,如InputStreamOutputStream等,Channel的常见实现如下所示。

FileChannel: 从文件中读写数据。

DatagramChannel: 能通过UDP读写网络中的数据。

SocketChannel: 能通过TCP读写网络中的数据。

ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

接下里通过一个TCP客户端的NIO实现来熟悉Channel的应用。

public class TCPClient {
public void start() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false);// 在非阻塞式信道上调用一个方法总是会立即返回
channel.connect(new InetSocketAddress("127.0.0.1", 8080)); if (channel.finishConnect()) {
int i = 0;
while (true) {
TimeUnit.SECONDS.sleep(1);
String info = "I'm " + (i++) + "-th information from client";
buffer.clear();
buffer.put(info.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
// 问题,这样一个个直接的输出,速度快么?
System.out.println(buffer);
channel.write(buffer);
}
}
}
} catch (Throwable ex) {
ex.printStackTrace();
}
}
}

比对一下传统的IO

try (RandomAccessFile aFile = new RandomAccessFile("src/test/resources/test.xml", "r")) {
// 1.读数据
byte[] buffer = new byte[1024];
StringBuilder requestString = new StringBuilder();
int bytesRead = 0;
while (-1 != (bytesRead = aFile.read(buffer))) {
requestString.append(new String(buffer), 0, bytesRead);
}
}

Buffer

NIO中主要的Buffer缓存实现包括ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer等,分别对应相应的基础类型,Buffer的主要作用请见下图。

Buffer的数据结构包括一个连续数组,表示缓冲区数组的总长度的capacity,记录下一个要操作的数据元素的位置position,以及limit、mark等字段,接下来请见一个最简单的NIO示例(其中使用到了文件锁和CharBuffer)。

public static void readFile() {
long timeBegin = System.currentTimeMillis();
try (RandomAccessFile aFile = new RandomAccessFile("src/nio.txt", "rw")) {
FileChannel fileChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
Charset charset = Charset.forName("UTF-8"); int position = 0;
while (true) {
// 文件锁,锁一段区域,shard为true表示共享锁
FileLock fileLock = fileChannel.lock(position, 1024, true);
int bytesRead = fileChannel.read(buf);
fileLock.release();
if (-1 == bytesRead)
break;
position = position + bytesRead;
buf.flip();
//将ByteBuffer转化为CharBuffer
System.out.println(charset.decode(buf));
buf.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
}

Selector

Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中,要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,如新连接进来,数据接收等。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好,接下来通过TCP服务端的NIO实现来熟悉Selector的应用。

public class TCPServer {
private static final int BUF_SIZE = 1024;
private static final int PORT = 8080;
private static final int TIMEOUT = 3000;
private TCPServer() {
}
public static final TCPServer instance = new TCPServer();
public static TCPServer getInstance() {
return instance;
} /*
* 关键类
*/
public void selector() {
try (Selector selector = Selector.open(); ServerSocketChannel ssChannel = ServerSocketChannel.open()) {
ssChannel.bind(new InetSocketAddress(PORT));
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) {
if (0 == selector.select(TIMEOUT)) {
System.out.println("==");
continue;
}
// 使用 for (SelectionKey key : selector.selectedKeys()) 方式无法移除对象
// Iterator对象的remove方法是迭代过程中删除元素的唯一方法
Iterator<SelectionKey> selectKeys = selector.selectedKeys().iterator();
while (selectKeys.hasNext()) {
SelectionKey key = selectKeys.next();
//分别处理接受、连接、读、写4中状态
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
handleRead(key);
}
if (key.isWritable() && key.isValid()) {
handleWrite(key);
}
if (key.isConnectable()) {
System.out.println("isConnectable = true");
}
selectKeys.remove();
}
}
} catch (Throwable ex) {
ex.printStackTrace();
}
} public void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();// 获取ServerSocket的Channel
SocketChannel sc = ssChannel.accept();// 监听新进来的链接
sc.configureBlocking(false);// 设置client链接为非阻塞
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(BUF_SIZE));// 将channel注册Selector
} public void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();// 获取链接client的channel
ByteBuffer buf = (ByteBuffer) key.attachment();// 获取附加在key上的数据
long bytesRead = channel.read(buf);// 读channel上数据到buf?这部分表达的是否正确?
while (bytesRead > 0) {
buf.flip();
while (buf.hasRemaining()) {
System.out.println(buf.getChar());
}
System.out.println();
buf.clear();
bytesRead = channel.read(buf);
}
if (-1 == bytesRead) {
channel.close();
}
} public void handleWrite(SelectionKey key) throws IOException {
ByteBuffer buf = (ByteBuffer) key.attachment();// 获取buffer对象
buf.flip();
SocketChannel channel = (SocketChannel) key.channel();
while (buf.hasRemaining()) {
channel.write(buf);
}
buf.compact();
}
}

进阶概念

Java IO与NIO的区别主要包括如下的3个方面。

面向流和面向缓冲:Java IO是面向流的,其意味着在读取所有字节前,数据未被缓存到任何地方,其不能前后移动流中的数据。而NIO是面向缓存的,可以方便在缓存区的移动数据。

阻塞和非阻塞IO:java IO是完全阻塞的,在数据读取或写入完成前,该线程一直被占用。而NIO的非阻塞模式,当线程获取不到数据时,可以被释放用于其他工作。

选择器Selector:其允许一个单独的线程来监视多个出入通道,利于线程的高效利用。

内存映射文件

对于内存映射文件,大家应该不会陌生,大学里学习windows网络编程时就终点介绍过该概念。在JAVA中,一般用BufferedReaderBufferedInputStream的来处理大文件。而当文件超大时推荐使用MappedByteBuffer,其是NIO引入的文件内存映射方案,读写性能极高。一般操作系统的内存包括物理内存和虚拟内存。其中虚拟内存一般使用的是页面映像文件,即硬盘中的某个特殊的文件。操作系统负责该页面文件内容的读写,这个过程叫”页面中断/切换”MappedByteBuffer与其类似,可以看做一个超级大的ByteBuffer,示例如下所示。

public class MappedByteBufferDemo {
public static void commonReadBigFile() {
try (RandomAccessFile aFile = new RandomAccessFile("D:/svn.rar", "rw");
FileChannel fileChannel = aFile.getChannel()) { long timeBegin = System.currentTimeMillis();
ByteBuffer buff = ByteBuffer.allocate((int) aFile.length());
buff.clear();
fileChannel.read(buff);
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
} catch (IOException e) {
e.printStackTrace();
}
} public static void highPerformanceReadBigFile() {
try (RandomAccessFile aFile = new RandomAccessFile("D:/svn.rar", "rw");
FileChannel fileChannel = aFile.getChannel()) { long timeBegin = System.currentTimeMillis();
MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, aFile.length());
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
} catch (IOException e) {
e.printStackTrace();
}
}
}

通过一个1G的文件做单元测试测试,普通的Buffer读需要1373ms,而内存映射文件只需要1ms,这个测试可能不太准确,但差异已经无比明显。当然使用内存映射文件也存在问题,就是它只要GC时才能被回收,会占用大量内存资源,我所了解的一个使用场景涉及复杂的推荐算法因素和规则的加载(国际机票组合选择),之后进行运算。

其他

Pipe:之前介绍的Channel虽然既可以用作读,也可以用作写,但它每次只能支持一种方式的通讯,即单工/半双工的。而Pipe是全双工的,其内部包含两个Channel,一个是SinkChannel用于写入数据,一个Source通道用于读取数据。

Scatter/Gatter:前者在读操作时将数据从一个Channel中读入到多个Buffer,而后者用于将多个Buffer写入一个Channel,常见的使用场景为将消息头和消息体一起写入到Channel中。

TransferFrom & TransferTo:传输方法用于通道之间的通信,比如FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中,而transferTo()方法将数据从FileChannel传输到其他的channel中。

Java的Path接口是Java NIO2 的一部分,是对Java6 和Java7的 NIO的更新。Java的Path接口在Java7 中被添加到Java NIO,位于java.nio.file包中, 其全路径是java.nio.file.Path。一个Path实例代表了一个文件系统中的路径。一个路径可以指向一个文件或者一个文件夹。一个路径可以是绝对路径或者是相对路径。绝对路径是从根路径开始的全路径,相对路径是一个相对其他路径的文件或文件夹路径。相对路径可能会造成一点混乱,但是不要担心,在本文章中,我会详细解释相对路径的。

DatagramChannel:用于基于UDP协议,用于发送和接受数据包,常用于QQ语音、视频等通讯质量要求不高的场景。

NIO2

Jdk7对NIO做了一些改进,包括对AIO的支持,以及增强型的文件操作类。过去的java.io.File访问文件系统时,无法利用特定文件系统的特性,且性能不高,因此引入了Path,Paths,Files等。对于AIO,其提供了AsynchronousChannelAsynchronousFileChannel等与NIO响应的类型,大大简化了异步IO操作,比如在过去我们需要自己管理线程池来进行Callable的调用返回Future<?>,而现在省去了该步骤,请见下面的示例(该场景下直接使用NIO更合适,AIO适合更加复杂的场景)。

public static void readFile() {
Path filePath = Paths.get("src/nio.txt"); long timeBegin = System.currentTimeMillis();
try (AsynchronousFileChannel afc = AsynchronousFileChannel.open(filePath)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
Charset charset = Charset.forName("UTF-8"); int position = 0;
for (;;) {
Future<Integer> futureResult = afc.read(buf, position);
int result = futureResult.get(); if (-1 == result)
break; position = position + result;
buf.flip();
System.out.println(charset.decode(buf));
buf.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
}

Linux的五种IO模型

通常来说,网络IO模型大致包括如下2大类,其中同步模型包括4种。

1. 同步模型

a.阻塞IO:简称bio,linux中默认所有的socket都是blocking,其特点是进程会一直阻塞,直到数据拷贝完成。

b.非阻塞IO:简称nio,与本文的NIO(New IO)不是一个概念,其特点是进程会反复调用IO函数,并马上返回,但在数据拷贝的过程中,进行仍然是阻塞的。

c.多路复用IO:由于NIO需要大量的轮训会消耗大量CPU时间,而如果这件事有操作内核来通知就好了,这是就会用到Linux的selectpollepoll函数。比如select,其可以对多个IO端口进行监控。

d.信号驱动IO:允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

2. 异步模型(aio):相对于同步IO,异步IO不是顺序执行。其特点是IO的两个阶段,进程都是非阻塞的。

小结

以上对Linux的IO模型进行了介绍,对应到java程序,那么io包中的操作其实就是阻塞IO的方式,而nio包中的Channel的类型就是非阻塞IO的方式,Selector提供了多路复用的方式,而AsynchronousChannel提供了异步IO的方式。此外,这几种IO模型并没有谁优谁劣的说法,需要结合具体场景进行分析,通常来说,高并发的程序会使用同步非阻塞的方式。如果感觉解释的不是很完善,请见博文聊聊Linux 五种IO模型,该博主通过和女友等餐的过程对5种IO的过程做了很好的诠释,大赞。

Tip:

之后的文章中将对Netty,mina框架进行介绍,其是NIO的封装库。

参考资料

netty官网

Java NIO系列教程

攻破JAVA NIO技术壁垒

完成端口(Completion Port)详解

java nio框架netty 与tomcat的关系

Tomcat7中NIO处理分析(一)

NIO文档

高性能IO模型浅析

聊聊同步、异步、阻塞与非阻塞

JavaNIO快速入门的更多相关文章

  1. Web Api 入门实战 (快速入门+工具使用+不依赖IIS)

    平台之大势何人能挡? 带着你的Net飞奔吧!:http://www.cnblogs.com/dunitian/p/4822808.html 屁话我也就不多说了,什么简介的也省了,直接简单概括+demo ...

  2. SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=》提升)

     SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=>提升,5个Demo贯彻全篇,感兴趣的玩才是真的学) 官方demo:http://www.asp.net/si ...

  3. 前端开发小白必学技能—非关系数据库又像关系数据库的MongoDB快速入门命令(2)

    今天给大家道个歉,没有及时更新MongoDB快速入门的下篇,最近有点小忙,在此向博友们致歉.下面我将简单地说一下mongdb的一些基本命令以及我们日常开发过程中的一些问题.mongodb可以为我们提供 ...

  4. 【第三篇】ASP.NET MVC快速入门之安全策略(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

  5. 【番外篇】ASP.NET MVC快速入门之免费jQuery控件库(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

  6. Mybatis框架 的快速入门

    MyBatis 简介 什么是 MyBatis? MyBatis 是支持普通 SQL 查询,存储过程和高级映射的优秀持久层框架.MyBatis 消除 了几乎所有的 JDBC 代码和参数的手工设置以及结果 ...

  7. grunt快速入门

    快速入门 Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器. Grunt 0.4.x 必须配合Node.js >= 0.8.0版本使用.:奇数版本 ...

  8. 【第一篇】ASP.NET MVC快速入门之数据库操作(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

  9. 【第四篇】ASP.NET MVC快速入门之完整示例(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

随机推荐

  1. B. Array

    题目链接:http://codeforces.com/contest/224/problem/B 具体大意: 输入n,m. 给你一个区间,让你找某一段区间中包含m个不同的数,并且这段区间中的某一个小区 ...

  2. mysql 案例~mysql元数据的sql统计

    一 简介:今天我们来收集下提取元数据的sql 二 前沿: information_schema  引擎 memory 元数据收集表 三 sql语句: 1#没有使用索引的表统计 SELECT t.TAB ...

  3. 论文笔记系列-DARTS: Differentiable Architecture Search

    Summary 我的理解就是原本节点和节点之间操作是离散的,因为就是从若干个操作中选择某一个,而作者试图使用softmax和relaxation(松弛化)将操作连续化,所以模型结构搜索的任务就转变成了 ...

  4. Maven入门-运行struts项目进行测试(三)

    maven运行struts项目进行测试: 在入门二中已经导入struts的jar包. 此时的pom.xml文件 <project xmlns="http://maven.apache. ...

  5. CentOS和RedHat Linux的区别

    RHEL 在发行的时候,有两种方式.一种是二进制的发行方式,另外一种是源代码的发行方式. 无论是哪一种发行方式,你都可以免费获得(例如从网上下载),并再次发布.但如果你使用了他们的在线升级(包括补丁) ...

  6. 【Linux】Linux下统计当前文件夹下的文件个数、目录个数

    统计当前文件夹下文件的个数,包括子文件夹里的 ls -lR|grep "^-"|wc -l 统计文件夹下目录的个数,包括子文件夹里的 ls -lR|grep "^d&qu ...

  7. SHA算法:签名串SHA算法Java语言参考(SHAHelper.java)

    SHAHelper.java package com.util; /** * @author wangxiangyu * @date:2017年10月16日 上午9:00:47 * 类说明:SHA签名 ...

  8. 解决git: 'subtree' is not a git command. See 'git --help'.

    一.第一方法 git clone https://github.com/git/git.git cd git/contrib/subtree sudo make prefix=/usr sudo ma ...

  9. c++ 引用方式传递数组

    值传递 (pass by value),指针传递(pass by pointer),当发生函数调用时,需要给形参分配存储单元.当传递是对象时,要调用拷贝构造函数.而且指针最后析构时,要处理内存释放问题 ...

  10. centos常用网络管理命令

    网卡配置命令:ifconfig (ip addr , ip link) ifconfig:显示所有活动状态的相关信息    ifconfig Interface:仅显示指定接口的相关信息    ifc ...