-代表IP位址的類別 -  InetAddress
以往我們在做Java Network程式設計時慣用java.net.InetAddress類別來
代表IP位址。我們只要呼叫InetAddress.getByName()這個方法,參數傳入
對方的IP或hostname就可以得到InetAddress的instance。

InetAddress ipAddr = InetAddress.getByName(“www.nccu.edu.tw”);

-JDK1.4的新類別 - InetSocketAddress

JDK1.4中,在java.net這個Package中新出現了一個類別,叫做InetSocketAddress。
根據JavaDoc的說明,它其實就是「IP位址+Port number」。我們可以看一下
InetSocketAddress的原始碼中在Class scope中宣告了三個屬性

public class InetSocketAddress extends SocketAddress {

    private String hostname = null;

    private InetAddress addr = null;

    private int port;
    .......(下略).......
}

從這裏可以發現,原來InetSocketAddress由真如JavaDoc所言,它只是把
InetAddress包起來,外加port number。InetSocketAddress是extend
SocketAddress類別,其實SocketAddress這個class中什麼也沒有,它所代
表的意思是說,如果要支援什麼協定,只要extend SocketAddress類別來
實作即可。如InetSocketAddress就是專門用在TCP/IP協定來定址的類別。

-InetSocketAddress的建構子有三個

InetSocketAddress(InetAddress addr, int port)
InetSocketAddress(String hostname, int port)
InetSocketAddress(int port)

所以我們在產生InetSocketAddress時,除了直接傳入InetAddress及port之
外,也可以直接傳入hostname。第三個因為沒有指定連結對象,所以是在寫
erver時才能用。

-利用SocketChannel連結伺服器
一般Network Programming會有二種方式,一種是我們寫作client side程式
去和已存在的server溝通。另一種就是我們寫server,再bind到某個port等
待別人來連(listen)。這裏先探討比較簡單的client side情況。

JDK1.4之前我們都是先從IP/Hostname得到一個InetAddress如

InetAddress ipAddr = InetAddress.getByName(“www.nccu.edu.tw”);

得到ip後,再把它和當做Socket類別建構子的參數傳入,這個時候才指定port
number(在這個例子中是8080)。

Socket socket = new Socket(ipAddr,8080);


這樣就可以和Server連上,連上後要取得資料的話,呼叫

socket.getInputStream();

這個method即可取得InputStream。
要用New I/O的SocketChannel來做的話,必須經過以下步驟
  
一、建立一個InetSocketAddress的instance。

InetSocketAddress socketAddr = new InetSocketAddress( www.nccu.edu.tw ,80);

二、利用SocketChannel.open();這個static method,建立一個SocketChannel的instance。

SocketChannel socketChannel = SocketChannel.open();

三、建立一個ByteBuffer的instance。

ByteBuffer buf = ByteBuffer.allocateDirect(1024);

四、利用SocketChannel的connect()方法來建立連結。

socketChannel.connect(socketAddr);

五、利用SocketChannel的read()方法把資料讀入ByteBuffer中。

socketChannel.read(buf);

再來就用(上)篇所提到的Buffer家族操作方式去操作即可。

Selector及SelectorProvider
在JDK1.4之後,總算可以達到Non-blocking的功能了。在前面我們其實是
利用SocketChannel類別以blocking方式來實作。SocketChannel類別還支
援Non-blocking的寫法,為什麼呢,因為這個類別除了implement ByteChannel
(可讀可寫)、ScatteringByteChannel(可讀區塊)、GatheringByteChannel
(可寫區塊)這三個類別之外,它本身是extend自AbstractSelectableChannel
這個類別。AbstractSelectableChannel是implement SelectableChannel。
SelectableChannel的子類別,都有一個method叫做register(),可以向
Selector註冊。
  
Selector在non-blocking中扮演關鍵性的角色。因為non-blocking的方式
之下,我們可以不用一直block在那邊等待網路另一端的回應,可以先做別
的事。當然網路另一端有回應時,必須有人通知我們。我們可以向Selector
註冊Channel及我們有興趣的事件。當這些事件發生時,Selector就會通知
我們。它傳回一組SelectionKey,這些key有一個method叫channel(),我們
從這個method就可以再度取我們剛註冊的channel,並加以處理了。

Selector這個類別是Abstract,這暗示我們,其實可能還有很多種Selector,
可以利用extend Selector類別的方式去擴充它的功能,我們在下面會印証
這個推論。

    取得Selector instance的方式是
  
Selector selector = Selector.open();

  利用open()這個靜態方法可以取得Selector的instance。
  觀察open()方法的內部,它其實是
  
  return SelectorProvider.provider().openSelector();
  

看到這裏,我們發現,其實為了可擴充性,它另外還制定了SelectorProvider
這個類別,來提供不同種類的Selector,果然印証之前的推論。

如果看SelectorProvider的原始碼,就會發現真象。它的過程如下:
Selector的open()呼叫SelectorProvider,要求它提供一個Selector的instance。
這時候SelectorProvider會檢查系統參數是否有

java.nio.channels.spi.SelectorProvider=???

這一個參數被定義,如果有,就laod那個SelectorProvider的類別來提供Selector。
如果沒有,就load一個名叫「DefaultSelectorProvider」的SelectorProvider。
其實DefaultSelectorProvider在原始碼中是設成「PollSelectorProvider」,這
代表很重要的意義,因為我們發現原來預設的SelectorProvider就是PollSelectorProvider,
而且從它的名字也可以推論出它的行為是「Poll(輪詢)」,而它提供的正是
「PollSelector」。更明確地說,PollSelector是JDK1.4目前唯一提供的Selector。

由上可知Seletor本身是一個Abstract類別,我們真正用到的類別其實是
PollSelectorImpl這個子類別。它們的關係如下。
  
public int select(long timeout)探討

Selector要怎麼通知我們說某某event已經發生呢?Selector有一個method叫做
select(),這個method的傳回值是int。如果有我們註冊的事情發生了,它的
傳回值就會大於0。說得明確一點,它傳回的是事件的數目。所以通常我們會
寫一個while迴圈來偵測。

while(selector.select(500)>0)
{
.......(do something)...
}

各位一定注意到了select()這個method有一個參數,我們傳了500進去,這個參
數是long,代表timeout。要確實了解這個timeout的意思,必須深入探討select()
這一個method,不要小看這個timeout,它可是被原封不動一直傳到最底層的Win32
API去哦,而且這個select()method是non-blocking I/O的關鍵所在。

不能免俗,我們還是要追查JDK的原始碼,由於PollSelector是目前唯一有實作的

Selector,所以以下都是以PollSelector的做法為主。我們發現,Select()這個
method在做完前置處理後,會call一個叫做doSelect(long  timeout)的method,
順便直接把timeout傳下去。再深入追查下去,會發現doSelect(long  timeout)
其實是call PollWrapper類別中一個名叫poll()的method。這個poll()類別對映
到Native Call,它呼叫的是WSAWaitForMultipleEvent()這個Win32 API(為了簡
化起見,這裏只討論Windows平台上的請況)。
  
查詢MSDN,發現WSAWaitForMultipleEvent()這個Win32 API有五個參數,第四個
就是timeout。因為timeout這個參數是一路傳下來,所以看到這裏,大家應該了
解它其實和這個Windows API的timeout的意義一樣,timeout之後,它就會直接
return WSA_WAIT_TIMEOUT這個傳回值(DWORD)。在java的觀點來看,在timeout時
間過了之後,它會直接進行下一行,並傳回0。
  
但是有個例外,JavaDoc上說,timeout的參數不能是負數,如果我們傳負數會怎
麼樣?會throw一個IllegalArgumentException。如果我們傳0的話,它會自動幫我
們改成-1,再傳到doSelect()中。在PollArrayWrapper.c中有一行是這樣寫的
  
  if (timeout < 0) {
          timeout = WSA_INFINITE;
  }
  
嗯,相信大家應該已經看出來,我們傳0的會發生什麼事吧?


利用SocketChannel連結伺服器(Non-Blocking)

討論完selector之後,我們要開始寫Non-Blocking的網路應用程式了,下面有幾
個步驟要注意一下。

前半段的寫法大致都一樣,利用SocketChannel連結到你的InetSocketAddress上。
但是要利用configureBlocking()告訴系統說我們要用non-blocking的方式。

InetSocketAddress socketAddr = new InetSocketAddress( www.nccu.edu.tw ,80);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(socketAddr);

再來要得到一個Selector的instance,然後向這個selector註冊你的channel。

Selector selector = selector.open();
socketChannel.register(selector,SelectionKey.OP_CONNECT | SelectionKey.OP_READ);

  發生事件之後,selector會把事件以SelectionKey的方式傳回來,
因為可能會有好幾個,所以它是放到一個Set中。因此我們會有二層while迴圈,
外層是等待event發生,內層是發生之後利用iterator來一一檢查SelectionKey,
針對這些event,我們再一一取出channel來做處理。由於我們註冊了OP_CONNECT
事件及OP_READ事件,所以利用if判別出來之後,就可以利用 key.channel得到
的SocketChannel去處理了。其它部份就比照前面所提Blocking方式處理即可。

while (selector.select(500) > 0)
{
  //取得SelectionKeys
  Set readyKeys = selector.selectedKeys();
  Iterator readyItor = readyKeys.iterator();
 //利用iterator來一一檢查SelectionKey
  while (readyItor.hasNext())
  {
     SelectionKey key = (SelectionKey)readyItor.next();
     readyItor.remove();//務必記得這一行
   //得到SocketChannel了!!
     SocketChannel keyChannel = (SocketChannel)key.channel();
    if (key.isConnectable()) {
       //在這裏處理OP_CONNECT事件
    } else if (key.isReadable()) {
       //在這裏處理OP_READ事件
    }
  }
}

結論

在這一篇文章中我們詳盡地介紹了Seletor,也實作了一個Blocking及non-blocking
的Client Networking程式。為了簡化討論範圍,Server的寫法請各位自行參考相關
資料,但基本的觀念都是雷同的。Java.nio Package也許是JDK1.4中最引人注目的
新功能,藉由native methd的實作,再加上non-blocking方式的支援,在效能上應有
不錯的表現。比較令人擔心的是,native call的大量引用,代表的是要JDK支援特定
的平台時,很可能要針該平台改寫的Code就變多了。目前JDK有win32、Solaris、linux
三種版本。如果別的平台(像FreeBSD)要支援的話,要port的人可就累了~~^.^。