Java NIO
1. 概述 Java NIO (New IO) 由以下三个核心组件组成:
通常,在NIO中,IO从一个Channel开始。数据可以从Channel中读到Buffer,也可以从Buffer中写道Channel。而Selector允许单个线程处理多个Channel。 2. Channel Channels和Streams很像,但还是有一些不同的:
如上所述,数据从通道读取到缓冲区中,从缓冲区写入通道中,如下图所示: Channel最重要的四种实现:
3. Buffer 在Java NIO中Buffer用来和Channel交互,数据从channel读到buffer中,从buffer写到channel中。 Buffer本质上是一个内存块,可以在其中写入数据,然后在以后再次读取。该内存块包装在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用该内存块。 使用Buffer读写数据,典型地分为四步:
当你向一个buffer中写数据时,buffer会跟踪你已经写了多少数据了。一旦你需要读取数据,你需要调用flip()方法将buffer从写入模式切换为读取模式。在读取模式下,buffer使你可以读取写入缓冲区的所有数据。 一旦你已经读取了所有数据,你需要清除buffer,以使得它可以再次被写入数据。有两个方法可以达到这个效果:clear()或者compact()。clear()方法会清理整个buffer,compact()方法只清理你已经读过的数据。任何未读的数据都将移至缓冲区的开头,并且将来写入buffer的数据在现在未读的数据之后。 1 RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
2 FileChannel inChannel = aFile.getChannel();
3
4 // create buffer with capacity of 48 bytes
5 ByteBuffer buf ByteBufferallocate(48 6
7 int bytesRead inChannelread(buf); //read into buffer.
8 while != -1) {
9
10 flip(); //make buffer ready for read
11
12 whilehasRemaining()){
13 Systemoutprint((char) get()); // read 1 byte at a time
14 }
15
16 clear(); //make buffer ready for writing
17 18 19 close();
Buffer有三个属性:
position和limit的含义取决于Buffer是处于读模式还是写模式,无论哪种模式下capacity的含义总是不变的。 Capacity? 作为一个内存块,Buffer有一个固定大小,也被称作“capacity”。一旦Buffer满了,就需要清空它(读取数据或清除数据),然后才能将更多数据写入其中。 Position? 当你将数据写入Buffer时,你需要在一个明确的位置写入。初始位置是0,当有数据被写入后,position会向前移动以指向下一个可写入的位置。position的最大值是capacity-1。 当你从Buffer中读取数据时,也需要从一个给定的位置处开始读取。当你将Buffer从写模式切换为读模式时,position会被重置为0。 Limit 在写模式下,limit表示你可以写多少数据到Buffer。在写模式下,limit的值等于capacity。 在读模式下,limit表示你可以从Buffer中读多少数据。因此,当从写模式切换为读模式时,limit被设置为在写模式是的position。换言之,写了多少就能读多少。 3.1. 分配一个缓冲区 为了获得一个Buffer,首先必须先给它分配空间。每种类型的Buffer都有一个allocate()方法来做这件事。? 1 // 分配字节缓冲区,容量为48字节
2 3 // 分配字符缓冲区,容量为1024个字符
4 CharBuffer CharBuffer1024);
3.2. 写数据到Buffer 有两种方式向Buffer中写数据:
//read into buffer
2 );
3
put127);
flip() flip()方法将Buffer从写模式切换为读模式,调用flip()将设置position为0,limit不变还是在刚才的位置。? 3.3. 从Buffer中读数据 有两种方式从Buffer中读数据:
//read from buffer into channel
bytesWritten write4 byte aByte ();
rewind() Buffer.rewind()设置position为0,以至于你可以从头再读一遍Buffer中的所有数据。 clear() clear()方法将position置为0,并且limit与capacity相等。换句话说,Buffer被清除了。其实,Buffer上的数据并没有被真正清除,只是告诉你你可以将数据写到哪里。 compact() compact()方法将所有未读的数据复制到Buffer的开头,然后它将position设置在最后一个未读元素的右侧,limit仍然等于capacity。现在,Buffer可以写了,只不过你不能覆盖之前那些未读的数据。? mark() 和 reset()? 通过调用Buffer.mark()你可以标记一个给定的位置。你可以在随后调用Buffer.reset()返回到刚才标记的位置那里。 4. Selector Selector是一个组件,它可以检查一个或多个Channel实例,并决定哪些Channel已经准备好读或写。通过这种方式,一个线程可以管理多个通道,从而实现管理多个网络连接(PS:Selelctor可以确定哪些Channel可读或可写,这样只需要一个线程就能管理多个网络连接) 4.1. 为什么要用Selector 使用单个线程来处理多个通道的优点是,处理通道所需的线程更少。事实上,你可以使用一个线程来处理所有的通道。对于操作系统来说,线程之间的切换非常昂贵,而且每个线程也会占用操作系统中的一些资源(内存)。因此,使用的线程越少越好。(PS:但是请记住,现代操作系统和CPU在多任务处理方面变得越来越好,因此,随着时间的推移,多线程的开销会越来越小。) 4.2. 创建Selector // 创建一个Selector
Selector selector Selectoropen4 // 注册Channel到Selector
5 channelconfigureBlocking(false6 SelectionKey key registerselectorSelectionKeyOP_READ);
Channel必须是非阻塞模式才能和Selector一起使用。这就意味着FileChannel不能和Selector一起用,因为FileChannel不能切换成非阻塞模式。 register()方法的第二个参数表示你希望通过Selector在Channel中监听的事件。有四种不同的事件可以被监听:
这四种事件用SelectionKey的四个常量来表示:
如果你对多个事件都感兴趣,可以这样写: 1 interestSet OP_READ | OP_WRITE;
4.3. 通过Selector选择Channel 在调用任意一个select()方法以后,会返回你感兴趣的并且相应事件已经准备好的channel给你。简单地来说就是,如果你对已经为读取做好准备的channel感兴趣,那么你将从select()方法中接收到这样的channel。
select()方法的返回值是一个int值,表示有多少个准备好的channel。也就是说,在上一次调用select()以后有多少个channel变成已准备好。 完整的示例: 2
3 4
true 8
9 readyChannels selectNow10
11 if == 0) continue;
12
13
14 Set<> selectedKeys selectedKeys16 IteratorkeyIterator iterator17
18 keyIteratorhasNext()) 19
20 next21
22 keyisAcceptable23 // a connection was accepted by a ServerSocketChannel.
24
25 } else isConnectable26 // a connection was established with a remote server.
27
28 isReadable29 // a channel is ready for reading
30
31 isWritable32 // a channel is ready for writing
33 34
35 remove36 37 }
5. SocketChannel Java NIO SocketChannel是连接到TCP网络Socket的通道。 创建SocketChannel有两种方式:
// Opening a SocketChannel
SocketChannel socketChannel SocketChannel3 socketChannelconnectInetSocketAddress"localhost"9000));
4
5 // Closing a SocketChannel
();?
5.1. 从SocketChannel中读取 1 );
SocketChannel.read()方法将数据从SocketChannel读到Buffer中,其返回值表示有多少字节被写道Buffer中。如果返回-1,则表示到达流的末尾。 5.2. 写数据到SocketChannel String newData = "hahaha" 4 newDatagetBytes());
7 9 10 11 }
注意,SocketChannel.write()是放在while循环体中的。由于无法保证write()方法将多少字节写入SocketChannel,因此,要重复调用write()方法,直到缓冲区没有字节可写为止。 5.3. 非阻塞模式 当一个SocketChannel被设置为非阻塞模式时,你就可以异步地调用connect(),read(),write()方法了。 connect()? 如果SocketChannel是非阻塞模式,那么当你调用connect()方法时,该方法可能在建立连接之前返回。为了确定连接是否已经成功建立,可以调用finishConnect()方法。 (! finishConnect() ){
5 //wait,or do something else...
6 }
write() 和 read() 在非阻塞模式下,write()方法可能会在未写入任何内容的情况下返回,因此需要在循环中调用write()。同样的,在非阻塞模式下,read()方法可能在没有读取任何数据的情况下就返回了,因此,需要注意返回的int,它告诉我们读取了多少字节。? 6. ServerSocketChannel Java NIO ServerSocketChannel是一个可以监听输入TCP连接的通道,就像标准Java网络中的ServerSocket一样。? ServerSocketChannel serverSocketChannel ServerSocketChannel2
serverSocketChannelsocket().bind99995 6 accept7 //do something with socketChannel...
8 }
6.1. Listening for Incoming Connections 通过调用ServerSocketChannel.accept()方法可以监听输入的连接。当accept()方法有返回的时候,它返回一个带有输入连接的SocketChannel。因此,accept()会阻塞直到有输入连接到来为止。通常的做法是这样的: 1 2 3 4 6.2. 非阻塞模式
ServerSocketChannel可以设置为非阻塞模式。在非阻塞模式下,调用accept()方法会立即返回,因此如果没有输入连接到达,它返回的可能是null。因此,必须检查返回的SocketChannel是否为null,下面是一个例子: 4 );
5
6 7 9 if!= null){
10 11 }
12 7. Java NIO vs. IO
? 7.1. Stream Oriented vs. Buffer Oriented Java NIO和IO之间的第一个大区别是IO是面向流的,而NIO是面向缓冲区的。 那是什么意思呢? 面向流的Java IO意味着你一次从流中读取一个或多个字节。如何处理读取的字节由你自己决定。它们不会被缓存到任何地方。此外,你不能在流中的数据中来回移动。如果需要来回移动从流中读取的数据,需要首先将其缓存到缓冲区中。 Java NIO的面向缓冲区的方法略有不同。数据被读入缓冲区,以后再从缓冲区中进行处理。你可以根据需要在缓冲区中来回移动。这使得在处理过程中更具灵活性。但是,你还需要检查缓冲区是否包含你需要的所有数据,以便对其进行完全处理。并且,你需要确保在将更多数据读入缓冲区时,不会覆盖缓冲区中尚未处理的数据。 7.2. Blocking vs. Non-blocking IO Java IO的各种流被阻塞。这意味着,当线程调用read()或write()时,该线程将被阻塞,直到有一些数据需要读取,或者数据被完全写入。 在此期间,线程无法执行其他任何操作。 Java NIO的非阻塞模式允许线程请求从通道读取数据,并且只获取当前可用的数据,如果当前没有可用的数据,则什么也得不到。在数据可以读取之前,线程不会一直处于阻塞状态,而是可以继续执行其他操作。 非阻塞写入也是如此。线程可以请求将某些数据写入通道,但不等待将其完全写入。然后线程可以继续运行,同时执行其他操作。 当线程在IO调用中没有被阻塞时,它们的空闲时间通常在其他通道上执行IO。也就是说,单个线程现在可以管理输入和输出的多个通道。 7.3. Selectors Java NIO的Selector允许单个线程监视多个输入通道。可以使用Selector注册多个通道,然后使用一个线程“select”具有可用于处理输入的通道,或者选择准备好进行写入的通道。这种选择器机制使单个线程可以轻松管理多个通道。 7.4. 不同的数据读取方式 Java IO: Reading data from a blocking stream? Java NIO: Reading data from a channel until all needed data is in buffer 8. 示例 ChatServer.java 1 package comcjs 3 import java.io.IOException 4 java.net.InetSocketAddress 5 java.nio.ByteBuffer 6 java.nio.channels.SelectionKey 7 java.nio.channels.Selector 8 java.nio.channels.ServerSocketChannel 9 java.nio.channels.SocketChannel10 java.nio.charset.Charsetjava.util.Iteratorjava.util.Set14 public class ChatServer 16 private 17 18
19 readBuffer 20 writeBuffer 22 private static final Charset CHARSET CharsetforName"UTF-8"23
24 public ChatServerport25 try 26 27 28 29
30 31 OP_ACCEPT32
catch IOException e34 printStackTrace35 37
38 /**
39 * 监听客户端连接
40 */
41 void listen() throws IOException 42 println"服务器启动成功"43 44
45 46
47 48 49 50
51 52
53 54
55 56
57 58
59 60 61 62 63 64 CHARSETencode"来自服务器的问候: 你好!"65 66 67 68 69 = 70 msg ""71 readBuffer> 72 73 += decode).toString74 75 msg76 77
78 // 给客户端回复消息
79 writeBuffer"服务器对你说: 收到"80 81 82 83
84 85 86 87
88 89 90 91 92
93 mainString[] args94 ChatServer chatServer 95 chatServer96 97 }
ChatClient.java 1 2
3 4 5 6 7 8 9 10 11 java.util.Scanner 12
13 ChatClient 14
15 16 17
18 19
20 21
22 ChatClienthost 23 24 25 26 27
28 (! 29 "正在等待连接" 30 31
32 "连接成功" 33
34 35 36
37 new ThreadHandler)).start 38
39 40 41 42 43
44 45 * 给服务器发消息
46 47 48 Scanner scanner Scannerin 49 scanner 50 line nextLine 51 null != && !equalslinetrim())) 52 53 54 55 56
57 58 * 接收来自服务器的消息
59 60 Handler implements Runnable 61
62 63
64 65 this 66 67
68 @Override
69 run 70 71 72
73 74
75 76
77 78
79 80
81 82
83 84 85 86 87 // 从写模式切换为读模式
88 89 90 91 92 93 94
95 96 97 98 99 100 101 102 103
104 105 ChatClient chatClient "127.0.0.1"106 chatClient107 108 }?
控制台 9. 参考 http://tutorials.jenkov.com/java-nio/index.html http://tutorials.jenkov.com/java-nio/socketchannel.html? (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |