前言
因為打算用java編寫異步通信的server和client程序,筆者便學習使用java.nio
開發包,其間遇到一些問題,上網卻發現網上對它的應用描述的不是很多。所以,筆者不惜班門弄斧,做些簡單的討論,以便大家更進一步的討論。
對相關類的簡單介紹
java.nio.*, 據說它提供了一些更加底層的一些功能,如:類似windows環境下的
AsyncSocket類的異步操作的功能,能顯著降低server端程序的線程管理開銷。
因為大多數應用是建立在TCP之上,所以在此只說說SocketChannel,
ServerSocketChannel,Selector和ByteBuffer這幾個類.前三個最終都源自channel類。而channel 類,可以理解為在具體I/O或文件對象之上抽象的一個操作對象,我們通過操作channel的讀寫達到對其對應的文件或I/O對象(包括socket)讀寫的目的。讀寫的內容在內存中放在ByteBuffer類提供的緩沖區。總而言之,channel作為一個橋梁,連接了I/O對象和內存中的ByteBuffer,實現了I/O的更高效的存取。
一個基于TCP的服務器端程序,必然有個偵聽端和若干個通信端,它們在nio中由對應的ServerSocketChannel 和SocketChannel類來實現。為了達到異步I/O操作的目的,需要Selector類,它能檢測到I/O對象的狀態。
SocketChannel類是抽象類,通過調用它的靜態函數open(),可生成一個
SocketChannel對象,該對象對應一個java.net.Socket,可通過SocketChannel.socket()獲得,而其對應的Socket也可通過調用函數getChannel()得到已建立的相應SocketChannel。
SocketChannel與它的socket是一一對應的。SocketChannel的操作與Socket也很相似.
ServerSocketChannel也是通過調用它的靜態函數open()生成的,只是它不能
直接調用bind()函數來綁定一個地址,需要它對應的ServerSocket來完成綁定工作,一般可按如下步驟做:
ServerSocketChannel ssc = new ServerSocketChannel.open();
ssc.socket().bind(InetSocketAddress(host,port));
羅嗦了半天,還是看看最簡單的C/S實現吧,服務器提供了基本的回射(echo)功
能,其中提供了較詳細的注釋。
源碼分析
1.服務器端:
////////////////////////
//AsyncServer.java
// by zztudou@163.com
////////////////////////
import java.nio.channels.SocketChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.spi.SelectorProvider;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Iterator;
import java.util.LinkedList;
import java.io.IOException;
class AsyncServer implements Runnable{
private ByteBuffer r_buff = ByteBuffer.allocate(1024);
private ByteBuffer w_buff = ByteBuffer.allocate(1024);
private static int port = 8848;
public AsyncServer(){
new Thread(this).start();
}
public void run(){
try{
//生成一個偵聽端
ServerSocketChannel ssc = ServerSocketChannel.open();
//將偵聽端設為異步方式
ssc.configureBlocking(false);
//生成一個信號監視器
Selector s = Selector.open();
//偵聽端綁定到一個端口
ssc.socket().bind(new InetSocketAddress(port));
//設置偵聽端所選的異步信號OP_ACCEPT
ssc.register(s,SelectionKey.OP_ACCEPT);
System.out.println("echo server has been set up 
");
while(true){
int n = s.select();
if (n == 0) {//沒有指定的I/O事件發生
continue;
}
Iterator it = s.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if (key.isAcceptable()) {//偵聽端信號觸發
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//接受一個新的連接
SocketChannel sc = server.accept();
sc.configureBlocking(false);
//設置該socket的異步信號OP_READ:當socket可讀時,
//觸發函數DealwithData();
sc.register(s,SelectionKey.OP_READ);
}
if (key.isReadable()) {//某socket可讀信號
DealwithData(key);
}
it.remove();
}
}
}
catch(Exception e){
e.printStackTrace();
}
}
public void DealwithData(SelectionKey key) throws IOException{
int count;
//由key獲取指定socketchannel的引用
SocketChannel sc = (SocketChannel)key.channel();
r_buff.clear();
//讀取數據到r_buff
while((count = sc.read(r_buff))> 0)
;
//確保r_buff可讀
r_buff.flip();
w_buff.clear();
//將r_buff內容拷入w_buff
w_buff.put(r_buff);
w_buff.flip();
//將數據返回給客戶端
EchoToClient(sc);
w_buff.clear();
r_buff.clear();
}
public void EchoToClient(SocketChannel sc) throws IOException{
while(w_buff.hasRemaining())
sc.write(w_buff);
}
public static void main(String args[]){
if(args.length > 0){
port = Integer.parseInt(args[0]);
}
new AsyncServer();
}
}
在當前目錄下運行:
javac AsynServer.java
后,若無編譯出錯,接下來可運行:
java AsynServer 或 java AsynServer ×××(端口號)
上述服務程序在運行時,可指定其偵聽端口,否則程序會取8848為默認端口。
2.客戶端的簡單示例:
////////////////////////
//AsyncClient.java
// by zztudou@163.com
////////////////////////
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
class AsyncClient{
private SocketChannel sc;
private final int MAX_LENGTH = 1024;
private ByteBuffer r_buff = ByteBuffer.allocate(MAX_LENGTH);
private ByteBuffer w_buff = ByteBuffer.allocate(MAX_LENGTH);
private static String host ;
private static int port = 8848;
public AsyncClient(){
try {
InetSocketAddress addr = new InetSocketAddress(host,port);
//生成一個socketchannel
sc = SocketChannel.open();
//連接到server
sc.connect(addr);
while(!sc.finishConnect())
;
System.out.println("connection has been established!
");
while(true){
//回射消息
String echo;
try{
System.err.println("Enter msg you'd like to send: ");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//輸入回射消息
echo = br.readLine();
//把回射消息放入w_buff中
w_buff.clear();
w_buff.put(echo.getBytes());
w_buff.flip();
}catch(IOException ioe){
System.err.println("sth. is wrong with br.readline() ");
}
//發送消息
while(w_buff.hasRemaining())
sc.write(w_buff);
w_buff.clear();
//進入接收狀態
Rec();
//間隔1秒
Thread.currentThread().sleep(1000);
}
}catch(IOException ioe){
ioe.printStackTrace();
}
catch(InterruptedException ie){
ie.printStackTrace();
}
}
////////////
//讀取server端發回的數據,并顯示
public void Rec() throws IOException{
int count;
r_buff.clear();
count=sc.read(r_buff);
r_buff.flip();
byte[] temp = new byte[r_buff.limit()];
r_buff.get(temp);
System.out.println("reply is " + count +" long, and content is: " + new String(temp));
}
public static void main(String args[]){
if(args.length < 1){//輸入需有主機名或IP地址
try{
System.err.println("Enter host name: ");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
host = br.readLine();
}catch(IOException ioe){
System.err.println("sth. is wrong with br.readline() ");
}
}
else if(args.length == 1){
host = args[0];
}
else if(args.length > 1){
host = args[0];
port = Integer.parseInt(args[1]);
}
new AsyncClient();
}
}
在當前目錄下運行:
javac AsynClient.java
后,若無編譯出錯,確認AsyncServer已經運行的情況下,接下來可運行:
java AsynClient hostname 或 java AsynClient hostname ×××(端口號)
并按提示進行操作即可。
總結
總的來說,用nio進行網絡編程還是很有新意的,服務器端軟件能在一個線程中維護與眾多客戶端的通信連接。筆者在本文中試圖用一個典型的回射例子說明如何用nio建立最基本的C/S應用。希望大家能試著用用它。
另外,筆者在實踐中也發現nio在應用中存在的一些難題,比如如何應用SocketChannel的繼承類,以及如何在socketchannel之上應用SSL(Secure Socket Layer)等等,因而希望這篇文章只是拋磚引玉,引起大家對nio作進一步的討論。