ServerSocketChannel
讓我們從最簡單的ServerSocketChannel來開始對socket通道類的討論
ServerSocketChannel是一個基於通道的socket監聽器。它同我們所熟悉的java.net.ServerSocket執行相同的基本任務,不過它增加了通道語義,因此能夠在非阻塞模式下運行。
用靜態的open( )工廠方法創建一個新的ServerSocketChannel對象,將會返回同一個未綁定的java.net.ServerSocket關聯的通道。該對等ServerSocket可以通過在返回的ServerSocketChannel上調用socket( )方法來獲取。作為ServerSocketChannel的對等體被創建的ServerSocket對象依賴通道實現。這些socket關聯的SocketImpl能識別通道。通道不能被封裝在隨意的socket對象外面。
由於ServerSocketChannel沒有bind( )方法,因此有必要取出對等的socket並使用它來綁定到一個端口以開始監聽連接。我們也是使用對等ServerSocket的API來根據需要設置其他的socket選項
- ServerSocketChannel ssc = ServerSocketChannel.open();
- ServerSocket serverSocket = ssc.socket(); // Listen on port 1234
- serverSocket.bind(new InetSocketAddress(1234));
同它的對等體java.net.ServerSocket一樣,ServerSocketChannel也有accept( )方法。
一旦您創建了一個ServerSocketChannel並用對等socket綁定了它,然后您就可以在其中一個上調用accept( )。
如果您選擇在ServerSocket上調用accept( )方法,那么它會同任何其他的ServerSocket表現一樣的行為:總是阻塞並返回一個java.net.Socket對象(阻塞的!!!!)
。如果您選擇在ServerSocketChannel上調用accept( )方法則會返回SocketChannel類型的對象,返回的對象能夠在非阻塞模式下運行。假設系統已經有一個安全管理器(security manager),兩種形式的方法調用都執行相同的安全檢查。
如果以非阻塞模式被調用,當沒有傳入連接在等待時,ServerSocketChannel.accept( )會立即返回null (因為他是非阻塞的所以要有返回,)。
正是這種檢查連接而不阻塞的能力實現了可伸縮性並降低了復雜性。可選擇性也因此得到實現。我們可以使用一個選擇器實例來注冊一個ServerSocketChannel對象以實現新連接到達時自動通知的功能。
演示如何使用一個非阻塞的accept( )方法:
- package com.anders.selector2;
- import java.net.InetSocketAddress;
- import java.net.ServerSocket;
- import java.nio.ByteBuffer;
- import java.nio.channels.ServerSocketChannel;
- import java.nio.channels.SocketChannel;
- public class ServerSocketChannelApp {
- private static final String MSG = "hello, I must be going \n";
- public static void main(String[] args) throws Exception {
- int port = 8989;
- ServerSocketChannel ssc = ServerSocketChannel.open();
- ServerSocket ss = ssc.socket();
- ss.bind(new InetSocketAddress(port));
- // set no blocking
- ssc.configureBlocking(false);
- ByteBuffer buffer = ByteBuffer.wrap(MSG.getBytes());
- while (true) {
- System.out.println("wait for connection ……");
- SocketChannel sc = ssc.accept();
- if (sc == null) {
- // no connections, snooze a while ...
- Thread.sleep(1000);
- } else {
- System.out.println("Incoming connection from " + sc.socket().getRemoteSocketAddress());
- buffer.rewind();
- //write msg to client
- sc.write(buffer);
- sc.close();
- }
- }
- }
- }
ps 阻塞和非阻塞
傳統的serversocket阻塞模式:
- public class ServerSocketApp {
- public static void main(String[] args) throws Exception {
- ServerSocket ss = new ServerSocket(8989);
- ss.accept();
- System.out.println(1);
- }
- }
運行這個程序 為什么沒有輸出1 ???
因為ServerSocket 是阻塞模式的 ,什么是阻塞,就是在沒有任何連接之前,accept方法一直在那里阻塞着,直到有connection來繼續往下執行,所以在運行程序的時候,並沒輸出1,若要輸出 telnet一下就可以了
nio中的 非阻塞:
- public static void main(String[] args) throws Exception {
- ServerSocketChannel ssc = ServerSocketChannel.open();
- ServerSocket ss = ssc.socket();
- ss.bind(new InetSocketAddress(8989));
- // set no blocking
- ssc.configureBlocking(false);
- ssc.accept();
- System.out.println(1);
- }
運行這個程序 有1 輸出!!
這就是因為 它是非阻塞模式的。
SocketChannel
- public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel {
- // This is a partial API listing
- public static SocketChannel open() throws IOException;
- public static SocketChannel open(InetSocketAddress remote) throws IOException;
- public abstract Socket socket();
- public abstract boolean connect(SocketAddress remote) throws IOException;
- public abstract boolean isConnectionPending();
- public abstract boolean finishConnect() throws IOException;
- public abstract boolean isConnected();
- public final int validOps();
- }
Socket和SocketChannel類封裝點對點、有序的網絡連接,類似於我們所熟知並喜愛的TCP/IP網絡連接。SocketChannel扮演客戶端發起同一個監聽服務器的連接。直到連接成功,它才能收到數據並且只會從連接到的地址接收
每個SocketChannel對象創建時都是同一個對等的java.net.Socket對象串聯的。靜態的open( )方法可以創建一個新的SocketChannel對象,而在新創建的SocketChannel上調用socket( )方法能返回它對等的Socket對象;在該Socket上調用getChannel( )方法則能返回最初的那個SocketChannel。
新創建的SocketChannel雖已打開卻是未連接的。在一個未連接的SocketChannel對象上嘗試一個I/O操作會導致NotYetConnectedException異常。
我們可以通過在通道上直接調用connect( )方法或在通道關聯的Socket對象上調用connect( )來將該socket通道連接。
一旦一個socket通道被連接,它將保持連接狀態直到被關閉。
您可以通過調用布爾型的isConnected( )方法來測試某個SocketChannel當前是否已連接。
- 第二種帶InetSocketAddress參數形式的open( )是在返回之前進行連接的便捷方法。這段代碼:
- SocketChannel socketChannel = SocketChannel.open (new InetSocketAddress ("somehost", somePort));
- 等價於下面這段代碼:
- SocketChannel socketChannel = SocketChannel.open( );
- socketChannel.connect (new InetSocketAddress ("somehost", somePort));
1 如果您選擇使用傳統方式進行連接——通過在對等Socket對象上調用connect( )方法,那么傳統的連接語義將適用於此。線程在連接建立好或超時過期之前都將保持阻塞。
2 如果您選擇通過在通道上直接調用connect( )方法來建立連接並且通道處於阻塞模式(默認模式),那么連接過程實際上是一樣的。在SocketChannel上並沒有一種connect( )方法可以讓您指定超時(timeout)值,當connect( )方法在非阻塞模式下被調用時SocketChannel提供並發連接:它發起對請求地址的連接並且立即返回值。
如果返回值是true,說明連接立即建立了(這可能是本地環回連接);
如果連接不能立即建立,connect( )方法會返回false且並發地繼續連接建立過程。
面向流的的socket建立連接狀態需要一定的時間,因為兩個待連接系統之間必須進行包對話以建立維護流socket所需的狀態信息。
跨越開放互聯網連接到遠程系統會特別耗時。假如某個SocketChannel上當前正由一個並發連接,isConnectPending( )方法就會返回true值。調用finishConnect( )方法來完成連接過程,該方法任何時候都可以安全地進行調用。假如在一個非阻塞模式的SocketChannel對象上調用finishConnect( )方法,將可能出現下列情形之一:
connect( )方法尚未被調用。那么將產生NoConnectionPendingException異常。
連接建立過程正在進行,尚未完成。那么什么都不會發生,finishConnect( )方法會立即返回false值。
在非阻塞模式下調用connect( )方法之后,SocketChannel又被切換回了阻塞模式。那么如果有必要的話,調用線程會阻塞直到連接建立完成,finishConnect( )方法接着就會返回true值。
在初次調用connect( )或最后一次調用finishConnect( )之后,連接建立過程已經完成。那么SocketChannel對象的內部狀態將被更新到已連接狀態,finishConnect( )方法會返回true值,然后SocketChannel對象就可以被用來傳輸數據了。
連接已經建立。那么什么都不會發生,finishConnect( )方法會返回true值。
當通道處於中間的連接等待(connection-pending)狀態時,您只可以調用finishConnect( )、isConnectPending( )或isConnected( )方法。
一旦連接建立過程成功完成,isConnected( )將返回true值。
- InetSocketAddress addr = new InetSocketAddress (host, port);
- SocketChannel sc = SocketChannel.open( );
- sc.configureBlocking (false);
- sc.connect (addr);
- while ( ! sc.finishConnect( )) {
- doSomethingElse( );
- }
- doSomethingWithChannel (sc);
- sc.close( );
一段用來管理異步連接的可用代碼。
- public class SocketChannelApp {
- public static void main(String[] args) throws Exception {
- InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 8989);
- SocketChannel sc = SocketChannel.open();
- sc.connect(addr);
- sc.configureBlocking(false);
- while (!sc.finishConnect()) {
- doSomethings();
- }
- //Do something with the connected socket
- ByteBuffer buffer = ByteBuffer.wrap(new String("Hello server!").getBytes());
- sc.write(buffer);
- sc.close();
- }
- private static void doSomethings() {
- System.out.println("do something useless!");
- }
- }
server還是采用上篇的 ,我把它簡單的改了改
- public class ServerSocketChannelApp {
- private static final String MSG = "hello, I must be going \n";
- public static void main(String[] args) throws Exception {
- int port = 8989;
- ServerSocketChannel ssc = ServerSocketChannel.open();
- ServerSocket ss = ssc.socket();
- ss.bind(new InetSocketAddress(port));
- // set no blocking
- ssc.configureBlocking(false);
- ByteBuffer buffer = ByteBuffer.wrap(MSG.getBytes());
- while (true) {
- // System.out.println("wait for connection ……");
- SocketChannel sc = ssc.accept();
- if (sc == null) {
- // no connections, snooze a while ...
- Thread.sleep(1000);
- } else {
- System.out.println("Incoming connection from " + sc.socket().getRemoteSocketAddress());
- ByteBuffer readerBuffer = ByteBuffer.allocate(1024);
- sc.read(readerBuffer);
- readerBuffer.flip();
- //output get
- out(readerBuffer);
- buffer.rewind();
- sc.write(buffer);
- sc.close();
- }
- }
- }
- private static void out(ByteBuffer readerBuffer) {
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < readerBuffer.limit(); i++) {
- char c = (char) readerBuffer.get();
- sb.append(new String(new char[]{c}));
- }
- System.out.println(sb.toString());
- }
- }
ps:
如果嘗試異步連接失敗,那么下次調用finishConnect( )方法會產生一個適當的經檢查的異常以指出問題的性質。通道然后就會被關閉並將不能被連接或再次使用。與連接相關的方法使得我們可以對一個通道進行輪詢並在連接進行過程中判斷通道所處的狀態。第四章中,我們將了解到如何使用選擇器來避免進行輪詢並在異步連接建立之后收到通知。Socket通道是線程安全的。並發訪問時無需特別措施來保護發起訪問的多個線程,不過任何時候都只有一個讀操作和一個寫操作在進行中。請記住,sockets是面向流的而非包導向的。它們可以保證發送的字節會按照順序到達但無法承諾維持字節分組。某個發送器可能給一個socket寫入了20個字節而接收器調用read( )方法時卻只收到了其中的3個字節。剩下的17個字節還是傳輸中。由於這個原因,讓多個不配合的線程共享某個流socket的同一側絕非一個好的設計選擇。
connect( )和finishConnect( )方法是互相同步的,並且只要其中一個操作正在進行,任何讀或寫的方法調用都會阻塞,即使是在非阻塞模式下。如果此情形下您有疑問或不能承受一個讀或寫操作在某個通道上阻塞,請用isConnected( )方法測試一下連接狀態。