2013-03-11 80 views
9

我是在線遊戲的主要開發人員。 玩家使用通過TCP/IP(TCP,而不是UDP)連接到遊戲服務器的特定客戶端軟件從經典的多線程到java.nio異步/非阻塞服務器

目前,服務器的體系結構是一個經典的多線程服務器,每個連接只有一個線程。 但是在繁忙時間,當連接的人數通常爲300或400人時,服務器越來越滯後。

我想知道,如果通過切換到java.nio。*異步I/O模型來處理多個連接的線程,如果性能會更好。 在Web上查找涵蓋此類服務器體系結構基礎知識的示例代碼非常簡單。然而,經過幾個小時的谷歌搜索,我沒有找到一些更高級的問題的答案:

1 - 該協議是基於文本,而不是基於二進制。客戶端和服務器交換以UTF-8編碼的文本行。單行文本代表單個命令,每行都以\ n或\ r \ n正確終止。 對於經典的多線程服務器,我有這樣的代碼:

public Connection (Socket sock) { 
this.in = new BufferedReader(new InputStreamReader(sock.getInputStream(), "UTF-8")); 
this.out = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream(), "UTF-8")); 
new Thread(this) .start(); 
} 

,並通過與線的readLine然後運行,數據讀取線。

在文檔中,我找到了一個可以從SocketChannel創建Reader的實用類Channels。但據說,如果頻道處於非阻塞模式,生成的閱讀器將無法工作,這與非阻塞模式必須使用我願意使用的高性能頻道選擇API的必要性相矛盾。所以,我懷疑這不是我想要做的正確解決方案。 因此,第一個問題是:如果我不能使用它,如何有效地並正確地處理在nio API中使用緩衝區和通道轉換原生Java字符串和從UTF-8編碼的數據轉換爲UTF-8編碼數據? 我是否必須手動使用get/put或包裝的字節數組?如何從ByteBuffer轉換爲以UTF-8編碼的字符串?我承認不太清楚如何在charset包中使用類,以及它如何工作。

2 - 在異步/非阻塞I/O世界中,如何處理連續讀取/寫入本質上依次執行的順序? 例如,登錄過程通常是基於質詢 - 響應的:服務器發送一個問題(特定的計算),客戶端發送響應,然後服務器檢查客戶端給出的響應。 答案是,我認爲,在整個登錄過程中肯定不會發送一個任務發送給工作線程,因爲這個過程非常長,並且存在凍結工作線程太多時間的風險(想象一下:10個池線程,10個玩家嘗試同時連接;與已經在線的玩家有關的任務被延遲,直到一個線程再次準備好爲止)。

3 - 如果兩個不同的線程同時在同一個通道上調用Channel.write(ByteBuffer)會發生什麼? 客戶可能收到混合線嗎?例如,如果一個線程發送「aaaaa」,另一個發送「bbbbb」,客戶端是否可以收到「aaabbbbbaa」,或者我確保每個郵件都是按順序發送的?呼叫返回後,我允許修改使用的緩衝區嗎? 或者換個問題,我需要額外的同步來避免這種情況嗎? 如果我需要額外的同步,在寫入完成後如何知道發佈何時鎖定等等? 恐怕答案並不像在選擇器中註冊OP_WRITE那麼簡單。通過這樣做,我注意到我始終都會收到準備就緒的事件,並始終爲所有客戶端退出Selector。大部分時間選擇爲無,因爲每個客戶端只有3或4條消息發送人,而選擇循環每秒執行數百次。因此,潛在地,主動地等待觀點,那是非常糟糕的。

4 - 多個線程可以在同一個選擇器上同時調用Selector.select,而沒有任何併發​​問題,比如缺少事件,調度兩次等等。

5 - 事實上,nio和它所說的一樣好?保持經典的多線程模型是否有趣,但是不建立每個連接的線程,使用更少的線程並在連接上循環以查找使用InputStream.isAvailable的數據可用性?這個想法是愚蠢的還是低效的?

+0

對於某些示例代碼,請查看Netty的源代碼:https://github.com/netty/netty它是一個非常好的庫。 – 2013-03-11 20:58:08

回答

4

1)是的。我認爲你需要編寫你自己的非阻塞readLine方法。還要注意的是,當有多個線路中的緩衝器,或者當存在一個不完整的線非阻塞讀取可以用信號:

實施例:(第一讀)

USER foo 
PASS 

(第二讀取)

bar 

您需要存儲(參見2)未被使用的數據,直到有足夠的信息準備好處理它爲止。

//channel was select for OP_READ 
read data from channel 
prepend data from previous read 
split complete lines 
save incomplete line 
execute commands 

2)您將需要保持每個客戶端的狀態。

Map<SocketChannel,State> clients = new HashMap<SocketChannel,State>(); 

當信道被連接,put新鮮的狀態到地圖

clients.put(channel,new State()); 

或存儲的當前狀態作爲SelectionKeythe attached object

然後,當執行每個命令時,更新狀態。你可以把它寫成一個單一的方法,或者做一些更奇特的事情,例如State的多態實現,其中每個狀態都知道如何處理某些命令(例如LoginState需要USER和PASS,然後將狀態更改爲新的AuthorizedState)。

3)我不記得每個通道使用NIO與許多異步寫入器,但文檔說它是線程安全的(我不會詳細說明,因爲我沒有證明這一點)。關於OP_WRITE,請注意,當寫入緩衝區爲未滿時,它會發出信號。換句話說,正如所說的here:OP_WRITE幾乎總是準備好,即當套接字發送緩衝區已滿時,所以你只會導致你的方法無意識地旋轉。

4)是的。 Selector.select()執行blocking selection operation

5)我認爲最困難的部分是從每個客戶端線程架構切換到讀取和寫入與處理分離的不同設計。一旦你完成了這個工作,使用頻道的工作比在阻止流的情況下自己的工作更容易。

+0

你可以添加一些精度?問題1:我需要手動播放get/put在緩衝區中,還是在包裝的字節數組中手動​​搜索\ n來拆分這些行,或者有更好的方法嗎? Q2:我可以使用SelectionKey的用戶對象來存儲該狀態信息嗎? +我將在問題1中添加一個額外的子問題以更加精確。感謝您的回答。 – QuentinC 2013-03-11 21:42:21