本文詳細的介紹了使用Java語言建立一套多線程服務器的過程
該服務器使用對象傳遞消息
在線程中使用隊列機制
使服務器的性能大大提高了
這套服務器可以被用於各種C/S或B/S結構的應用程序中
Java語言是完全面向對象的它的線程機制和對象序列化特別容易使用使用Java來建立一套多線程服務器要比使用其它語言方便的多如果你再把它的異常處理機制利用好那麼你就可以建立一個商業級的多線程服務器了由於采用了消息隊列和Socket傳輸方式所以不會出現丟消息的問題這套服務器可以作為實時聊天服務器多人協同的協作服務器等等
消息系統的建立
這套服務器的消息系統采用的是對象傳輸的機制而不是以前常常使用的字符串傳輸采用對象傳輸的好處是擴展方便如需要建立一個新的消息只需要從一個統一的基類繼承下來然後再寫自己實現的方法就行了這樣也符合面向對象領域裡一條重要的原則OCP(open_closed Principle)即一個好的設計應該能夠容納新的功能的增加但是增加的方式不是修改原有的類而是添加新的類
首先建立一個基類Msg該抽象類中有兩個域sender和receiver分別紀錄消息的發送者和接收者這兩個域是在構造消息類時就填寫的receiver域可以為空空表示發給誰都可以由轉發服務器來決定該類的方法包括取得這兩個域的值和消息的處理函數消息的處理函數process()是空函數供繼承者重載
建立了這個抽象基類後你就可以繼承它完成你自己的類舉個例子假如我要建立一個分組協同工作的繪圖系統而且支持組員之間的對話那麼我可以建立如下的類集合
SendTextMsg(String senderString receiverString info)//向指定的人發送對話
AddLineMsg(String senderPoint aPoint b)//在指定的點之間繪制一條直線
AddRectangle(String senderpoint startPoint end)//建立指定的矩形
AddRotundaMsg(String senderPoint centerint radius)//建立指定的圓
RemoveObjectMsg(String senderint ID)//刪除指定編號的圖形對象
……
以此類推可以建立很多的消息類在每個類的內部都由一個處理該類的方法process()填寫該方法就可以實現對消息類的處理而服務器只負責完成消息的轉發功能這樣一套消息系統就建立了
服務器的結構
如果要服務器實現同時為每個客戶端服務就要使用多線程建立一個線程池當有客戶端連接時就在池中開辟一個線程為它服務同樣要避免大量消息到達時處理不過來而導致丟失的情況就要使用消息隊列這個服務器是分層的處理的
服務器的工作過程是這樣的建立了一個Server類作為主類它含有程序的入口函數main()在構造函數中初始化一個數組存放ClientSingle類它其實就是單獨處理一個連接用戶的類然後啟動一個線程PORTListenThread該線程的作用就是監聽端口上有沒有人登陸當有人連接時交給Server的addClient()處理Server的addClient()方法會在剛才那個數組中建立一個ClientSingle對象然後把剩下的事都交給它做
端口監聽線程類PORTListenThread
該線程類在run()函數的開始部分首先要檢查serverScoket是否為空保證循環開始時不要出錯然後進入一個死循環的監聽
while(true) { //死循環監
try{Socket clientSocket=null;
clientSocket=serverSocketaccept();
serveraddClient(clientSocket);//轉交Server處理
}
catch (IOException e){Systemoutprintln(監聽端口時出錯+e);}//顯示錯誤
}
單個客戶端在連接池中的映像類ClientSingle
每一個客戶端連接到服務器後服務器會自動在連接池中建立該客戶端的一個映像所有的操作都交給這個映像去具體執行所以ClientSingle中一定要包含客戶端的一些基本的信息比如客戶端的名稱登陸時間等等在該類中有兩個消息隊列sendQueue(發送隊列)和receiveQueue(接收隊列)緩存消息
ClientSingle類是繼承自Thread的它還是一個調用者在初始化的時候啟動兩個子線程類SingleSender和SingleListener運行SingleSender負責監聽指令發送隊列中有沒有指令有則發送SingleListener負責監聽有沒有消息到達有則把這些消息加入到接收隊列中去由ClientSingle處理所以ClientSingle的主要任務就是對這兩個隊列的處理這兩個隊列可以用Vector實現非常地簡單
//將消息加入發送隊列中
synchronized void send(Object o) { sendQueueadd(o); }
為了穩定控制子線程的運行並不鼓勵在run()方法的死循環標志都用true而是使用了一個布爾型的變量finish外部可以通過把這個標志置為假而停止線程的運行
發送子線程類啟動後執行run()中的循環(以finish為結束標志)在該循環內首先判斷ClientSingle中的發送隊列是否為空為空時睡眠一定的時間再重新判斷這也是一個while循環不為空則開始處理隊列中的消息把它取出後放入輸出流中發送
public void run(){
while (!fatherfinish){ //循環監聽
while(fathervisEmpty()){ //當發送隊列為空的時候線程睡眠毫秒
try{Threadsleep();}
catch(InterruptedException e){Systemoutprintln(e);}
}
if (!fathervisEmpty()){ //發送隊列不為空時
try{
Object a=fathervfirstElement();//取出隊列中的第一個消息
fathervremoveElementAt();//從隊列中刪除
ooswriteObject(a);//發送該消息
oosflush();
}catch(IOException e){
displayMessage( 傳輸失敗 !);
fatherfinish=false;
}
}
}
}
接收子線程SingleListener類和發送子線程是類似的它們的run()方法都差不多不同的是接收子線程把收到的消息加入到ClientSingle的接收隊列中去由它處理
ClientSingle類的run()方法就在循環地讀取接收隊列receiveQueue中的內容為空時等待不為空時依次取出處理和轉發處理消息的函數是processMsg()它只是執行消息類自己的process()方法罷了在處理完後會調用Server類的方法進行各種類型的轉發
分組轉發的實現類Group
為了實現對客戶端分組我建立了Group類在這個類中有一個列表存放已經存在於連接池中的那些ClientSingle類的引址只要遍歷整個列表就能訪問所有組中的成員這個列表可以用Vector實現也可以用哈希表我推薦後者主要是為了能夠按名字存取
組對象本身也是可以存在Server類的組列表中的
分組功能對多人的協同系統來說是非常重要的特別是分組對某一個共享空間操作的時候就以上面的協同繪圖系統為例如果個人裡有三個人要另起爐灶那麼他們三個的畫板就不能讓其他人看到這就必須有組個劃分
主服務器類Server
Server類是最核心的類它在這個框架中起到調度全局的作用上面介紹的那些類都由它來統一的構造和調用
Server類的域包括一個定長的數組存放ClientSingle實例它就是連接池的實現還要有一個哈希表存放Group實例Server類的方法都是對這兩個類的操作
建立ClientSingle數組的目的是保證服務器的穩定性其實你也可以選擇不建立它只是動態地構造對象但是那樣不好管理連接的用戶而且由於各種操作系統對進程的處理不同動態建立服務線程會很不穩定所以我先建立一個數組作為這些對象的容器在開始時就估計好連接者的最大數量Server類的addClient()函數
void addClient(Socket socket){
int c=;
try{while (sch[c]!=null) C++;}//搜索數組中的空余空間
catch(ArrayIndexOutOfBoundsException e){
try{ socketclose();}//出現異常關閉槽連接
catch(IOException ee){ Systemoutprintln(數組溢出);}
return;
}
sch[c]=new ClientSingle(csocketfatherthis);//在搜索到的位置建立ClientSingle對象
}
Server類中轉發的方法有sendToAll()sendToOne()sendToGroup()等等這些方法都是對線程池中的方法的操作比較簡單不外乎都是找到線程池中的某個ClientSingle對象然後調用它的send()方法罷了
注意這些轉發的方法可能被很多子線程同時調用所以為了保持線程的穩定千萬記住要在方法前加synchronized關鍵字
總結
通過上面的描述你可以發現要建立穩定的服務器程序消息隊列和線程池是很重要的此外也要考慮到很多的意外情況的發生一般的程序員在寫完線程的run()方法的循環後就不管了其實還應該考慮跳出循環後的資源釋放等等問題
From:http://tw.wingwit.com/Article/program/Java/gj/201311/27736.html