由上篇文章中知道,通道Channel
和緩沖器Buffer
兩者需要共同作用,但是選擇器Selector
只會作用在繼承了抽象類SelectableChannel
的網(wǎng)絡IO中,下面由簡單的FileChannel
開始了解nio
包的使用。
FileChannel
之前已經(jīng)說過,在java 1.4的時候,改寫了傳統(tǒng)IO包下的三個類用以生成通道類FileChannel
,這里使用緩沖器類ByteBuffer
來對文件進行操作,如下所示:
/**
**FileChannel示例 文件拷貝
**/
public static void main(String[] arg0) throws IOException{
FileChannel inputChanel = new FileInputStream("/test/test.text").getChannel();
FileChannel outputChanel = new FileOutputStream("/test/test1.text").getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = inputChanel.read(buf))!= -1){
buf.flip();
outputChanel.write(buf);
buf.clear();
}
}
如上所示,代碼會將test.text中的數(shù)據(jù)拷貝到test1.text中,其中將通道中的數(shù)據(jù)讀取到ByteBuffer
后,經(jīng)過三個步驟,即flip()
方法調(diào)用,調(diào)用寫通道outputChannel
的write()
方法,最后調(diào)用ByteBuffer
的clear()
方法。雖說完成了功能,但是這種方法的效率并不是很高的,因為每一次讀的時候都需要在while
循環(huán)中調(diào)用了幾步。在FileChannel
類中還有兩個方法transferFrom(ReadableByteChannel src,long position, long count)
和transferTo(long position, long count,WritableByteChannel target)
,可以通過這兩個方法來做到文件拷貝,如下所示:
/**
**FileChannel示例 文件拷貝 transferTo() & transferFrom()
**/
public static void main(String[] arg0) throws IOException{
FileChannel inputChanel = new FileInputStream("/test/nio1.text").getChannel();
FileChannel outputChanel = new FileOutputStream("/test/test.text").getChannel();
// inputChanel.transferTo(0, inputChanel.size(), outputChanel);
outputChanel.transferFrom(inputChanel, 0, inputChanel.size());
}
上面的例子運行完之后,通過將兩個通道連接也能正確的拷貝文件。需要注意的是文中的代碼為了簡便全部是直接拋出異常也沒有關閉流,如果實際書寫請酌情處理。看了FileChannel
操作普通文件,那么可以看一下怎么操作大文件的,記得之前說過這樣一個類MappedByteBuffer
,使用它即可快速操作,如下:
/**
**MappedByteBuffer示例 操作文件
**/
public static void main(String[] arg0) throws IOException{
FileChannel randomAccessChannel = new RandomAccessFile("/test/nio1.text", "rw").getChannel();
FileChannel outputChannel = new FileOutputStream("/test/test.text").getChannel();
long size = randomAccessChannel.size();
MappedByteBuffer mapBuffer = randomAccessChannel.map(MapMode.READ_ONLY, 0, size);
int len = 1024;
byte[] buf = new byte[len];
long cycle = size/len;
while(mapBuffer.hasRemaining()&&cycle>=0){
if(cycle==0){
len = (int)size % len;
}
mapBuffer.get(buf,0,len);
System.out.println("--"+new String(buf,0,len));
cycle--;
}
}
如上通過類FileChannel
映射文件,會打印出文件中的所有數(shù)據(jù)。在這里對于速度的提升可能無法看出,但是換一個大文件對比一下普通的ByteBuffer
就可以很容易的看出來,如下所示:
/**
**MappedByteBuffer對比普通ByteBuffer
**/
public static void main(String[] arg0) throws IOException{
FileChannel randomAccessChannel = new RandomAccessFile("/test/cpicgxwx.war", "r").getChannel();
long size = randomAccessChannel.size();
long cur = System.currentTimeMillis();
MappedByteBuffer mapBuffer = randomAccessChannel.map(MapMode.READ_ONLY, 0, size);
System.out.println("MappedByteBuffer spend "+(System.currentTimeMillis()-cur));
ByteBuffer buf = ByteBuffer.allocate(1024*1024*150);
cur = System.currentTimeMillis();
randomAccessChannel.read(buf);
System.out.println("ByteBuffer spend "+(System.currentTimeMillis()-cur));
}
輸出:
MappedByteBuffer spend 4
ByteBuffer spend 259
這樣很明顯就看的出來兩者的效率,對于一個大于100M的文件,MappedByteBuffer
比ByteBuffer
效率高了60多倍,更不用說幾個G大小的文件了。
這里會不會有疑惑,為什么類MappedByteBuffer
可以這么快?其實是因為它并不是讀取文件到內(nèi)存中而是像類名Mapped
一樣映射文件。當然這個類其實也存在一些問題,如內(nèi)存占用和文件關閉,被類MappedByteBuffer
打開的文件只有在垃圾收集的時候才關閉,而這個垃圾收集的點是不確定的,在網(wǎng)上有人提供了這樣一個方法來關閉這個文件映射,如下所示:
/**
**MappedByteBuffer 文件映射 Clear()
**/
public static void main(String[] arg0) throws Exception{
File file = new File("/test/nio.text");
RandomAccessFile randomAccessFile = new RandomAccessFile(file,"r");
FileChannel channel = randomAccessFile.getChannel();
long size = channel.size();
long cur = System.currentTimeMillis();
MappedByteBuffer mapBuffer = channel.map(MapMode.READ_ONLY, 0, size);
System.out.println("MappedByteBuffer spend "+(System.currentTimeMillis()-cur));
ByteBuffer buf = ByteBuffer.allocate(1024*1024*150);
cur = System.currentTimeMillis();
channel.read(buf);
System.out.println("ByteBuffer spend "+(System.currentTimeMillis()-cur));
channel.close();
randomAccessFile.close();
// clean(mapBuffer);
System.out.println(file.delete());
}
public static void clean(final Object buffer) throws Exception {
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
try {
Method getCleanerMethod = buffer.getClass().getMethod("cleaner",new Class[0]);
getCleanerMethod.setAccessible(true);
sun.misc.Cleaner cleaner =(sun.misc.Cleaner)getCleanerMethod.invoke(buffer,new Object[0]);
cleaner.clean();
} catch(Exception e) {
e.printStackTrace();
}
return null;}});
}
輸出:
MappedByteBuffer spend 0
ByteBuffer spend 157
false
如上所示:在注銷掉clean(mapBuffer);
時,由于系統(tǒng)還持有這個文件的句柄,無法刪除文件,導致打印出false
,加上這個方法調(diào)用后就可以了。除了反射調(diào)用sun.misc.Cleaner cleaner
外,還可以直接調(diào)用,如下:
/**
**sun.misc.Cleaner調(diào)用
**/
public static void unmap(MappedByteBuffer buffer){
sun.misc.Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
cleaner.clean();
}
這樣調(diào)用也是可以刪除文件的,這樣就補足了上面所說的問題。
在java 1.4還加入了文件加鎖機制FileLock
,它允許我們同步訪問某個作為共享資源的文件,這個文件鎖直接通過映射本地操作系統(tǒng)的加鎖工具,所以對其他操作系統(tǒng)的進程也是可見的。這個文件鎖可以通過FileChannel
的tryLock()
或lock()
方法獲取,當然也提供了有參數(shù)的方法來加鎖文件的一部分,如lock(long position, long size, boolean shared)
和tryLock(long position, long size, boolean shared)
,這里參數(shù)boolean shared
指是否共享鎖。這個就不放實例了,感興趣可以試試。
下面看看其他的網(wǎng)絡Channel
。
SocketChannel
在上一篇就說道NIO的強大功能部分來自它的非阻塞特性,這一點在網(wǎng)絡IO中效果表現(xiàn)的更加明顯,如對ServerSocket
的accept()
方法會等待某一個客戶端連接而導致阻塞,或者InputStream
的read
方法阻塞到數(shù)據(jù)完全讀完。一般來說,我們在調(diào)用一個方法之前并不知道它是不是會阻塞,但是NIO提供了這樣的方法來配置它的阻塞行為,以實現(xiàn)非阻塞信道。
/**
**nio非阻塞信道配置
**/
channel.configureBlocking(false);
非阻塞信道的優(yōu)勢在于它調(diào)用的方法都會有一個即時返回,用來指示所請求的操作的完成程度。下面用一個例子來演示,客戶端使用NIO非阻塞信道,服務器使用IO實現(xiàn),如下:
/**
**SocketChannel示例
**/
public static void main(String[] arg0) throws InterruptedException{
new Thread(){
public void run() {
server();
}
}.start();
new Thread(){
public void run() {
try {
client();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}.start();
}
public static void client() throws InterruptedException{
SocketChannel client = null;
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
client = SocketChannel.open();
client.configureBlocking(false);
client.connect(new InetSocketAddress("192.168.191.5",8080));
if(client.finishConnect()){
int i = 0;
while(true){
Thread.sleep(3000);
String test = "test "+i+" from client";
i++;
buf.clear();
buf.put(test.getBytes());
buf.flip();
while(buf.hasRemaining()){
System.out.println(buf);
client.write(buf);
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void server(){
ServerSocket server = null;
InputStream in = null;
try {
server = new ServerSocket(8080);
int recvMsgSize = 0;
byte[] recvBuf = new byte[1024];
while(true){
System.out.println("mark in server 1");
Socket clntSocket = server.accept();
System.out.println("mark in server 2");
SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();
System.out.println("Handling client at "+clientAddress);
in = clntSocket.getInputStream();
while((recvMsgSize=in.read(recvBuf))!=-1){
byte[] temp = new byte[recvMsgSize];
System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
System.out.println(new String(temp));
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
輸出:
mark in server 1
mark in server 2
Handling client at /192.168.191.5:57982
java.nio.HeapByteBuffer[pos=0 lim=18 cap=1024]
test 0 from client
java.nio.HeapByteBuffer[pos=0 lim=18 cap=1024]
test 1 from client
...
在上面的例子中,如果將client
方法先啟動就會出現(xiàn)只打印mark in server 1
,后面就不會打印了,造成這樣的情況是因為client.finishConnect()
方法返回false
直接往程序后面跑了,并不會繼續(xù)阻塞直到連上服務端,但是換過個順序讓server
先啟動時,返回true
就能連接成功,因為會在accept()
方法阻塞,直到有客戶端client()
連接。這里就可以看到這個非阻塞的特點。
當然不僅僅只有客戶端client
有這也的非阻塞特性,服務器端也是存在的。
ServerSocketChannel
類似于類SocketChannel
,網(wǎng)絡IO服務器端的非阻塞特性是通過類ServerSocketChannel
來實現(xiàn)的。可以從下面這個例子看出:
/**
**ServerSocketChannel示例
**/
public static void server(){
ServerSocketChannel server = null;
try {
server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(8080));
server.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
byte[] bytes = new byte[512];
System.out.println("--服務器啟動-- ");
while(true){
SocketChannel socket = server.accept();
while(socket!=null&&socket.isConnected()){
buf.clear();
int len = socket.read(buf);
if(len == -1){
socket.close();
System.out.println("連接斷開");
}
buf.flip();
while(buf.hasRemaining()){
buf.get(bytes,0,buf.remaining());
System.out.println(new String(bytes));
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
輸出:
--服務器啟動--
test 0 from client
test 1 from client
...
這個例子簡單的將上一個例子改換了一下(其中的各種異常和資源的操作都沒有完善,實際開發(fā)請勿使用),服務器如果將 while(socket!=null)
換成if(socket!=null)
,那么這個程序只會打印前面一次從客戶端發(fā)送過來的數(shù)據(jù),連接后只讀取了一次。在這里通過int len = socket.read(buf);
中的len
判斷后面是否還會有傳來數(shù)據(jù),如果數(shù)據(jù)長度是-1,則關閉連接。這樣改過后就是非阻塞的網(wǎng)絡IO。
直到這里,都只是使用了前面兩個重要的概念Buffer
和Channel
,現(xiàn)在可以了解選擇器Selector
并配合使用。
Selector & SelectionKey
選擇器這部分,不僅僅只有類Selector
,它還有一個特別重要的類SelectionKey
。在之前的文章中簡單的了解過這兩個類,這里可以回顧一下:要注冊Selector
則需要這個Channel
繼承類SelectableChannel
,這里只有通道類FileChannel
沒有繼承;注冊的Selector
實體可以返回一個SelectionKey
集合,通過這個集合可以對不同的通道做出相應的操作,這樣就避免了傳統(tǒng)的網(wǎng)絡IO為每一個連接創(chuàng)建一個線程而花費大量的資源,只用一個線程就可以解決問題。Selector
管理多個Channel
的結構圖如下所示:
下面可以看一下結合了選擇器類Selector
后構建的簡單服務器代碼:
/**
**Selector 示例,簡單服務器
**/
public static void server(){
System.out.println("--開始啟動服務器");
Selector selector = null;
ServerSocketChannel server = null;
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.socket().bind(new InetSocketAddress(8080));
System.out.println("--監(jiān)聽8080端口");
selector = Selector.open();
server.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("--服務器已啟動成功");
while(true){
int num = selector.select();
if(num == 0){
continue;
}
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while(selectionKeys.hasNext()){
SelectionKey selectionKey = selectionKeys.next();
selectionKeys.remove();
if(selectionKey.isAcceptable()){
System.out.println("-連接請求:");
ServerSocketChannel serverSocket = (ServerSocketChannel)selectionKey.channel();
SocketChannel socket = serverSocket.accept();
socket.configureBlocking(false);
socket.register(selector, SelectionKey.OP_READ);
}else if(selectionKey.isReadable() && selectionKey.isValid()){
System.out.println("-讀取:");
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
byte[] bytes = new byte[1024];
while(channel.isConnected()){
buf.clear();
int len = channel.read(buf);
if(len == -1){
channel.close();
selector.selectNow();
System.out.println("-連接關閉:"+channel.isConnected());
}
if(len > 0){
channel.write(ByteBuffer.wrap("收到消息啦".getBytes()));
System.out.println(buf+"-buf-length -"+len);
buf.flip();
while(buf.hasRemaining()){
buf.get(bytes, 0, len);
System.out.println(new String(bytes));
}
}
}
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
輸出:
--開始啟動服務器
--監(jiān)聽8080端口
--服務器已啟動成功
-連接請求:
-讀取:
java.nio.HeapByteBuffer[pos=18 lim=1024 cap=1024]-buf-length -18
test 0 from client
...
上面的例子中,通過selector
注冊相應的通道,通過selector
獲取的selectionkey
來做對應的動作。在這個例子一直遇到了異常如ClosedChannelException
或者提示遠程關閉了一個連接,導致一直很疑惑,因為我明明調(diào)用了SelectableChannel.close()
方法的。查過資料才知道,關閉一個已經(jīng)注冊的SelectableChannel
需要兩個步驟:
1.取消注冊的
key
,這個可以通過SelectionKey.cancel
方法,也可以通過SelectableChannel.close
方法,或者中斷阻塞在該channel
上的IO操作的線程來做到。
2.后續(xù)的Selector.selectXXX()
方法的調(diào)用才真正地關閉 本地Socket
。
因此,如果調(diào)用了close()
方法后沒有調(diào)用selectXXX()
方法,那么本地socket
將進入CLOSE-WAIT
狀態(tài)。就是這個原因造成在buf.read(bytes)
時發(fā)生CloseChannelException
,因為在上面這個例子中我使用了while(channel.isConnected())
來進行條件循環(huán),如果轉(zhuǎn)換一下思路,不用while
循環(huán),而是把多次傳遞的信息分成多個Channel
來發(fā)送,是不是就會好一點。每一次接收的都是新SocketChannel
實例,而不在一個實例中循環(huán),造成上面那樣的不調(diào)用Selector.selectXXX()
無法真正關閉連接的問題。
這里的Selector
和SelectorKey
還有很多細節(jié)的地方需要再細細研磨,操作。當然現(xiàn)在也可以選擇成熟的NIO框架如Netty使用,以免進入一些不了解的坑中。
DatagramChannel
類似于之前的類SocketChannel
,類DatagramChannel
處理的也是網(wǎng)絡IO,但是它對應的是UDP連接,因為UDP是無連接數(shù)據(jù)包的網(wǎng)絡協(xié)議,所以它并不能像其他通道一樣讀取和寫入數(shù)據(jù),但是提供了receive()
方法和send()
方法來使用。如下所示:
/**
**DatagramChannel示例
**/
服務器端
public static void testDatagramChannel(){
DatagramChannel datagramChannel = null;
Selector selector = null;
try {
selector = Selector.open();
datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.socket().bind(new InetSocketAddress(8080));
datagramChannel.register(selector, SelectionKey.OP_READ);
System.out.println("---服務器啟動");
while(true){
int num = selector.select();
if(num == 0)continue;
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while(selectionKeys.hasNext()){
SelectionKey selectionKey = selectionKeys.next();
selectionKeys.remove();
if(selectionKey.isReadable()){
DatagramChannel channel = (DatagramChannel)selectionKey.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
channel.receive(buf);
buf.flip();
byte[] bytes = new byte[1024];
int len = buf.remaining();
buf.get(bytes,0,len);
String receive = new String(bytes,0,len);
System.out.println(receive);
}
if(selectionKey.isWritable()){
System.out.println("--write");
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
客戶端:
public static void testClient(){
DatagramChannel datagramChannel = null;
try {
System.out.println("--客戶端開始");
datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("這是個Demo".getBytes());
buf.flip();
datagramChannel.send(buf, new InetSocketAddress("192.168.191.3", 8080));
datagramChannel.close();
System.out.println(buf);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
輸出:
--客戶端開始
java.nio.HeapByteBuffer[pos=10 lim=10 cap=1024]
---服務器啟動
這是個Demo
在上面這個demo
中有幾點可以說一下,類DatagramChannel
是個注冊到Selector
中后,這個selector.select()
方法是個阻塞方法,只有等新連接進入時才會繼續(xù)向下執(zhí)行,并不會因為之前對DatagramChannel
設置了非阻塞而使這個方法非阻塞。相對于SocketChannel
類來說,變化并不大。
Pipe
看到這個名字,就已經(jīng)很眼熟了,就像之前的PipedInputStream/PipedOutputStream
和PipedReader/PipedWriter
,這個類也是實現(xiàn)線程間通信的功能。在上篇文章中也有提到,在這個類中是使用兩個靜態(tài)內(nèi)部類SourceChannel
和SinkChannel
來實現(xiàn)功能的,代碼如下所示:
/**
**Pipe 管道示例
**/
public static void testPipe() throws IOException{
final Pipe pipe = Pipe.open();
ExecutorService executor = Executors.newScheduledThreadPool(2);
executor.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Pipe.SinkChannel sinkChannel = pipe.sink();
ByteBuffer buf = ByteBuffer.allocate(1024);
int i = 0;
try {
while(i<4){
Thread.sleep(1000);
buf.clear();
String text = "Pipe test "+ i;
buf.put(text.getBytes());
buf.flip();
sinkChannel.write(buf);
i++;
}
sinkChannel.close();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
executor.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(1024);
while(sourceChannel.isOpen()){
try {
buf.clear();
int len = sourceChannel.read(buf);
if(len == -1)sourceChannel.close();
if(len>0){
byte[] bytes = new byte[1024];
buf.flip();
buf.get(bytes, 0, len);
System.out.println(new String(bytes,0,len));
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
}
輸出:
Pipe test 0
Pipe test 1
Pipe test 2
Pipe test 3
如上所示,管道Pipe
的操作和之前的幾個Channel
的操作并沒有太大的變化,這個類完成線程間的通信靠的是它的兩個靜態(tài)內(nèi)部類,把握住著一點,其余就需要研究書寫細節(jié)了。
總的來說,這部分其實很重要,這里也只是先打一點基礎,如果想學的更深入的話,可以找找相關的框架進行學習,如Netty
。有錯誤疑惑的地方還請麻煩指出一起學習,對于這部分我也是看了相關內(nèi)容并沒有在實際的工作中用到,很多地方可能并不深入或細節(jié)并不完善,還請指出,之后會一一完善。
本文參考
Java NIO 系列教程
攻破JAVA NIO技術壁壘
通俗編程——白話NIO之Selector
NIO的SelectableChannel關閉的一個問題
TCP和UDP的區(qū)別(轉(zhuǎn))