楊健 ()
中南工業大學
年
月
摘要
代碼號為
Merlin
的J
SE
帶來了一些激動人心的新特性
諸如對正則表達式的支持
異步輸入輸出流
通道(Channel)
字符集等
雖然該版本還處在測試階段
但這些新特性早已讓開發人員們躍躍欲試
在Merlin發布之前
異步輸入輸出流的應用還只是C
C++程序員的特殊武器;在Merlin中引入異步輸入輸出機制之後
Java程序員也可以利用它完成很多簡潔卻是高質量的代碼了
本文將介紹怎樣使用異步輸入輸出流來編寫Socket進程通信程序
同步?異步輸入輸出機制的引入
在Merlin之前
編寫Socket程序是比較繁瑣的工作
因為輸入輸出都必須同步
這樣
對於多客戶端客戶/服務器模式
不得不使用多線程
即為每個連接的客戶都分配一個線程來處理輸入輸出
由此而帶來的問題是可想而知的
程序員不得不為了避免死鎖
線程安全等問題
進行大量的編碼和測試
很多人都在抱怨為什麼不在Java中引入異步輸入輸出機制
比較官方的解釋是
任何一種應用程序接口的引入
都必須兼容任何操作平台
因為Java是跨平台的
而當時支持異步輸入輸出機制的操作平台顯然不可能是全部
自Java
Platform以後
分離出J
SE
J
ME
J
EE三種不同類型的應用程序接口
以適應不同的應用開發
Java標准的制訂者們意識到了這個問題
並且支持異步輸入輸出機制的操作平台在當今操作平台中處於主流地位
於是
Jdk(J
SE) 的第五次發布中引入了異步輸入輸出機制
以前的Socket進程通信程序設計中
一般客戶端和服務器端程序設計如下
服務器端
//服務器端監聽線程
while (true) {
Socket clientSocket;
clientSocket = socket
accept(); //取得客戶請求Socket
如果沒有//客戶請求連接
線程在此處阻塞
//用取得的Socket構造輸入輸出流
PrintStream os = new PrintStream(new
BufferedOutputStream(clientSocket
getOutputStream()
)
false);
BufferedReader is = new BufferedReader(new
InputStreamReader(clientSocket
getInputStream()));
//創建客戶會話線程
進行輸入輸出控制
為同步機制
new ClientSession();
}
客戶端
clientSocket = new Socket(HOSTNAME
LISTENPORT);//連接服務器套接字
//用取得的Socket構造輸入輸出流
PrintStream os = new PrintStream(new
BufferedOutputStream(clientSocket
getOutputStream()
)
false);
BufferedReader is = new BufferedReader(new
InputStreamReader(clientSocket
getInputStream()));
//進行輸入輸出控制
以上代碼段只是用同步機制編寫Socket進程通信的一個框架
實際上要考慮的問題要復雜的多(有興趣的讀者可以參考我的一篇文章《Internet 實時通信系統設計與實現》)
將這樣一個框架列出來
只是為了與用異步機制實現的Socket進程通信進行比較
下面將介紹使用異步機制的程序設計
用異步輸入輸出流編寫Socket進程通信程序
在Merlin中加入了用於實現異步輸入輸出機制的應用程序接口包
java
nio(新的輸入輸出包
定義了很多基本類型緩沖(Buffer))
java
nio
channels(通道及選擇器等
用於異步輸入輸出)
java
nio
charset(字符的編碼解碼)
通道(Channel)首先在選擇器(Selector)中注冊自己感興趣的事件
當相應的事件發生時
選擇器便通過選擇鍵(SelectionKey)通知已注冊的通道
然後通道將需要處理的信息
通過緩沖(Buffer)打包
編碼/解碼
完成輸入輸出控制
通道介紹
這裡主要介紹ServerSocketChannel和 SocketChannel
它們都是可選擇的(selectable)通道
分別可以工作在同步和異步兩種方式下(注意
這裡的可選擇不是指可以選擇兩種工作方式
而是指可以有選擇的注冊自己感興趣的事件)
可以用nfigureBlocking(Boolean )來設置其工作方式
與以前版本的API相比較
ServerSocketChannel就相當於ServerSocket(ServerSocketChannel封裝了ServerSocket)
而SocketChannel就相當於Socket(SocketChannel封裝了Socket)
當通道工作在同步方式時
編程方法與以前的基本相似
這裡主要介紹異步工作方式
所謂異步輸入輸出機制
是指在進行輸入輸出處理時
不必等到輸入輸出處理完畢才返回
所以異步的同義語是非阻塞(None Blocking)
在服務器端
ServerSocketChannel通過靜態函數open()返回一個實例serverChl
然後該通道調用serverChl
socket()
bind()綁定到服務器某端口
並調用register(Selector sel
SelectionKey
OP_ACCEPT)注冊OP_ACCEPT事件到一個選擇器中(ServerSocketChannel只可以注冊OP_ACCEPT事件)
當有客戶請求連接時
選擇器就會通知該通道有客戶連接請求
就可以進行相應的輸入輸出控制了
在客戶端
clientChl實例注冊自己感興趣的事件後(可以是OP_CONNECT
OP_READ
OP_WRITE的組合)
調用nnect(InetSocketAddress )連接服務器然後進行相應處理
注意
這裡的連接是異步的
即會立即返回而繼續執行後面的代碼
選擇器和選擇鍵介紹
選擇器(Selector)的作用是
將通道感興趣的事件放入隊列中
而不是馬上提交給應用程序
等已注冊的通道自己來請求處理這些事件
換句話說
就是選擇器將會隨時報告已經准備好了的通道
而且是按照先進先出的順序
那麼
選擇器是通過什麼來報告的呢?選擇鍵(SelectionKey)
選擇鍵的作用就是表明哪個通道已經做好了准備
准備干什麼
你也許馬上會想到
那一定是已注冊的通道感興趣的事件
不錯
例如對於服務器端serverChl來說
可以調用key
isAcceptable()來通知serverChl有客戶端連接請求
相應的函數還有
SelectionKey
isReadable()
SelectionKey
isWritable()
一般的
在一個循環中輪詢感興趣的事件(具體可參照下面的代碼)
如果選擇器中尚無通道已注冊事件發生
調用Selector
select()將阻塞
直到有事件發生為止
另外
可以調用selectNow()或者select(long timeout)
前者立即返回
沒有事件時返回
值
後者等待timeout時間後返回
一個選擇器最多可以同時被
個通道一起注冊使用
應用實例
下面是用異步輸入輸出機制實現的客戶/服務器實例程序――程序清單
(限於篇幅
只給出了服務器端實現
讀者可以參照著實現客戶端代碼)
程序類圖
public class NBlockingServer {
int port =
;
int BUFFERSIZE =
;
Selector selector = null;
ServerSocketChannel serverChannel = null;
HashMap clientChannelMap = null;//用來存放每一個客戶連接對應的套接字和通道
public NBlockingServer( int port ) {
this
clientChannelMap = new HashMap();
this
port = port;
}
public void initialize() throws IOException {
//初始化
分別實例化一個選擇器
一個服務器端可選擇通道
this
selector = Selector
open();
this
serverChannel = ServerSocketChannel
open();
thnfigureBlocking(false);
InetAddress localhost = InetAddress
getLocalHost();
InetSocketAddress isa = new InetSocketAddress(localhost
this
port );
this
serverChannel
socket()
bind(isa);//將該套接字綁定到服務器某一可用端口
}
//結束時釋放資源
public void finalize() throws IOException {
this
serverChannel
close();
this
selector
close();
}
//將讀入字節緩沖的信息解碼
public String decode( ByteBuffer byteBuffer ) throws
CharacterCodingException {
Charset charset = Charset
forName(
ISO
);
CharsetDecoder decoder = charset
newDecoder();
CharBuffer charBuffer = decoder
decode( byteBuffer );
String result = charBuffer
toString();
return result;
}
//監聽端口
當通道准備好時進行相應操作
public void portListening() throws IOException
InterruptedException {
//服務器端通道注冊OP_ACCEPT事件
SelectionKey acceptKey =this
serverChannel
register( this
selector
SelectionKey
OP_ACCEPT );
//當有已注冊的事件發生時
select()返回值將大於
while (acceptKey
selector()
select() >
) {
System
out
println(
event happened
);
//取得所有已經准備好的所有選擇鍵
Set readyKeys = this
selector
selectedKeys();
//使用迭代器對選擇鍵進行輪詢
I
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26159.html