NIO框架的流行,使得開發大併發、高性能的互聯網服務端成爲可能。這其中最流行的無非就是MINA和Netty了,MINA目前的主要版本是MINA2、而Netty的主要版本是Netty3和Netty4(Netty5已經被取消開發了:詳見此文)。
本文將演示的是一個基於MINA2的UDP服務端和一個標準UDP客戶端(Java實現)雙向通訊的完整例子。
實際上,MINA2的官方代碼裏有完整的UDP通訊Demo代碼,但Demo裏客戶端是須要依賴MINA2的客戶端庫的,而若是簡單地去掉MINA2的lib依賴,改成標準的Java UDP代碼,則服務端和客戶端的數據收發都會存在未知多餘字節等。固然,本文已解決了這些問題,請繼續往下閱讀。php
- 更多即時通信技術資料:http://www.52im.net/forum.php?mod=collection&op=all
html
- 移動端即時通信交流羣:215891622 推薦java
有關MINA和Netty的入門文章不少,但多數都是複製、粘貼的未經證明的來路不明內容,對於初次接觸的人來講,一個能夠運行且編碼規範的Demo,顯然要比各類「詳解」、「深刻分析」之類的要來的直接和有意義。本系列入門文章正是基於此種考慮而寫,雖無精深內容,但至少但願對初次接觸MINA、Netty的人有所啓發,起到拋磚引玉的做用。
本文是《NIO框架入門》系列文章中的第 2 篇,目錄以下:git
言歸正傳,本文要演示的Demo包含兩部分,Java UDP客戶端和MINA2 UDP服務端,客戶端將每隔3秒向服務端發送一條消息,而服務端在收到消息後立刻回覆一條消息給客戶端。
也就是說,服務端和客戶端都要實現消息的發送和接收,這也就實現了雙向通訊。若是有心的話,稍加改造,也就很容易實現一個簡陋的聊天程序了。下節將將給出真正的實現代碼。
補充一句,本文中的客戶端代碼跟系列文章中的上篇《NIO框架入門(一):服務端基於Netty4的UDP雙向通訊Demo演示》,用的是相同的代碼,若是您正在評估MIN2和Netty4的UDP服務端區別,可對照着上篇,進行詳細對照和分析哦。程序員
MINA2的官方網站是:http://mina.apache.org/,最新版本是 Mina 2.0.13,以下圖:github
補充說明:建議2進制和源碼都下載下來,2進制制包中是編譯好的jar包,源碼包中包含了官方的Demo源代碼,須要的時候能夠對照着看看,對於最佳實踐來講,有必定的參考價值。
apache
通常狀況下,只須要mina-core-2.0.13.jar包就好了,見下圖:
補充說明:由於MINA2中使用了slf日框架,須要找到相應的lib才能最終運行,具體下一段再說明。
編程
我的習慣用Eclipse,你如用NetBeans或InteliJ也沒問題,具體創建過程不熟練的請自行百度吧,惟一注意的是把MINA2的lib包引用進來,個人見下圖:
補充說明:MINA2中須要的slf日誌框架的jar包,主要額外找到並導進來,否則不能最終運行哦。你也能夠下載本Demo中的服務端源碼,直接用就是了。api
1 public class EchoServer 2 { 3 private static Logger logger = LoggerFactory.getLogger(EchoServer.class); 4 5 public static void main(String[] args) throws Exception 6 { 7 // ** Acceptor設置 8 NioDatagramAcceptor acceptor = new NioDatagramAcceptor(); 9 // 此行代碼能讓你的程序總體性能提高10倍 10 acceptor.getFilterChain() 11 .addLast("threadPool", new ExecutorFilter(Executors.newCachedThreadPool())); 12 // 設置MINA2的IoHandler實現類 13 acceptor.setHandler(new EchoSeverHandler()); 14 // 設置會話超時時間(單位:毫秒),不設置則默認是10秒,請按需設置 15 acceptor.setSessionRecycler(new ExpiringSessionRecycler(15 * 1000)); 16 17 // ** UDP通訊配置 18 DatagramSessionConfig dcfg = acceptor.getSessionConfig(); 19 dcfg.setReuseAddress(true); 20 // 設置輸入緩衝區的大小,壓力測試代表:調整到2048後性能反而下降 21 dcfg.setReceiveBufferSize(1024); 22 // 設置輸出緩衝區的大小,壓力測試代表:調整到2048後性能反而下降 23 dcfg.setSendBufferSize(1024); 24 25 // ** UDP服務端開始偵聽 26 acceptor.bind(new InetSocketAddress(9999)); 27 28 logger.info("[IMCORE]UDP服務器正在端口 9999 上監聽中..."); 29 } 30 }
補充說明:MINA2的UDP服務端看起來比Netty4的要繁瑣一些(有關Netty4的UDP服務端請見:http://www.52im.net/thread-367-1-1.html),實際上MINA2的代碼風格更符合通常程序員的編碼習慣,更好懂一些,而Netty4因歷經多個大版本的進化,看起來很是簡潔,但實現上並無MINA2看起來那麼直觀。固然,僅是我的理解,不當以外請見諒。
數組
1 public class EchoSeverHandler extends IoHandlerAdapter 2 { 3 private static Logger logger = LoggerFactory.getLogger(EchoSeverHandler.class); 4 public static final CharsetDecoder decoder = (Charset.forName("UTF-8")).newDecoder(); 5 6 /** 7 * MINA的異常回調方法。 8 * <p> 9 * 本類中將在異常發生時,當即close當前會話。 10 * 11 * @param session 發生異常的會話 12 * @param cause 異常內容 13 * @see IoSession#close(boolean) 14 */ 15 @Override 16 public void exceptionCaught(IoSession session, Throwable cause) throws Exception 17 { 18 logger.error("[IMCORE]exceptionCaught捕獲到錯了,緣由是:"+cause.getMessage(), cause); 19 session.close(true); 20 } 21 22 /** 23 * MINA框架中收到客戶端消息的回調方法。 24 * <p> 25 * 本類將在此方法中實現完整的即時通信數據交互和處理策略。 26 * <p> 27 * 爲了提高併發性能,本方法將運行在獨立於MINA的IoProcessor以外的線程池中, 28 * 詳見 {@link ServerLauncher#initAcceptor()}中的MINA設置代碼 。 29 * 30 * @param session 收到消息對應的會話引用 31 * @param message 收到的MINA的原始消息封裝對象,本類中是 {@link IoBuffer}對象 32 * @throws Exception 當有錯誤發生時將拋出異常 33 */ 34 @Override 35 public void messageReceived(IoSession session, Object message)throws Exception 36 { 37 //*********************************************** 接收數據 38 // 讀取收到的數據 39 IoBuffer buffer = (IoBuffer) message; 40 String body = buffer.getString(decoder); 41 // 注意:當客戶使用不依賴於MINA庫的狀況下,如下官方推 42 // 薦的讀取方法會在數據首部出現幾個字節的未知亂碼 43 // message.toString() 44 logger.debug("【NOTE】>>>>>> 收到客戶端的數據:"+body); 45 46 //*********************************************** 回覆數據 47 String strToClient = "Hello,我是Server,個人時間戳是"+System.currentTimeMillis(); 48 byte[] res = strToClient.getBytes("UTF-8"); 49 // 組織IoBuffer數據包的方法:本方法才能夠正確地讓客戶端UDP收到byte數組 50 IoBuffer buf = IoBuffer.wrap(res); 51 52 // 向客戶端寫數據 53 WriteFuture future = session.write(buf); 54 // 在100毫秒超時間內等待寫完成 55 future.awaitUninterruptibly(100); 56 // The message has been written successfully 57 if( future.isWritten() ) 58 { 59 // send sucess! 60 } 61 // The messsage couldn't be written out completely for some reason. 62 // (e.g. Connection is closed) 63 else 64 { 65 logger.warn("[IMCORE]回覆給客戶端的數據發送失敗!"); 66 } 67 } 68 }
注意點1:服務端的EchoSeverHandler.java文件中,接收數據時取得文本內容的兩行代碼是解決無MINA2客戶端依賴的關鍵,具體代碼以下:
IoBuffer buffer = (IoBuffer) message;
String body = buffer.getString(decoder);
而官方的代碼是直接:message.toString(),這會存在不能正確地解碼標準Java UDP代碼實現的客戶端發過來的數據問題哦。
注意點2:服務端的EchoSeverHandler.java文件中,發送數據時的IoBuffer對象組織方法是解決無MINA2客戶端依賴的客戶端能正確接收MINA2服務端發過去的數據的關鍵,具體代碼以下:
IoBuffer buf = IoBuffer.wrap(res);
而官方的Demo代碼以及網上的各類實現更爲複雜,但會致使客戶端接收數據有多餘字節,官方的會產生多餘字節和亂碼的代碼以下:
IoBuffer buf = IoBuffer.allocate(res.length); buf.setAutoExpand(true); buf.putInt(res.length); buf.put(res); buf.flip(); buf.shrink();
爲了讓客戶端代碼看起來更簡潔,我把Socket管理代碼提煉到 LocalUDPSocketProvider類、把UDP數據監聽和接收提煉到了 LocalUDPDataReciever類(實際上這兩個同名類是簡化自MobileIMSDK工程哦)。
1 public class EchoClient 2 { 3 public static void main(String[] args) throws Exception 4 { 5 // 初始化本地UDP的Socket 6 LocalUDPSocketProvider.getInstance().initSocket(); 7 // 啓動本地UDP監聽(接收數據用的) 8 LocalUDPDataReciever.getInstance().startup(); 9 10 // 循環發送數據給服務端 11 while(true) 12 { 13 // 要發送的數據 14 String toServer = "Hi,我是客戶端,個人時間戳"+System.currentTimeMillis(); 15 byte[] soServerBytes = toServer.getBytes("UTF-8"); 16 17 // 開始發送 18 boolean ok = UDPUtils.send(soServerBytes, soServerBytes.length); 19 if(ok) 20 Log.d("EchoClient", "發往服務端的信息已送出."); 21 else 22 Log.e("EchoClient", "發往服務端的信息沒有成功發出!!!"); 23 24 // 3000秒後進入下一次循環 25 Thread.sleep(3000); 26 } 27 } 28 }
補充說明:客戶端代碼沒有使用任何依賴,純Java UDP代碼實現(若是是Andriod平臺,代碼也幾乎不用改就能用),部分代碼修改自 開源即時通信框架 MobileIMSDK(去掉了不少保證健壯性代碼,如今看起來要簡潔的多,便於初學者學習)。
1 public class LocalUDPSocketProvider 2 { 3 private static final String TAG = LocalUDPSocketProvider.class.getSimpleName(); 4 private static LocalUDPSocketProvider instance = null; 5 private DatagramSocket localUDPSocket = null; 6 7 public static LocalUDPSocketProvider getInstance() 8 { 9 if (instance == null) 10 instance = new LocalUDPSocketProvider(); 11 return instance; 12 } 13 14 public void initSocket() 15 { 16 try 17 { 18 // UDP本地監聽端口(若是爲0將表示由系統分配,不然使用指定端口) 19 this.localUDPSocket = new DatagramSocket(ConfigEntity.localUDPPort); 20 // 調用connect以後,每次send時DatagramPacket就不須要設計目標主機的ip和port了 21 // * 注意:connect方法必定要在DatagramSocket.receive()方法以前調用, 22 // * 否則整send數據將會被錯誤地阻塞。這或許是官方API的bug,也或許是調 23 // * 用規範就應該這樣,但沒有找到官方明確的說明 24 this.localUDPSocket.connect( 25 InetAddress.getByName(ConfigEntity.serverIP), ConfigEntity.serverUDPPort); 26 this.localUDPSocket.setReuseAddress(true); 27 Log.d(TAG, "new DatagramSocket()已成功完成."); 28 } 29 catch (Exception e) 30 { 31 Log.w(TAG, "localUDPSocket建立時出錯,緣由是:" + e.getMessage(), e); 32 } 33 } 34 35 public DatagramSocket getLocalUDPSocket() 36 { 37 return this.localUDPSocket; 38 } 39 }
1 public class LocalUDPDataReciever 2 { 3 private static final String TAG = LocalUDPDataReciever.class.getSimpleName(); 4 private static LocalUDPDataReciever instance = null; 5 private Thread thread = null; 6 7 public static LocalUDPDataReciever getInstance() 8 { 9 if (instance == null) 10 instance = new LocalUDPDataReciever(); 11 return instance; 12 } 13 14 public void startup() 15 { 16 this.thread = new Thread(new Runnable() 17 { 18 public void run() 19 { 20 try 21 { 22 Log.d(LocalUDPDataReciever.TAG, "本地UDP端口偵聽中,端口=" + ConfigEntity.localUDPPort + "..."); 23 24 //開始偵聽 25 LocalUDPDataReciever.this.udpListeningImpl(); 26 } 27 catch (Exception eee) 28 { 29 Log.w(LocalUDPDataReciever.TAG, "本地UDP監聽中止了(socket被關閉了?)," + eee.getMessage(), eee); 30 } 31 } 32 }); 33 this.thread.start(); 34 } 35 36 private void udpListeningImpl() throws Exception 37 { 38 while (true) 39 { 40 byte[] data = new byte[1024]; 41 // 接收數據報的包 42 DatagramPacket packet = new DatagramPacket(data, data.length); 43 44 DatagramSocket localUDPSocket = LocalUDPSocketProvider.getInstance().getLocalUDPSocket(); 45 if ((localUDPSocket == null) || (localUDPSocket.isClosed())) 46 continue; 47 48 // 阻塞直到收到數據 49 localUDPSocket.receive(packet); 50 51 // 解析服務端發過來的數據 52 String pFromServer = new String(packet.getData(), 0 , packet.getLength(), "UTF-8"); 53 Log.w(LocalUDPDataReciever.TAG, "【NOTE】>>>>>> 收到服務端的消息:"+pFromServer); 54 } 55 } 56 }
[1] 客戶端運行結果:
[2] 服務端運行結果:
補充說明:服務端的運行結果中,控制檯下每次多出的4行log輸出,其實是MINA2內部代碼的debug信息,目前除非關閉整個log的輸出,否則它必定會出現,但願你在運行代碼時看到這莫名其妙的4行log內容,不要感到奇怪哦,這4行log大體內容以下所示:
[DEBUG] - [13:26:36.393]Event MESSAGE_RECEIVED has been fired for session 1 | (IoFilterEvent^fire:117) [DEBUG] - [13:26:36.393]Firing a MESSAGE_SENT event for session 1 | (IoFilterEvent^fire:68) [DEBUG] - [13:26:36.393]Event MESSAGE_SENT has been fired for session 1 | (IoFilterEvent^fire:117) [DEBUG] - [13:26:39.393]Firing a MESSAGE_RECEIVED event for session 1 | (IoFilterEvent^fire:68)
客戶端代碼就是標準的Java UDP代碼(無MINA2的客戶端依賴),看起來跟其它例子不同的緣由只是我把它提煉了一下,沒本質區別。一樣的代碼改改也能夠很好的用在Android端。實際上,上面的客戶端代碼就是從開源 MobileIMSDK 的Java端扒出來的,有興趣的也能夠看看它的Android端、Server端、iOS端,簡化一下能夠用做你自已的各類用途。
若是你閱讀過本系列的上一篇《NIO框架入門(一):服務端基於Netty4的UDP雙向通訊Demo演示》,應該能明顯地感受的出來MINA2的UDP服務端API接口使用要是Netty4的繁瑣,並且MINA2還存在獨立客戶端(非依賴於MINA2客戶端)實現時的多餘字節和亂碼問題。但我的認爲MINA2的代碼風格更符合通常程序員的編碼習慣,更好懂一些,而Netty4因歷經多個大版本的進化,雖起來很是簡潔,但實現並非那麼直觀。固然,至於MINA仍是Netty,請客觀一評估和使用,由於兩者並沒有本質區別。
[1] MINA和Netty的源碼在線學習和查閱:
MINA-2.x地址是:http://docs.52im.net/extend/docs/src/mina2/
MINA-1.x地址是:http://docs.52im.net/extend/docs/src/mina1/
Netty-4.x地址是:http://docs.52im.net/extend/docs/src/netty4/
Netty-3.x地址是:http://docs.52im.net/extend/docs/src/netty3/
[2] MINA和Netty的API文檔在線查閱:
MINA-2.x API文檔(在線版):http://docs.52im.net/extend/docs/api/mina2/
MINA-1.x API文檔(在線版):http://docs.52im.net/extend/docs/api/mina1/
Netty-4.x API文檔(在線版):http://docs.52im.net/extend/docs/api/netty4/
Netty-3.x API文檔(在線版):http://docs.52im.net/extend/docs/api/netty3/
[3] 更多有關NIO編程的資料:
請進入精華資料專輯:http://www.52im.net/forum.php?mod=collection&action=view&ctid=9
[4] 有關IM聊天應用、消息推送技術的資料:
請進入精華資料專輯:http://www.52im.net/forum.php?mod=collection&op=all
[5] 技術交流和學習:
可直接進入 即時通信開發者社區 討論和學習網絡編程、IM聊天應用、消息推送應用的開發。
博客園上不知如何上傳附件,如需完整Eclipse源碼工程請聯繫做者,或者進入連接 http://www.52im.net/thread-373-1-1.html 自行下載。
完整源碼工程截圖以下:
(本文同步發佈於:http://www.52im.net/thread-373-1-1.html)