NIO详解(九):Channel详解
1. Channel概述
Java NIO的通道類似流,但又有些不同。
通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。
1.1 Channel 的實現
這些是Java NIO中最重要的通道的實現:
- FileChannel: FileChannel 從文件中讀寫數據。
- DatagramChannel:DatagramChannel 能通過UDP讀寫網絡中的數據。
- SocketChannel:SocketChannel 能通過TCP讀寫網絡中的數據。
- ServerSocketChannel:ServerSocketChannel可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。
2. Channel 數據傳輸之Scatter/Gather模式
Java NIO開始支持scatter/gather,scatter/gather用于描述從Channe中讀取或者寫入到Channel的操作。
分散(scatter)從Channel中讀取是指在讀操作時將讀取的數據寫入多個buffer中。因此,Channel將從Channel中讀取的數據“分散(scatter)”到多個Buffer中。
聚集(gather)寫入Channel是指在寫操作時將多個buffer的數據寫入同一個Channel,因此,Channel 將多個Buffer中的數據“聚集(gather)”后發送到Channel。
scatter / gather經常用于需要將傳輸的數據分開處理的場合,例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不同的buffer中,這樣你可以方便的處理消息頭和消息體。
2.1 Scattering Reads
Scattering Reads是指數據從一個channel讀取到多個buffer中。如下圖描述:
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024);ByteBuffer[] bufferArray = { header, body };channel.read(bufferArray);buffer首先被插入到數組,然后再將數組作為channel.read() 的輸入參數。read()方法按照buffer在數組中的順序將從channel中讀取的數據寫入到buffer,當一個buffer被寫滿后,channel緊接著向另一個buffer中寫。Scattering Reads在移動下一個buffer前,必須填滿當前的buffer,這也意味著它不適用于動態消息(譯者注:消息大小不固定)。換句話說,如果存在消息頭和消息體,消息頭必須完成填充(例如 128byte),Scattering Reads才能正常工作。
2.2 Gathering Writes
Gathering Writes是指數據從多個buffer寫入到同一個channel。如下圖描述:
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024);//write data into buffersByteBuffer[] bufferArray = { header, body };channel.write(bufferArray);buffers數組是write()方法的入參,write()方法會按照buffer在數組中的順序,將數據寫入到channel。
3. ServerSocketChannel
Java NIO中的 ServerSocketChannel 是一個可以監聽新進來的TCP連接的通道, 就像標準IO中的ServerSocket一樣。ServerSocketChannel類在 java.nio.channels包中。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(9999));while(true){SocketChannel socketChannel =serverSocketChannel.accept();//do something with socketChannel... }3.1 打開 ServerSocketChannel
通過調用 ServerSocketChannel.open() 方法來打開ServerSocketChannel.如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();3.2 關閉 ServerSocketChannel
通過調用ServerSocketChannel.close() 方法來關閉ServerSocketChannel. 如:
serverSocketChannel.close();3.3 監聽新進來的連接
通過 ServerSocketChannel.accept() 方法監聽新進來的連接。當 accept()方法返回的時候,它返回一個包含新進來的連接的 SocketChannel。因此, accept()方法會一直阻塞到有新連接到達。
通常不會僅僅只監聽一個連接,在while循環中調用 accept()方法. 如下面的例子:
while(true){SocketChannel socketChannel =serverSocketChannel.accept();//do something with socketChannel... }3.4 ServerSocketChannel非阻塞模式
ServerSocketChannel可以設置成非阻塞模式。在非阻塞模式下,accept() 方法會立刻返回,如果還沒有新進來的連接,返回的將是null。 因此,需要檢查返回的SocketChannel是否是null.
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(9999)); serverSocketChannel.configureBlocking(false);while(true){SocketChannel socketChannel =serverSocketChannel.accept();if(socketChannel != null){//do something with socketChannel...} }4. SocketChannel
Java NIO中的SocketChannel是一個連接到TCP網絡套接字的通道。可以通過以下2種方式創建SocketChannel:
4.1 打開SocketChannel
下面是SocketChannel的打開方式:
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("192.168.1.1", 80));4.2 關閉 SocketChannel
當用完SocketChannel之后調用SocketChannel.close()關閉SocketChannel:
socketChannel.close();4.3 從 SocketChannel 讀取數據
要從SocketChannel中讀取數據,調用一個read()的方法之一。以下是例子:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = socketChannel.read(buf);首先,分配一個Buffer。從SocketChannel讀取到的數據將會放到這個Buffer中。然后,調用SocketChannel.read()。該方法將數據從SocketChannel 讀到Buffer中。read()方法返回的int值表示讀了多少字節進Buffer里。如果返回的是-1,表示已經讀到了流的末尾(連接關閉了)。
4.4 寫入 SocketChannel 寫入數據
寫數據到SocketChannel用的是SocketChannel.write()方法,該方法以一個Buffer作為參數。示例如下:
String newData = "New String to write to file..." + System.currentTimeMillis();ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes());buf.flip();while(buf.hasRemaining()) {channel.write(buf); }注意SocketChannel.write()方法的調用是在一個while循環中的。Write()方法無法保證能寫多少字節到SocketChannel。所以,我們重復調用write()直到Buffer沒有要寫的字節為止。
4.5 非阻塞模式
可以設置 SocketChannel 為非阻塞模式(non-blocking mode).設置之后,就可以在異步模式下調用connect(), read() 和write()了。
connect()
如果SocketChannel在非阻塞模式下,此時調用connect(),該方法可能在連接建立之前就返回了。為了確定連接是否建立,可以調用finishConnect()的方法。像這樣:
socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("192.168.1.1", 80));while(! socketChannel.finishConnect() ){//wait, or do something else... }write()
非阻塞模式下,write()方法在尚未寫出任何內容時可能就返回了。所以需要在循環中調用write()。前面已經有例子了,這里就不贅述了。
read()
非阻塞模式下,read()方法在尚未讀取到任何數據時可能就返回了。所以需要關注它的int返回值,它會告訴你讀取了多少字節。
非阻塞模式與選擇器
非阻塞模式與選擇器搭配會工作的更好,通過將一或多個SocketChannel注冊到Selector,可以詢問選擇器哪個通道已經準備好了讀取,寫入等。Selector與SocketChannel的搭配使用會在后面詳講。
5. FileChannel
Java NIO中的FileChannel是一個連接到文件的通道。可以通過文件通道讀寫文件。FileChannel無法設置為非阻塞模式,他總是運行在阻塞模式下。
5.1 打開FileChannel
在使用FileChannel之前,必須先打開它。但是,我們無法直接打開一個FileChannel,需要通過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例。下面是通過RandomAccessFile打開FileChannel的示例:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel();5.2 從FileChannel讀取數據
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);首先,分配一個Buffer。從FileChannel中讀取的數據將被讀到Buffer中。然后,調用FileChannel.read()方法。該方法將數據從FileChannel讀取到Buffer中。read()方法返回的int值表示了有多少字節被讀到了Buffer中。如果返回-1,表示到了文件末尾。
5.3 向FileChannel寫數據
使用FileChannel.write()方法向FileChannel寫數據,該方法的參數是一個Buffer。如:
String newData = "New String to write to file..." + System.currentTimeMillis();ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes());buf.flip();while(buf.hasRemaining()) {channel.write(buf); }注意FileChannel.write()是在while循環中調用的。因為無法保證write()方法一次能向FileChannel寫入多少字節,因此需要重復調用write()方法,直到Buffer中已經沒有尚未寫入通道的字節。
5.4 關閉FileChannel
用完FileChannel后必須將其關閉。如:
channel.close();5.5 FileChannel的position方法
有時可能需要在FileChannel的某個特定位置進行數據的讀/寫操作。可以通過調用position()方法獲取FileChannel的當前位置。也可以通過調用position(long pos)方法設置FileChannel的當前位置。
long pos = channel.position(); channel.position(pos +123);如果將位置設置在文件結束符之后,然后試圖從文件通道中讀取數據,讀方法將返回-1 —— 文件結束標志。
5.6 FileChannel的size方法
FileChannel實例的size()方法將返回該實例所關聯文件的大小。如:
long fileSize = channel.size();5.7 FileChannel的truncate方法
可以使用FileChannel.truncate()方法截取一個文件。截取文件時,文件將中指定長度后面的部分將被刪除。如:
channel.truncate(1024);這個例子截取文件的前1024個字節。
5.8 FileChannel的force方法
FileChannel.force()方法將通道里尚未寫入磁盤的數據強制寫到磁盤上。出于性能方面的考慮,操作系統會將數據緩存在內存中,所以無法保證寫入到FileChannel里的數據一定會即時寫到磁盤上。要保證這一點,需要調用force()方法。
force()方法有一個boolean類型的參數,指明是否同時將文件元數據(權限信息等)寫到磁盤上。
下面的例子同時將文件數據和元數據強制寫到磁盤上:
channel.force(true);6. DatagramChannel
Java NIO中的DatagramChannel是一個能收發UDP包的通道。因為UDP是無連接的網絡協議,所以不能像其它通道那樣讀取和寫入。它發送和接收的是數據包。
6.1 打開 DatagramChannel
下面是 DatagramChannel 的打開方式:
DatagramChannel channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(9999));6.2 DatagramChannel接收數據
通過receive()方法從DatagramChannel接收數據,如:
ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); channel.receive(buf);receive()方法會將接收到的數據包內容復制到指定的Buffer. 如果Buffer容不下收到的數據,多出的數據將被丟棄。
6.3 DatagramChannel發送數據
通過send()方法從DatagramChannel發送數據,如:
String newData = "New String to write to file..." + System.currentTimeMillis();ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip();int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));這個例子發送一串字符到”jenkov.com”服務器的UDP端口80。 因為服務端并沒有監控這個端口,所以什么也不會發生。也不會通知你發出的數據包是否已收到,因為UDP在數據傳送方面沒有任何保證。
6.4 DatagramChannel連接到特定的地址
可以將DatagramChannel“連接”到網絡中的特定地址的。由于UDP是無連接的,連接到特定地址并不會像TCP通道那樣創建一個真正的連接。而是鎖住DatagramChannel ,讓其只能從特定地址收發數據。
channel.connect(new InetSocketAddress("192.168.1.1", 80));當連接后,也可以使用read()和write()方法,就像在用傳統的通道一樣。只是在數據傳送方面沒有任何保證。這里有幾個例子:
int bytesRead = channel.read(buf); int bytesWritten = channel.write(but);總結
以上是生活随笔為你收集整理的NIO详解(九):Channel详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NIO详解(五):Buffer详解
- 下一篇: NIO详解(九):Selector详解