java中的NIO总结

一.简介

NIO(Non-blocking I/O,在Java领域,也称为New I/O),在jdk1.4 里提供的新api 。Sun 官方标榜的特性如下: 为所有的原始类型提供(Buffer)缓存支持,字符集编码解码解决方案。
Channel :一个新的原始I/O 抽象。 支持锁和内存映射文件的文件访问接口。 提供多路(non-bloking) 非阻塞式的高伸缩性网络I/O 。NIO也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,
成为解决高并发与大量连接、I/O处理问题的有效方式。

二.NIO的原理2.1 传统的IO的原理和弊端

使用传统的I/O程序读取文件内容, 并写入到另一个文件(或Socket), 如下程序:
[Java] 纯文本查看 复制代码
1
2
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
会有较大的性能开销, 主要表现在一下两方面:
1. 上下文切换(context switch), 此处有4次用户态和内核态的切换
2. Buffer内存开销, 一个是应用程序buffer, 另一个是系统读取buffer以及socket buffer
其运行示意图如下
java中的NIO总结
1) 先将文件内容从磁盘中拷贝到操作系统buffer
2) 再从操作系统buffer拷贝到程序应用buffer
3) 从程序buffer拷贝到socket buffer
4) 从socket buffer拷贝到协议引擎.
传统BIO代码举例:
TraditionalClient.java
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.net.Socket;
 
public class TraditionalClient {
 
    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        // 创建socket链接
        Socket socket = new Socket("localhost", 2000);
        System.out.println("Connected with server " + socket.getInetAddress() + ":" + socket.getPort());
        // 读取文件
        FileInputStream inputStream = new FileInputStream("C:/sss.txt");
        // 输出文件
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());
        // 缓冲区4096K
        byte[] b = new byte[4096];
        // 传输长度
        long read = 0;
        long total = 0;
        // 读取文件,写到socketio中
        while ((read = inputStream.read(b)) >= 0) {
            total = total + read;
            output.write(b);
        }
        // 关闭
        output.close();
        socket.close();
        inputStream.close();
        // 打印时间
        System.out.println("bytes send--" + total + " and totaltime--" + (System.currentTimeMillis() - start));
    }
}

 

TraditionalServer.java
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;
 
public class TraditionalServer {
 
    public static void main(String args[]) throws Exception {
        // 监听端口
        ServerSocket server_socket = new ServerSocket(2000);
        System.out.println("等待,端口为:" + server_socket.getLocalPort());
 
        while (true) {
            // 阻塞接受消息
            Socket socket = server_socket.accept();
            // 打印链接信息
            System.out.println("新连接: " + socket.getInetAddress() + ":" + socket.getPort());
            // 从socket中获取流
            DataInputStream input = new DataInputStream(socket.getInputStream());
            // 接收数据
            byte[] byteArray = new byte[4096];
            while (true) {
                int nread = input.read(byteArray, 0, 4096);
                System.out.println(new String(byteArray, "UTF-8"));
                if (-1 == nread) {
                    break;
                }
            }
            socket.close();
            server_socket.close();
            System.out.println("Connection closed by client");
 
        }
    }
}


[size=1.17em]2.2 为什么需要NIO

两个字,效率,NIO能够处理的所有场景,原IO基本都能做到,NIO因效率而生,效率包括处理速度和吞吐量(Througthout, Scalability)。Java原IO都是流式的(Stream Oriented),一个Byte一个Byte的读取,且需要在JVM(用户空间)和操作系统内核空间之间复制数据(Bytes),速度较慢。IO主要分两块,文件系统IO和网络IO。文件系统方面,通过批量处理(Buffer),操作直接委托给操作系统(Direct),充分的利用操作系统的IO能力,提高访问性能,不过NIO还不支持文件系统的异步调用。网络IO方面,提供非阻塞操作,减少处理网络IO的线程数,增强可伸缩性(Scalability)。额外提一下,线程切换(Context Swith)是繁重的,多了会严重影响性能(可伸缩性, Scalability),线程越多越糟糕,n核的机器参考的线程数是n或n+1。

2.3 NIO的原理和优势

NIO技术省去了将操作系统的read buffer拷贝到程序的buffer, 以及从程序buffer拷贝到socket buffer的步骤, 直接将 read buffer 拷贝到 socket buffer.
java 的 FileChannel.transferTo() 方法就是这样的实现, 这个实现是依赖于操作系统底层的sendFile()实现的.
publicvoid transferTo(long position, long count, WritableByteChannel target);
它的底层调用的是系统调用sendFile()方法
sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
如下图
 
java中的NIO总结java中的NIO总结转存失败重新上传取消java中的NIO总结


2.4 IO和NIO的区别:

1. 面向流与面向缓冲 
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。
即IO是基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
2. 阻塞与非阻塞IO 
IO的各种流是阻塞的,即:当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被完全读取,或数据完全写入。该线程在此期间不能再干任何事情了。
而NIO采用的是非阻塞模式,即:一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 

2.5 NIO的三个重要抽象:

  Java NIO引入的最重要三个抽象是Buffer,Channel,Selector,因为Java NIO中文件系统部分是阻塞的的,故不会用到Selector。下面分别来介绍。

Buffer

  读或写数据的暂存容器,与Channel匹配使用。提供批量的向Channel写入或者读取数据功能。有四个int属性需要理解:
    mark  标记的位置,default -1。
    position 游标当前位置
    limit 存活(可用)Byte最后一个的位置
    capacity Buffer的最大容量
  很多Buffer的操作都与上面四个属性有关,比如flip(),rewind(), clear()等最重要的一个Buffer实现是ByteBuffer,主要实现对Byte的存取操作,常用的还有CharBuffer。

Channel

  Client与IO设备之间读写的交互通道,数据先放到Buffer里面,再经Channel写入设备,或者从Channel里面读取数据。常用的有FileChannel,ServerSocketChannel和SocketChannel。

Selector

  针对非阻塞(No-Blocking)的Channel,提供事件通知的机制。很多教程强调Selector提供了多路复用(单个线程或少数线程负责多个Channel的事件通知)的机制,非阻塞式编程是基于事件的,Selector就是事件的通讯机制。非阻塞式才是关键,Event Bus构建非阻塞式编程的环境。
NIO代码举例一:
TransferToClient.java
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
 
public class TransferToClient {
 
    public static void main(String[] args) throws IOException {
        long start = System.currentTimeMillis();
        // 打开socket的nio管道
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 9026));// 绑定相应的ip和端口
        socketChannel.configureBlocking(true);// 设置阻塞
        // 将文件放到channel中
        FileChannel fileChannel = new FileInputStream("C:/sss.txt").getChannel();// 打开文件管道
        //做好标记量
        long size = fileChannel.size();
        int pos = 0;
        int offset = 4096;
        long curnset = 0;
        long counts = 0;
        //循环写
        while (pos<size) {
            curnset = fileChannel.transferTo(pos, 4096, socketChannel);// 把文件直接读取到socket chanel中,返回文件大小
            pos+=offset;
            counts+=curnset;
        }
        //关闭
        fileChannel.close();
        socketChannel.close();
        //打印传输字节数
        System.out.println(counts);
        // 打印时间
        System.out.println("bytes send--" + counts + " and totaltime--" + (System.currentTimeMillis() - start));
    }
}

 

TransferToServer.java
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
 
public class TransferToServer {
 
    public static void main(String[] args) throws IOException {
        // 创建socket channel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.setReuseAddress(true);// 地址重用
        serverSocket.bind(new InetSocketAddress("localhost", 9026));// 绑定地址
        System.out.println("监听端口 : " + new InetSocketAddress("localhost", 9026).toString());
 
        // 分配一个新的字节缓冲区
        ByteBuffer dst = ByteBuffer.allocate(4096);
        // 读取数据
        while (true) {
            SocketChannel channle = serverSocketChannel.accept();// 接收数据
            System.out.println("Accepted : " + channle);
            channle.configureBlocking(true);// 设置阻塞,接不到就停
            int nread = 0;
            while (nread != -1) {
                try {
                    nread = channle.read(dst);// 往缓冲区里读
                    byte[] array = dst.array();//将数据转换为array
                    //打印
                    String string = new String(array, 0, dst.position());
                    System.out.print(string);
                    dst.clear();
                } catch (IOException e) {
                    e.printStackTrace();
                    nread = -1;
                }
            }
        }
    }
}

 

NIO代码举例二:
NIOClient.java
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import java.io.IOException; 
import java.net.InetSocketAddress; 
import java.nio.ByteBuffer; 
import java.nio.channels.SelectionKey; 
import java.nio.channels.Selector; 
import java.nio.channels.SocketChannel; 
import java.util.Iterator; 
   
/**
 * NIO客户端
 */ 
public class NIOClient { 
   //通道管理器 
   private Selector selector; 
  
   /**
    * 获得一个Socket通道,并对该通道做一些初始化的工作
    * @param ip 连接的服务器的ip
    * @param port  连接的服务器的端口号         
    * @throws IOException
    */ 
   public void initClient(String ip,int port) throws IOException { 
       // 获得一个Socket通道 
       SocketChannel channel = SocketChannel.open(); 
       // 设置通道为非阻塞 
       channel.configureBlocking(false); 
       // 获得一个通道管理器 
       this.selector = Selector.open(); 
          
       // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调用channel.finishConnect();才能完成连接 
       channel.connect(new InetSocketAddress(ip,port)); 
       //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。 
       channel.register(selector, SelectionKey.OP_CONNECT); 
   
  
   /**
    * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
    * @throws IOException
    */ 
   public void listen() throws IOException { 
       // 轮询访问selector 
       while (true) { 
           selector.select(); 
           // 获得selector中选中的项的迭代器 
           Iterator ite = this.selector.selectedKeys().iterator(); 
           while (ite.hasNext()) { 
               SelectionKey key = (SelectionKey) ite.next(); 
               // 删除已选的key,以防重复处理 
               ite.remove(); 
               // 连接事件发生 
               if (key.isConnectable()) { 
                   SocketChannel channel = (SocketChannel) key.channel();
                   // 如果正在连接,则完成连接 
                   if(channel.isConnectionPending()){ 
                       channel.finishConnect(); 
                   
                   // 设置成非阻塞 
                   channel.configureBlocking(false); 
                   //在这里可以给服务端发送信息哦 
                   channel.write(ByteBuffer.wrap((new String("向服务端发送了一条信息").getBytes("UTF-8") ))); 
                   //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。 
                   channel.register(this.selector, SelectionKey.OP_READ); 
                      
                   // 获得了可读的事件 
               } else if (key.isReadable()) { 
                       read(key); 
               
  
           
       
   
   /**
    * 处理读取服务端发来的信息 的事件
    * @param key
    * @throws IOException 
    */ 
   public void read(SelectionKey key) throws IOException{ 
       //和服务端的read方法一样 
   
      
   /**
    * 启动客户端测试
    * @throws IOException 
    */ 
   public static void main(String[] args) throws IOException { 
       NIOClient client = new NIOClient(); 
       client.initClient("localhost",8001); 
       client.listen(); 
   
  
}
NIOServer.java
[Java] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
import java.io.IOException; 
import java.net.InetSocketAddress; 
import java.nio.ByteBuffer; 
import java.nio.channels.SelectionKey; 
import java.nio.channels.Selector; 
import java.nio.channels.ServerSocketChannel; 
import java.nio.channels.SocketChannel; 
import java.nio.charset.Charset;
import java.util.Iterator; 
   
/**
 * NIO服务端
 */ 
public class NIOServer { 
   //通道管理器 
   private Selector selector; 
  
   /**
    * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
    * @param port  绑定的端口号
    * @throws IOException
    */ 
   public void initServer(int port) throws IOException { 
       // 获得一个ServerSocket通道 
       ServerSocketChannel serverChannel = ServerSocketChannel.open(); 
       // 设置通道为非阻塞 
       serverChannel.configureBlocking(false); 
       // 将该通道对应的ServerSocket绑定到port端口 
       serverChannel.socket().bind(new InetSocketAddress(port)); 
       // 获得一个通道管理器 
       this.selector = Selector.open(); 
       //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后, 
       //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。 
       serverChannel.register(selector, SelectionKey.OP_ACCEPT); 
   
  
   /**
    * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
    * @throws IOException
    */ 
   public void listen() throws IOException { 
       System.out.println("服务端启动成功!"); 
       // 轮询访问selector 
       while (true) { 
           //当注册的事件到达时,方法返回;否则,该方法会一直阻塞 
           selector.select(); 
           // 获得selector中选中的项的迭代器,选中的项为注册的事件 
           Iterator ite = this.selector.selectedKeys().iterator(); 
           while (ite.hasNext()) { 
               SelectionKey key = (SelectionKey) ite.next(); 
               // 删除已选的key,以防重复处理 
               ite.remove(); 
               // 客户端请求连接事件 
               if (key.isAcceptable()) { 
                   ServerSocketChannel server = (ServerSocketChannel) key.channel();
                   // 获得和客户端连接的通道 
                   SocketChannel channel = server.accept(); 
                   // 设置成非阻塞 
                   channel.configureBlocking(false); 
  
                   //在这里可以给客户端发送信息哦 
                   channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes("UTF-8"))); 
                   //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。 
                   channel.register(this.selector, SelectionKey.OP_READ); 
                   // 获得了可读的事件 
               } else if (key.isReadable()) { 
                       read(key); 
               
  
           
  
       
   
   /**
    * 处理读取客户端发来的信息 的事件
    * @param key
    * @throws IOException 
    */ 
   public void read(SelectionKey key) throws IOException{ 
       // 服务器可读取消息:得到事件发生的Socket通道 
       SocketChannel channel = (SocketChannel) key.channel(); 
       // 创建读取的缓冲区 
       ByteBuffer buffer = ByteBuffer.allocate(10); 
       channel.read(buffer); 
       byte[] data = buffer.array(); 
       String msg = new String(new String(data).trim().getBytes(),"UTF-8"); 
       System.out.println("服务端收到信息:"+msg); 
       ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes()); 
       channel.write(outBuffer);// 将消息回送给客户端 
   
      
   /**
    * 启动服务端测试
    * @throws IOException 
    */ 
    public static void main(String[] args) throws IOException { 
        NIOServer server = new NIOServer(); 
        server.initServer(8001); 
        server.listen(); 
    
   
}


三.BIO、NIO、AIO适用场景分析:

BIO方式适用于连接数目比较小并且一次发送大量数据的场景,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多,每次只是发送少量的数据,服务器需要支持超大量的长时间连接。比如10000个连接以上,并且每个客户端并不会频繁地发送太多数据。例如总公司的一个中心服务器需要收集全国便利店各个收银机的交易信息,只需要少量线程按需处理维护的大量长期连接,或者聊天服务器.Jetty、Mina、Netty、ZooKeeper等都是基于NIO方式实现。

 

对于BIO、NIO、AIO的区别和应用场景,知乎上有位同学是这样回答的:
BIO:Apache,Tomcat。主要是并发量要求不高的场景.如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。
NIO:Nginx,Netty。主要是高并发量要求的场景,如果需要管理同时打开的成千上万个连接,这些连接,例如聊天服务器,实现NIO的服务器可能是一个优势。
用形象的例子来理解一下概念,以银行取款为例:
同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写)。
异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API)。
阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)。
非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,
你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。