更多 Java IO & NIO方面的文章,請參見文集《Java IO & NIO》
Java IO VS NIO
- JDK 1.4 之前,java.io 包,面向流的I/O系統(tǒng)(字節(jié)流或者字符流)
- 系統(tǒng)一次處理一個(gè)字節(jié)
- 速度慢
- JDK 1.4 提供,java.nio 包,面向塊的I/O系統(tǒng)
- 系統(tǒng)一次處理一個(gè)塊
- 速度快
Buffer 緩沖區(qū)
緩沖區(qū)實(shí)際上是一個(gè)容器對象,更直接的說,其實(shí)就是一個(gè)數(shù)組。
在 NIO 庫中,所有數(shù)據(jù)都是用緩沖區(qū)處理的:
- 在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的;
- 在寫入數(shù)據(jù)時(shí),它也是寫入到緩沖區(qū)中的;
在 NIO 中,所有的緩沖區(qū)類型都繼承于抽象類 Buffer。常見的緩沖區(qū) Buffer 包括:
- ByteBuffer 存儲(chǔ)了字節(jié)數(shù)組
final byte[] hb;
- CharBuffer
final char[] hb;
- ByteBuffer 與 CharBuffer 之間的轉(zhuǎn)換需要使用字符集 Charset
- Charset 具體使用,參見 Java Charset 字符集
- ShortBuffer
final short[] hb;
- IntBuffer
final int[] hb;
- LongBuffer
final long[] hb;
- FloatBuffer
final float[] hb;
- DoubleBuffer
final double[] hb;
Buffer 類的屬性:
-
private int mark = -1;
記錄一個(gè)標(biāo)記位置 private int position = 0;
A buffer's <i>position</i> is the index of the next element to be read or written. A buffer's position is never negative and is never greater than its limit.
當(dāng)前操作的位置
private int limit;
A buffer's <i>limit</i> is the index of the first element that should not be read or written. A buffer's limit is never negative and is never greater than its capacity.
可以存放的元素的個(gè)數(shù)
private int capacity;
A buffer's <i>capacity</i> is the number of elements it contains. The capacity of a buffer is never negative and never changes.
數(shù)組容量
- 大小關(guān)系:mark <= position <= limit <= capacity
Buffer 類的方法:
-
allocate(int capacity)
分配一個(gè)緩沖區(qū),默認(rèn) limit = capacity -
put()
在當(dāng)前位置添加元素 -
get()
得到當(dāng)前位置的元素 -
clear()
將 Buffer 從 讀模式 切換到 寫模式 (該方法實(shí)際不會(huì)清空原 Buffer 的內(nèi)容)
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
-
flip()
將 Buffer 從 寫模式 切換到 讀模式
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
** clear()
VS flip()
**:
- 在寫模式下,Buffer 的 limit 表示你最多能往 Buffer 里寫多少數(shù)據(jù)。
- 因此寫之前,調(diào)用
clear()
,使得limit = capacity;
- 因此寫之前,調(diào)用
- 在讀模式時(shí),Buffer 的 limit 表示你最多能從 Buffer 里讀多少數(shù)據(jù)。
- 因此讀之前,調(diào)用
flip()
,使得limit = position;
- 因此讀之前,調(diào)用
IntBuffer 的使用:
public static void main(String[] args) throws Exception {
// 創(chuàng)建 int 緩沖區(qū) capacity 為 4
// 默認(rèn) limit = capacity
IntBuffer buffer = IntBuffer.allocate(4);
System.out.println("Capacity & Limit: " + buffer.capacity() + " " + buffer.limit());
// 往 Buffer 中寫數(shù)據(jù)
buffer.put(11);
buffer.put(22);
buffer.put(33);
buffer.put(44);
System.out.println("Position: " + buffer.position());
// 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print(buffer.get() + " ");
}
}
輸出:
Capacity & Limit: 4 4
Position: 4
11 22 33 44
Channel 通道
- Java NIO 的核心概念,表示的是對支持 I/O 操作的實(shí)體的一個(gè)連接
- 通過它可以讀取和寫入數(shù)據(jù)(并不是直接操作,而是通過 Buffer 來處理)
- 雙向的
常用的 Channel 包括:
- FileChannel 從文件中讀寫數(shù)據(jù)
- DatagramChannel 從 UDP 中讀寫數(shù)據(jù)
- SocketChannel 從 TCP 中讀寫數(shù)據(jù)
- ServerSocketChannel 監(jiān)聽新進(jìn)來的 TCP 連接,每一個(gè)新進(jìn)來的連接都會(huì)創(chuàng)建一個(gè) SocketChannel。
FileChannel 連接到文件的通道
FileChannel 無法設(shè)置為非阻塞模式,只能運(yùn)行在阻塞模式下
常用方法:
-
int read(ByteBuffer dst)
從 Channel 中讀取數(shù)據(jù),寫入 Buffer -
int write(ByteBuffer src)
從 Buffer 中讀取數(shù)據(jù),寫入 Channel -
long size()
得到 Channel 中文件的大小 -
long position()
得到 Channel 中文件的當(dāng)前操作位置 -
FileChannel position(long newPosition)
設(shè)置 Channel 中文件的當(dāng)前操作位置
使用 FileChannel 來復(fù)制文件的例子:
public static void main(String[] args) throws Exception {
// 通過 InputStream 或者 OutputStream 來構(gòu)造 FileChannel
FileChannel in = new FileInputStream("a.txt").getChannel();
FileChannel out = new FileOutputStream("b.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 調(diào)用 channel 的 read 方法往 Buffer 中寫數(shù)據(jù)
while(in.read(buffer) != -1) {
// 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
buffer.flip();
// 從 Buffer 中讀數(shù)據(jù),寫入到 channel
out.write(buffer);
// 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
buffer.clear();
}
// 或者使用如下代碼
// out.transferFrom(in, 0, in.size());
}
SocketChannel 連接到 TCP 套接字的通道
SocketChannel 可以設(shè)置為阻塞模式或非阻塞模式
使用 SocketChannel 來建立 TCP 連接,發(fā)送并接收數(shù)據(jù),默認(rèn)使用 阻塞模式:
public static void main(String[] args) throws Exception {
// 打開 SocketChannel
SocketChannel channel = SocketChannel.open();
// connect 方法會(huì)阻塞,直至連接建立成功
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 發(fā)送數(shù)據(jù)
String msg = "This is client.";
// 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
buffer.clear();
buffer.put(msg.getBytes());
// 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
buffer.flip();
channel.write(buffer);
// 接收數(shù)據(jù)
// 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
buffer.clear();
// 調(diào)用 channel 的 read 方法往 Buffer 中寫數(shù)據(jù)
channel.read(buffer);
// 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
buffer.flip();
// 從 Buffer 中讀數(shù)據(jù)
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
}
使用 SocketChannel 的 非阻塞模式 來建立 TCP 連接,發(fā)送并接收數(shù)據(jù):
public static void main(String[] args) throws Exception {
// 打開 SocketChannel
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (!channel.finishConnect()) {
// 發(fā)送數(shù)據(jù)
String msg = "This is client.";
// 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
buffer.clear();
buffer.put(msg.getBytes());
// 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
buffer.flip();
channel.write(buffer);
// 接收數(shù)據(jù)
// 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
buffer.clear();
// 調(diào)用 channel 的 read 方法往 Buffer 中寫數(shù)據(jù)
channel.read(buffer);
// 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
buffer.flip();
// 從 Buffer 中讀數(shù)據(jù)
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
}
}
ServerSocketChannel 監(jiān)聽 TCP 連接的通道
ServerSocketChannel 可以設(shè)置為阻塞模式或非阻塞模式
使用 ServerSocketChannel 來監(jiān)聽 TCP 連接,默認(rèn)使用 阻塞模式:
public static void main(String[] args) throws Exception {
// 打開 SocketChannel
ServerSocketChannel channel = ServerSocketChannel.open();
// 綁定端口
channel.socket().bind(new InetSocketAddress(8080));
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
// accept 方法會(huì)阻塞,直至監(jiān)聽到 TCP 連接
SocketChannel socketChannel = channel.accept();
System.out.println("A new connection...");
// 接收數(shù)據(jù)
// 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
buffer.clear();
// 調(diào)用 channel 的 read 方法往 Buffer 中寫數(shù)據(jù)
socketChannel.read(buffer);
// 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
buffer.flip();
// 從 Buffer 中讀數(shù)據(jù)
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
// 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
// 發(fā)送數(shù)據(jù)
String msg = "This is server.";
// 在往 Buffer 中寫數(shù)據(jù)之前,調(diào)用 clear()
buffer.clear();
buffer.put(msg.getBytes());
// 在從 Buffer 中讀數(shù)據(jù)之前,調(diào)用 flip()
buffer.flip();
socketChannel.write(buffer);
}
}
Selector 選擇器
Selector 允許單個(gè)進(jìn)程可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的 IO,即監(jiān)聽多個(gè)端口的 Channel。
關(guān)于 IO 模式,參見 Linux IO 模型 中對多路復(fù)用 IO Multiplexing IO 的說明。
引用:
多路復(fù)用 IO Multiplexing IO
- 單個(gè)進(jìn)程可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的 IO,即監(jiān)聽多個(gè)端口的 IO
- 適用于連接數(shù)很高的情況
- 實(shí)現(xiàn)方式:select,poll,epoll 系統(tǒng)調(diào)用
- 注冊多個(gè)端口的監(jiān)聽 Socket,比如 8080,8081
- 當(dāng)用戶進(jìn)程調(diào)用 select 方法后,整個(gè)用戶進(jìn)程被阻塞,OS 內(nèi)核會(huì)監(jiān)聽所有注冊的 Socket
- 當(dāng)任何一個(gè)端口的 Socket 中的數(shù)據(jù)準(zhǔn)備好了( 8080 或者 8081),select 方法就會(huì)返回
- 隨后用戶進(jìn)程再調(diào)用 read 操作,將數(shù)據(jù)從 OS 內(nèi)核緩存區(qū)拷貝到應(yīng)用程序的地址空間。
-
多路復(fù)用 IO 類似于 多線程結(jié)合阻塞 IO
- 要實(shí)現(xiàn)監(jiān)聽多個(gè)端口的 IO,還可以通過多線程的方式,每一個(gè)線程負(fù)責(zé)監(jiān)聽一個(gè)端口的 IO
- 如果處理的連接數(shù)不是很高的話,使用 多路復(fù)用 IO 不一定比使用 多線程結(jié)合阻塞 IO 的服務(wù)器性能更好,可能延遲還更大
- 多路復(fù)用 IO 的優(yōu)勢并不是對于單個(gè)連接能處理得更快,而是在于能處理更多的連接
Selector 使用步驟:
- 創(chuàng)建 Selector
- 創(chuàng)建 Channel,可以創(chuàng)建多個(gè) Channel,即監(jiān)聽多個(gè)端口,比如 8080,8081
-
將 Channel 注冊到 Selector 中
- 如果一個(gè) Channel 要注冊到 Selector 中, 那么這個(gè) Channel 必須是非阻塞的, 即
channel.configureBlocking(false);
- 因此 FileChannel 是不能夠使用 Selector 的, 因?yàn)?FileChannel 都是阻塞的
- 注冊時(shí),需要指定了對 Channel 的什么事件感興趣,包括:
- SelectionKey.OP_CONNECT:TCP 連接
static final int OP_CONNECT = 1 << 3;
- SelectionKey.OP_ACCEPT:確認(rèn)
static final int OP_ACCEPT = 1 << 4;
- SelectionKey.OP_READ:讀
static final int OP_READ = 1 << 0;
- SelectionKey.OP_WRITE:寫
static final int OP_WRITE = 1 << 2;
- 可以使用或運(yùn)算 | 來組合,例如
SelectionKey.OP_READ | SelectionKey.OP_WRITE
- SelectionKey.OP_CONNECT:TCP 連接
- register 方法返回一個(gè) SelectionKey 對象,包括:
-
int interestOps()
:調(diào)用 register 注冊 channel 時(shí)所設(shè)置的 interest set. -
int readyOps()
:Channel 所準(zhǔn)備好了的操作selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
-
public abstract SelectableChannel channel();
: 得到 Channel -
public abstract Selector selector();
:得到 Selector -
public final Object attachment
:得到附加對象
-
- 如果一個(gè) Channel 要注冊到 Selector 中, 那么這個(gè) Channel 必須是非阻塞的, 即
-
不斷重復(fù):
- 調(diào)用 Selector 對象的 select() 方法,該方法會(huì)阻塞,直至注冊的事件發(fā)生
- 事件發(fā)生,調(diào)用 Selector 對象的 selectedKeys() 方法獲取 selected keys
- 遍歷每個(gè) selected key:
- 從 selected key 中獲取對應(yīng)的 Channel 并處理
- 在 OP_ACCEPT 事件中, 從 key.channel() 返回的是 ServerSocketChannel
- 在 OP_WRITE 和 OP_READ 事件中, 從 key.channel() 返回的是 SocketChannel
- 關(guān)閉 Selector
示例:
public static void main(String args[]) throws Exception {
// 創(chuàng)建 Selector
Selector selector = Selector.open();
// 創(chuàng)建 Server Socket,監(jiān)聽端口 8080
ServerSocketChannel serverChannel1 = ServerSocketChannel.open();
serverChannel1.socket().bind(new InetSocketAddress(8080));
// 如果一個(gè) Channel 要注冊到 Selector 中, 那么這個(gè) Channel 必須是非阻塞的
serverChannel1.configureBlocking(false);
// 創(chuàng)建 Server Socket,監(jiān)聽端口 8081
ServerSocketChannel serverChannel2 = ServerSocketChannel.open();
serverChannel2.socket().bind(new InetSocketAddress(8081));
// 如果一個(gè) Channel 要注冊到 Selector 中, 那么這個(gè) Channel 必須是非阻塞的
serverChannel2.configureBlocking(false);
// 將 Channel 注冊到 Selector 中
serverChannel1.register(selector, SelectionKey.OP_ACCEPT);
serverChannel2.register(selector, SelectionKey.OP_ACCEPT);
// 不斷重復(fù)
while (true) {
// 調(diào)用 Selector 對象的 select() 方法,該方法會(huì)阻塞,直至注冊的事件發(fā)生
selector.select();
// 事件發(fā)生,調(diào)用 Selector 對象的 selectedKeys() 方法獲取 selected keys
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
// 遍歷每個(gè) selected key:
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isAcceptable()) {
// 在 OP_ACCEPT 事件中, 從 key.channel() 返回的是 ServerSocketChannel
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 調(diào)用 accept 方法獲取 TCP 連接 SocketChanne
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 注冊 SocketChannel
clientChannel.register(key.selector(), SelectionKey.OP_READ | SelectionKey.OP_WRITE);
System.out.println("Accept event");
}
if (key.isReadable()) {
// 在 OP_WRITE 和 OP_READ 事件中, 從 key.channel() 返回的是 SocketChannel
SocketChannel clientChannel = (SocketChannel) key.channel();
System.out.println("Read event");
// 可以從 clientChannel 中讀數(shù)據(jù),通過 ByteBuffer
// TO DO
}
if (key.isWritable()) {
// 在 OP_WRITE 和 OP_READ 事件中, 從 key.channel() 返回的是 SocketChannel
SocketChannel clientChannel = (SocketChannel) key.channel();
System.out.println("Write event");
// 可以向 clientChannel 中寫數(shù)據(jù),通過 ByteBuffer
// TO DO
}
}
}
}