Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 497, comments - 13, trackbacks - 0, articles - 1

          導航

          公告


            ① 即時通訊開發社區
            地址: 52im.net
            專業的資料、社區

            ② 關注我的公眾號:

            讓技術不再封閉

            ③ 我的Github
            地址: 點此進入
            好代碼,與大家分享
          <2016年6月>
          2930311234
          567891011
          12131415161718
          19202122232425
          262728293012
          3456789

          常用鏈接

          留言簿(286)

          隨筆檔案

          文章檔案

          搜索

          •  

          最新評論

          閱讀排行榜

          評論排行榜

          60天內閱讀排行

          前言

          NIO框架的流行,使得開發大并發、高性能的互聯網服務端成為可能。這其中最流行的無非就是MINA和Netty了,MINA目前的主要版本是MINA2、而Netty的主要版本是Netty3Netty4(Netty5已經被取消開發了:詳見此文)。

          本文將演示的是一個基于MINA2的UDP服務端和一個標準UDP客戶端(Java實現)雙向通信的完整例子。

          實際上,MINA2的官方代碼里有完整的UDP通信Demo代碼,但Demo里客戶端是需要依賴MINA2的客戶端庫的,而如果簡單地去掉MINA2的lib依賴,改為標準的Java UDP代碼,則服務端和客戶端的數據收發都會存在未知多余字節等。當然,本文已解決了這些問題,請繼續往下閱讀。

          《NIO框架入門》系列文章

          有關MINA和Netty的入門文章很多,但多數都是復制、粘貼的未經證實的來路不明內容,對于初次接觸的人來說,一個可以運行且編碼規范的Demo,顯然要比各種“詳解”、“深入分析”之類的要來的直接和有意義。本系列入門文章正是基于此種考慮而寫,雖無精深內容,但至少希望對初次接觸MINA、Netty的人有所啟發,起到拋磚引玉的作用。


          本文是《NIO框架入門》系列文章中的第2篇,目錄如下:

          本文亮點

          • Demo中的代碼源自作者的開源工程,有實用價值:
            源碼均修改自作者的即時通訊開源工程 MobileIMSDK,只是為了方便學習理解而作了簡化,有一定的實用價值;
          • 本例中客戶端UDP實現無需第3方依賴:
            通常MINA或Netty的例子里,客戶端也都是用的MINA或Netty的客戶端lib,本文將直接使用標準UDP代碼(便于跨平臺實現,比如iOS平臺),不依賴于第3方包;
          • 完整可執行源碼、方便學習:
            完整的Demo源碼,適合新手直接運行,便于學習和研究。

          Demo的代碼邏輯

          言歸正傳,本文要演示的Demo包含兩部分,Java UDP客戶端和MINA2 UDP服務端,客戶端將每隔3秒向服務端發送一條消息,而服務端在收到消息后馬上回復一條消息給客戶端。


          也就是說,服務端和客戶端都要實現消息的發送和接收,這也就實現了雙向通信。如果有心的話,稍加改造,也就很容易實現一個簡陋的聊天程序了。下節將將給出真正的實現代碼。


          補充一句,本文中的客戶端代碼跟系列文章中的上篇《NIO框架入門(一):服務端基于Netty4的UDP雙向通信Demo演示》,用的是相同的代碼,如果您正在評估MIN2和Netty4的UDP服務端區別,可對照著上篇,進行詳細對照和分析哦。

          MINA2服務端準備工作

          [1] 第一步:下載MINA2

          MINA2的官方網站是:http://mina.apache.org/,最新版本是 Mina 2.0.13,如下圖:


          補充說明:建議2進制和源碼都下載下來,2進制制包中是編譯好的jar包,源碼包中包含了官方的Demo源代碼,需要的時候可以對照著看看,對于最佳實踐來說,有一定的參考價值。


          [2] 第二步:找到MINA2的核心庫文件

          一般情況下,只需要mina-core-2.0.13.jar包就行了,見下圖:


          補充說明:因為MINA2中使用了slf日框架,需要找到相應的lib才能最終運行,具體下一段再說明。


          [3] 第三步:建好MINA2的服務端Java工程,準備開擼

          個人習慣用Eclipse,你如用NetBeans或InteliJ也沒問題,具體建立過程不熟練的請自行百度吧,唯一注意的是把MINA2的lib包引用進來,我的見下圖:


          補充說明:MINA2中需要的slf日志框架的jar包,主要額外找到并導進來,不然不能最終運行哦。你也可以下載本Demo中的服務端源碼,直接用就是了。

          服務端代碼實現

          [1] 服務端主類 EchoServer.java:

           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看起來那么直觀。當然,僅是個人理解,不當之外請見諒。


          [2] 服務端Handler類 EchoSeverHandler.java:

           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 }


          [3] 重要說明

          注意點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] 客戶端主類 EchoClient.java:

           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(去掉了很多保證健壯性代碼,現在看起來要簡潔的多,便于初學者學習)。


          [2] Socket操作類 LocalUDPSocketProvider.java:

           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 }


          [3] 數據接收類 LocalUDPDataReciever.java:

           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 }

          Demo執行效果

          [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)



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 鄂托克前旗| 板桥市| 三门峡市| 天气| 浙江省| 建平县| 孟村| 武强县| 融水| 安乡县| 景谷| 丹阳市| 东宁县| 庆安县| 睢宁县| 枣强县| 新竹县| 三亚市| 佛山市| 开化县| 云浮市| 威远县| 莱西市| 北票市| 峡江县| 肥乡县| 文昌市| 沙田区| 烟台市| 宁乡县| 安多县| 高邮市| 西林县| 庆阳市| 宝坻区| 新巴尔虎右旗| 黄龙县| 台南市| 辛集市| 昭平县| 镇平县|