Channel就是一個通道,用於傳輸數據,兩端分別是緩沖區和實體(文件或者套接字),通道的特點(也是NIO的特點):通道中的數據總是要先讀到一個緩沖區,或者總是要從一個緩沖區中讀入。
Channel的分類
1) FileChannel:從文件中讀寫數據
2) SocketChannel:通過TCP協議讀寫網絡中的數據
3) ServerSocketChannel:在服務器端可以監聽新進來的TCP連接,像WEB服務器那樣,對每一個新進來的請求創建一個SocketChannel
4) DatagramChannel:通過UDP協議讀寫網絡中的數據
上面眾多的分類,是對應了不同的實體,這些通道包括了文件IO、TCP和UDP網絡IO。
下面來看看Channel的源碼:
1 public interface Channel extends Closeable { 2 8 public boolean isOpen(); 9 27 public void close() throws IOException; 28 29 }
從這里我們可以看到,Channel接口只提供了關閉通道和檢測通道是否打開這兩個方法,剩下方法的都是由子接口和實現類來定義提供。
我們選擇其中幾個來看看這些接口的源碼:
1 public interface WritableByteChannel 2 extends Channel 3 { 4 5 public int write(ByteBuffer src) throws IOException; 6 7 }
public interface ReadableByteChannel extends Channel
{
public int read(ByteBuffer dst) throws IOException; }
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel { }
前面我提到過:通道可以只讀、只寫或者同時讀寫,因為Channel類可以只實現只讀接口ReadableByteChannel或者只實現只寫接口WritableByteChannel,而我們常用的Channel類FileChannel、SocketChannel、DatagramChannel是雙向通信的, 因為實現了ByteChannel接口。
Channel的獲取
IO在廣義上可以分為:文件IO和網絡IO。文件IO對應的通道為FileChannel,而網絡IO對應的通道則有三個:SocketChannel、ServerSoketChannel和DatagramChannel。
一、文件通道
FileChannel對象不能直接創建,只能通過FileInputStream、OutputStream、RandomAccessFile對象的getChannel()來獲取,如:
FileInputStream fis = new FileInputStream("c:/in.txt"); FileChannel fic = fis.getChannel();
FileChannel無法設置為非阻塞模式,它總是運行在阻塞模式下。
1)使用通道讀取文件
1 public class NIOFileReadTest 2 { 3 public static void main(String[] args) throws IOException 4 { 5 RandomAccessFile raf = new RandomAccessFile("D:/in.txt","rw"); 6 FileChannel fis = raf.getChannel(); 7 ByteBuffer buffer = ByteBuffer.allocate(1024); 8 fis.read(buffer); 9 buffer.flip(); 10 while(buffer.hasRemaining()) 11 { 12 System.out.print((char)buffer.get()); 13 } 14 buffer.clear(); 15 fis.close(); 16 } 17 }
執行結果:
FileChannel
ByteBuffer
SelectorPicked
2)使用通道寫入文件
public class NIOFileWriteTest { public static void main(String[] args) throws Exception { FileOutputStream fos = new FileOutputStream("d:/out.txt"); FileChannel fc = fos.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(10); buffer.clear(); String str = "Channel"; buffer.put(str.getBytes()); buffer.flip(); while(buffer.hasRemaining()) { fc.write(buffer); } fc.close(); fos.close(); } }
在這里總是要記住channel是要關閉的。
ByteBuffer中的方法我在下一章再詳細介紹,這里只要注意這點即可:通道只能使用ByteBuffer,不管是讀還是寫,通道都要對接緩沖區。
3)通道的常用方法
position();返回通道的文件位置
position(long newPosition):設置通道的文件位置
將上面讀文件的程序修改下,來觀察這幾個方法:
1 public class NIOFileReadTest 2 { 3 public static void main(String[] args) throws IOException 4 { 5 RandomAccessFile raf = new RandomAccessFile("D:/in.txt","rw"); 6 FileChannel fis = raf.getChannel(); 7 System.out.println("此通道文件的總長度:" +fis.size()); 8 //當前通道的文件位置 9 long position = fis.position(); 10 System.out.println("通道當前的位置:" + position); 11 //設置新的通道文件位置,從這個位置開始讀取 12 fis.position(position + 8); 13 ByteBuffer buffer = ByteBuffer.allocate(50); 14 fis.read(buffer); 15 buffer.flip(); 16 while(buffer.hasRemaining()) 17 { 18 System.out.print((char)buffer.get()); 19 } 20 buffer.clear(); 21 fis.close(); 22 } 23 }
執行結果:
此通道文件的總長度:33 通道當前的位置:0 nel ByteBuffer Selector
FileChannel是線程安全的,可以多個線程在同一個實例上並發操作,但是其中有些方法(改變文件通道位置或者文件大小的方法)必須是單線程操作。
二、網絡通道
SocketChannel是一個連接到TCP套接字的通道,獲取的方式有兩種:
1、打開一個SocketChannel並連接到互聯網上某台服務器。
2、一個新連接到達ServerSocketChannel時,會創建一個SocketChannel。
上面這兩種模式跟IO的Socket、ServerSocket類似,下面分別來看看客戶端和服務器端:
一、SocketChannel
從通道中讀取數據
1 public class SocketChannelTest 2 { 3 public static void main(String[] args) throws Exception 4 { 5 //獲取socket通道 6 SocketChannel sc = SocketChannel.open(); 7 //設置為非阻塞模式 8 sc.configureBlocking(false); 9 //建立連接,非阻塞模式下,該方法可能在連接建立之前就返回了 10 sc.connect(new InetSocketAddress("wap.cmread.com",80)); 11 //判斷連接是否建立 12 while(!sc.finishConnect()) 13 { 14 System.out.println("連接未建立"); 15 Thread.sleep(5); 16 } 17 ByteBuffer buffer = ByteBuffer.allocate(48); 18 int byteRead = sc.read(buffer); 19 System.out.println(byteRead); 20 sc.close(); 21 buffer.clear(); 22 } 23 }
執行結果;
連接未建立
連接未建立
0
1、第6、7行是獲取一個socket通道,並且設置為非阻塞模式。
2、由於是非阻塞模式,通道在調用方法connect/read/writer這三個方法時,會出現這些情況:連接未建立,connect方法就返回了;尚未讀取任何數據時,read方法就返回;尚未寫出任何內容時,writer就返回。
3、在12行的循環代碼中,是判斷連接是否建立,從執行結果來看,循環執行了兩次連接才建立(在循環里線程還有休眠)。
4、由於只是建立了連接,通道里面其實沒有任何的數據。
5、第18行調用read方法,由於是非阻塞模式,所以在並未讀取任何數據的情況下就返回0(盡管通道里面沒有數據)。
將數據寫入通道
1 public class SocketChannelTest 2 { 3 public static void main(String[] args) throws Exception 4 { 5 SocketChannel sc = SocketChannel.open(); 6 String str = "non-blocking socket channel"; 7 ByteBuffer buffer = ByteBuffer.allocate(100); 8 buffer.put(str.getBytes()); 9 buffer.flip(); 10 while(buffer.hasRemaining()) 11 { 12 sc.write(buffer); 13 } 14 sc.close(); 15 buffer.clear(); 16 } 17 }
1、SocketChannel.write()方法的調用是在一個while循環中的。Write()方法無法保證能寫多少字節到SocketChannel。所以,我們重復調用write()直到Buffer沒有要寫的字節為止。
二、ServerSocketChannel
ServerSocketChannel是一個可以監聽新進來的TCP連接的通道。
1 public class ServerSocketChannelTest 2 { 3 public static void main(String[] args) throws Exception 4 { 5 ServerSocketChannel ssc = ServerSocketChannel.open(); 6 ssc.socket().bind(new InetSocketAddress(80)); 7 ssc.configureBlocking(false); 8 while(true) 9 { 10 SocketChannel sc = ssc.accept(); 11 if(null != sc) 12 { 13 //do something; 14 } 15 } 16 } 17 }
1、第5、6、7行,獲取一個ServerSocketChannel,並且監聽80端口,設置為非阻塞模式。
2、通過accept方法監聽新接入進來的連接,這個方法會返回一個包含新進來的連接的SocketChannel(服務器端的通道的獲取方式)。如果是阻塞模式,該方法會一直阻塞直到有新的連接進來。如果是非阻塞模式,則accept方法會立刻返回,返回值是null。
3、第11行,是因為在非阻塞模式下,需要檢查SocketChannel是否為null。
三、socket通道與socket
1 ServerSocketChannel ssc = ServerSocketChannel.open(); 2 ServerSocket socket = ssc.socket(); 3 ServerSocketChannel ssc1 = socket.getChannel();
1、從這代碼片段可以大概看到這樣一種關系:所有socket通道(SocketChannel/ServerSocketChanne/DatagramSocketChannel)在被實例化之后,都是伴隨生成對應的socket對象,就是前面IO章節介紹的java.net類(Socket/ServerSocket/DatagramSocket)。通過通道類的socket方法來獲取。
2、java.net類(Socket/ServerSocket/DatagramSocket)現在可以通過getChannel方法來獲取對應的通道。前提是這些socket對象不是使用傳統方式(直接實例化)創建的。否則它就沒有關聯的socket通道,調用getChannel方法返回總是null。