前言
NIO框架的流行,使得開(kāi)發(fā)大并發(fā)、高性能的互聯(lián)網(wǎng)服務(wù)端成為可能。這其中最流行的無(wú)非就是MINA和Netty了,MINA目前的主要版本是MINA2、而Netty的主要版本是Netty3和Netty4(Netty5已經(jīng)被取消開(kāi)發(fā)了:詳見(jiàn)此文)。
本文將演示的是一個(gè)基于MINA2的UDP服務(wù)端和一個(gè)標(biāo)準(zhǔn)UDP客戶端(Java實(shí)現(xiàn))雙向通信的完整例子。
實(shí)際上,MINA2的官方代碼里有完整的UDP通信Demo代碼,但Demo里客戶端是需要依賴MINA2的客戶端庫(kù)的,而如果簡(jiǎn)單地去掉MINA2的lib依賴,改為標(biāo)準(zhǔn)的Java UDP代碼,則服務(wù)端和客戶端的數(shù)據(jù)收發(fā)都會(huì)存在未知多余字節(jié)等。當(dāng)然,本文已解決了這些問(wèn)題,請(qǐng)繼續(xù)往下閱讀。
《NIO框架入門》系列文章
有關(guān)MINA和Netty的入門文章很多,但多數(shù)都是復(fù)制、粘貼的未經(jīng)證實(shí)的來(lái)路不明內(nèi)容,對(duì)于初次接觸的人來(lái)說(shuō),一個(gè)可以運(yùn)行且編碼規(guī)范的Demo,顯然要比各種“詳解”、“深入分析”之類的要來(lái)的直接和有意義。本系列入門文章正是基于此種考慮而寫,雖無(wú)精深內(nèi)容,但至少希望對(duì)初次接觸MINA、Netty的人有所啟發(fā),起到拋磚引玉的作用。
本文是《NIO框架入門》系列文章中的第2篇,目錄如下:
本文亮點(diǎn)
- Demo中的代碼源自作者的開(kāi)源工程,有實(shí)用價(jià)值:
源碼均修改自作者的即時(shí)通訊開(kāi)源工程 MobileIMSDK,只是為了方便學(xué)習(xí)理解而作了簡(jiǎn)化,有一定的實(shí)用價(jià)值; - 本例中客戶端UDP實(shí)現(xiàn)無(wú)需第3方依賴:
通常MINA或Netty的例子里,客戶端也都是用的MINA或Netty的客戶端lib,本文將直接使用標(biāo)準(zhǔn)UDP代碼(便于跨平臺(tái)實(shí)現(xiàn),比如iOS平臺(tái)),不依賴于第3方包; - 完整可執(zhí)行源碼、方便學(xué)習(xí):
完整的Demo源碼,適合新手直接運(yùn)行,便于學(xué)習(xí)和研究。
Demo的代碼邏輯
言歸正傳,本文要演示的Demo包含兩部分,Java UDP客戶端和MINA2 UDP服務(wù)端,客戶端將每隔3秒向服務(wù)端發(fā)送一條消息,而服務(wù)端在收到消息后馬上回復(fù)一條消息給客戶端。
也就是說(shuō),服務(wù)端和客戶端都要實(shí)現(xiàn)消息的發(fā)送和接收,這也就實(shí)現(xiàn)了雙向通信。如果有心的話,稍加改造,也就很容易實(shí)現(xiàn)一個(gè)簡(jiǎn)陋的聊天程序了。下節(jié)將將給出真正的實(shí)現(xiàn)代碼。
補(bǔ)充一句,本文中的客戶端代碼跟系列文章中的上篇《NIO框架入門(一):服務(wù)端基于Netty4的UDP雙向通信Demo演示》,用的是相同的代碼,如果您正在評(píng)估MIN2和Netty4的UDP服務(wù)端區(qū)別,可對(duì)照著上篇,進(jìn)行詳細(xì)對(duì)照和分析哦。
MINA2服務(wù)端準(zhǔn)備工作
[1] 第一步:下載MINA2
MINA2的官方網(wǎng)站是:http://mina.apache.org/,最新版本是 Mina 2.0.13,如下圖:
補(bǔ)充說(shuō)明:建議2進(jìn)制和源碼都下載下來(lái),2進(jìn)制制包中是編譯好的jar包,源碼包中包含了官方的Demo源代碼,需要的時(shí)候可以對(duì)照著看看,對(duì)于最佳實(shí)踐來(lái)說(shuō),有一定的參考價(jià)值。
[2] 第二步:找到MINA2的核心庫(kù)文件
一般情況下,只需要mina-core-2.0.13.jar包就行了,見(jiàn)下圖:

補(bǔ)充說(shuō)明:因?yàn)镸INA2中使用了slf日框架,需要找到相應(yīng)的lib才能最終運(yùn)行,具體下一段再說(shuō)明。
[3] 第三步:建好MINA2的服務(wù)端Java工程,準(zhǔn)備開(kāi)擼
個(gè)人習(xí)慣用Eclipse,你如用NetBeans或InteliJ也沒(méi)問(wèn)題,具體建立過(guò)程不熟練的請(qǐng)自行百度吧,唯一注意的是把MINA2的lib包引用進(jìn)來(lái),我的見(jiàn)下圖:

補(bǔ)充說(shuō)明:MINA2中需要的slf日志框架的jar包,主要額外找到并導(dǎo)進(jìn)來(lái),不然不能最終運(yùn)行哦。你也可以下載本Demo中的服務(wù)端源碼,直接用就是了。
服務(wù)端代碼實(shí)現(xiàn)
[1] 服務(wù)端主類 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設(shè)置
8 NioDatagramAcceptor acceptor = new NioDatagramAcceptor();
9 // 此行代碼能讓你的程序整體性能提升10倍
10 acceptor.getFilterChain()
11 .addLast("threadPool", new ExecutorFilter(Executors.newCachedThreadPool()));
12 // 設(shè)置MINA2的IoHandler實(shí)現(xiàn)類
13 acceptor.setHandler(new EchoSeverHandler());
14 // 設(shè)置會(huì)話超時(shí)時(shí)間(單位:毫秒),不設(shè)置則默認(rèn)是10秒,請(qǐng)按需設(shè)置
15 acceptor.setSessionRecycler(new ExpiringSessionRecycler(15 * 1000));
16
17 // ** UDP通信配置
18 DatagramSessionConfig dcfg = acceptor.getSessionConfig();
19 dcfg.setReuseAddress(true);
20 // 設(shè)置輸入緩沖區(qū)的大小,壓力測(cè)試表明:調(diào)整到2048后性能反而降低
21 dcfg.setReceiveBufferSize(1024);
22 // 設(shè)置輸出緩沖區(qū)的大小,壓力測(cè)試表明:調(diào)整到2048后性能反而降低
23 dcfg.setSendBufferSize(1024);
24
25 // ** UDP服務(wù)端開(kāi)始偵聽(tīng)
26 acceptor.bind(new InetSocketAddress(9999));
27
28 logger.info("[IMCORE]UDP服務(wù)器正在端口 9999 上監(jiān)聽(tīng)中...");
29 }
30 }
補(bǔ)充說(shuō)明:MINA2的UDP服務(wù)端看起來(lái)比Netty4的要繁瑣一些(有關(guān)Netty4的UDP服務(wù)端請(qǐng)見(jiàn):http://www.52im.net/thread-367-1-1.html),實(shí)際上MINA2的代碼風(fēng)格更符合一般程序員的編碼習(xí)慣,更好懂一些,而Netty4因歷經(jīng)多個(gè)大版本的進(jìn)化,看起來(lái)非常簡(jiǎn)潔,但實(shí)現(xiàn)上并沒(méi)有MINA2看起來(lái)那么直觀。當(dāng)然,僅是個(gè)人理解,不當(dāng)之外請(qǐng)見(jiàn)諒。
[2] 服務(wù)端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的異常回調(diào)方法。
8 * <p>
9 * 本類中將在異常發(fā)生時(shí),立即close當(dāng)前會(huì)話。
10 *
11 * @param session 發(fā)生異常的會(huì)話
12 * @param cause 異常內(nèi)容
13 * @see IoSession#close(boolean)
14 */
15 @Override
16 public void exceptionCaught(IoSession session, Throwable cause) throws Exception
17 {
18 logger.error("[IMCORE]exceptionCaught捕獲到錯(cuò)了,原因是:"+cause.getMessage(), cause);
19 session.close(true);
20 }
21
22 /**
23 * MINA框架中收到客戶端消息的回調(diào)方法。
24 * <p>
25 * 本類將在此方法中實(shí)現(xiàn)完整的即時(shí)通訊數(shù)據(jù)交互和處理策略。
26 * <p>
27 * 為了提升并發(fā)性能,本方法將運(yùn)行在獨(dú)立于MINA的IoProcessor之外的線程池中,
28 * 詳見(jiàn) {@link ServerLauncher#initAcceptor()}中的MINA設(shè)置代碼 。
29 *
30 * @param session 收到消息對(duì)應(yīng)的會(huì)話引用
31 * @param message 收到的MINA的原始消息封裝對(duì)象,本類中是 {@link IoBuffer}對(duì)象
32 * @throws Exception 當(dāng)有錯(cuò)誤發(fā)生時(shí)將拋出異常
33 */
34 @Override
35 public void messageReceived(IoSession session, Object message)throws Exception
36 {
37 //*********************************************** 接收數(shù)據(jù)
38 // 讀取收到的數(shù)據(jù)
39 IoBuffer buffer = (IoBuffer) message;
40 String body = buffer.getString(decoder);
41 // 注意:當(dāng)客戶使用不依賴于MINA庫(kù)的情況下,以下官方推
42 // 薦的讀取方法會(huì)在數(shù)據(jù)首部出現(xiàn)幾個(gè)字節(jié)的未知亂碼
43 // message.toString()
44 logger.debug("【NOTE】>>>>>> 收到客戶端的數(shù)據(jù):"+body);
45
46 //*********************************************** 回復(fù)數(shù)據(jù)
47 String strToClient = "Hello,我是Server,我的時(shí)間戳是"+System.currentTimeMillis();
48 byte[] res = strToClient.getBytes("UTF-8");
49 // 組織IoBuffer數(shù)據(jù)包的方法:本方法才可以正確地讓客戶端UDP收到byte數(shù)組
50 IoBuffer buf = IoBuffer.wrap(res);
51
52 // 向客戶端寫數(shù)據(jù)
53 WriteFuture future = session.write(buf);
54 // 在100毫秒超時(shí)間內(nèi)等待寫完成
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]回復(fù)給客戶端的數(shù)據(jù)發(fā)送失敗!");
66 }
67 }
68 }
[3] 重要說(shuō)明
注意點(diǎn)1:
服務(wù)端的EchoSeverHandler.java文件中,接收數(shù)據(jù)時(shí)取得文本內(nèi)容的兩行代碼是解決無(wú)MINA2客戶端依賴的關(guān)鍵,具體代碼如下:
IoBuffer buffer = (IoBuffer) message;
String body = buffer.getString(decoder);
而官方的代碼是直接:message.toString(),這會(huì)存在不能正確地解碼標(biāo)準(zhǔn)Java UDP代碼實(shí)現(xiàn)的客戶端發(fā)過(guò)來(lái)的數(shù)據(jù)問(wèn)題哦。
注意點(diǎn)2:
服務(wù)端的EchoSeverHandler.java文件中,發(fā)送數(shù)據(jù)時(shí)的IoBuffer對(duì)象組織方法是解決無(wú)MINA2客戶端依賴的客戶端能正確接收MINA2服務(wù)端發(fā)過(guò)去的數(shù)據(jù)的關(guān)鍵,具體代碼如下:
IoBuffer buf = IoBuffer.wrap(res);
而官方的Demo代碼以及網(wǎng)上的各種實(shí)現(xiàn)更為復(fù)雜,但會(huì)導(dǎo)致客戶端接收數(shù)據(jù)有多余字節(jié),官方的會(huì)產(chǎn)生多余字節(jié)和亂碼的代碼如下:
IoBuffer buf = IoBuffer.allocate(res.length);
buf.setAutoExpand(true);
buf.putInt(res.length);
buf.put(res);
buf.flip();
buf.shrink();
客戶端代碼實(shí)現(xiàn)
為了讓客戶端代碼看起來(lái)更簡(jiǎn)潔,我把Socket管理代碼提煉到 LocalUDPSocketProvider類、把UDP數(shù)據(jù)監(jiān)聽(tīng)和接收提煉到了 LocalUDPDataReciever類(實(shí)際上這兩個(gè)同名類是簡(jiǎn)化自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 // 啟動(dòng)本地UDP監(jiān)聽(tīng)(接收數(shù)據(jù)用的)
8 LocalUDPDataReciever.getInstance().startup();
9
10 // 循環(huán)發(fā)送數(shù)據(jù)給服務(wù)端
11 while(true)
12 {
13 // 要發(fā)送的數(shù)據(jù)
14 String toServer = "Hi,我是客戶端,我的時(shí)間戳"+System.currentTimeMillis();
15 byte[] soServerBytes = toServer.getBytes("UTF-8");
16
17 // 開(kāi)始發(fā)送
18 boolean ok = UDPUtils.send(soServerBytes, soServerBytes.length);
19 if(ok)
20 Log.d("EchoClient", "發(fā)往服務(wù)端的信息已送出.");
21 else
22 Log.e("EchoClient", "發(fā)往服務(wù)端的信息沒(méi)有成功發(fā)出!!!");
23
24 // 3000秒后進(jìn)入下一次循環(huán)
25 Thread.sleep(3000);
26 }
27 }
28 }
補(bǔ)充說(shuō)明:客戶端代碼沒(méi)有使用任何依賴,純Java UDP代碼實(shí)現(xiàn)(如果是Andriod平臺(tái),代碼也幾乎不用改就能用),部分代碼修改自 開(kāi)源即時(shí)通訊框架 MobileIMSDK(去掉了很多保證健壯性代碼,現(xiàn)在看起來(lái)要簡(jiǎn)潔的多,便于初學(xué)者學(xué)習(xí))。
[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本地監(jiān)聽(tīng)端口(如果為0將表示由系統(tǒng)分配,否則使用指定端口)
19 this.localUDPSocket = new DatagramSocket(ConfigEntity.localUDPPort);
20 // 調(diào)用connect之后,每次send時(shí)DatagramPacket就不需要設(shè)計(jì)目標(biāo)主機(jī)的ip和port了
21 // * 注意:connect方法一定要在DatagramSocket.receive()方法之前調(diào)用,
22 // * 不然整send數(shù)據(jù)將會(huì)被錯(cuò)誤地阻塞。這或許是官方API的bug,也或許是調(diào)
23 // * 用規(guī)范就應(yīng)該這樣,但沒(méi)有找到官方明確的說(shuō)明
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創(chuàng)建時(shí)出錯(cuò),原因是:" + e.getMessage(), e);
32 }
33 }
34
35 public DatagramSocket getLocalUDPSocket()
36 {
37 return this.localUDPSocket;
38 }
39 }
[3] 數(shù)據(jù)接收類 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端口偵聽(tīng)中,端口=" + ConfigEntity.localUDPPort + "...");
23
24 //開(kāi)始偵聽(tīng)
25 LocalUDPDataReciever.this.udpListeningImpl();
26 }
27 catch (Exception eee)
28 {
29 Log.w(LocalUDPDataReciever.TAG, "本地UDP監(jiān)聽(tīng)停止了(socket被關(guān)閉了?)," + 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 // 接收數(shù)據(jù)報(bào)的包
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 // 阻塞直到收到數(shù)據(jù)
49 localUDPSocket.receive(packet);
50
51 // 解析服務(wù)端發(fā)過(guò)來(lái)的數(shù)據(jù)
52 String pFromServer = new String(packet.getData(), 0 , packet.getLength(), "UTF-8");
53 Log.w(LocalUDPDataReciever.TAG, "【NOTE】>>>>>> 收到服務(wù)端的消息:"+pFromServer);
54 }
55 }
56 }
Demo執(zhí)行效果
[1] 客戶端運(yùn)行結(jié)果:

[2] 服務(wù)端運(yùn)行結(jié)果:

補(bǔ)充說(shuō)明:服務(wù)端的運(yùn)行結(jié)果中,控制臺(tái)下每次多出的4行l(wèi)og輸出,實(shí)際上是MINA2內(nèi)部代碼的debug信息,目前除非關(guān)閉整個(gè)log的輸出,不然它一定會(huì)出現(xiàn),希望你在運(yùn)行代碼時(shí)看到這莫名其妙的4行l(wèi)og內(nèi)容,不要感到奇怪哦,這4行l(wèi)og大致內(nèi)容如下所示:
[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)
結(jié)語(yǔ)
客戶端代碼就是標(biāo)準(zhǔn)的Java UDP代碼(無(wú)MINA2的客戶端依賴),看起來(lái)跟其它例子不一樣的原因只是我把它提煉了一下,沒(méi)本質(zhì)區(qū)別。同樣的代碼改改也可以很好的用在Android端。實(shí)際上,上面的客戶端代碼就是從開(kāi)源 MobileIMSDK 的Java端扒出來(lái)的,有興趣的也可以看看它的Android端、Server端、iOS端,簡(jiǎn)化一下可以用作你自已的各種用途。
如果你閱讀過(guò)本系列的上一篇《NIO框架入門(一):服務(wù)端基于Netty4的UDP雙向通信Demo演示》,應(yīng)該能明顯地感覺(jué)的出來(lái)MINA2的UDP服務(wù)端API接口使用要是Netty4的繁瑣,而且MINA2還存在獨(dú)立客戶端(非依賴于MINA2客戶端)實(shí)現(xiàn)時(shí)的多余字節(jié)和亂碼問(wèn)題。但個(gè)人認(rèn)為MINA2的代碼風(fēng)格更符合一般程序員的編碼習(xí)慣,更好懂一些,而Netty4因歷經(jīng)多個(gè)大版本的進(jìn)化,雖起來(lái)非常簡(jiǎn)潔,但實(shí)現(xiàn)并不是那么直觀。當(dāng)然,至于MINA還是Netty,請(qǐng)客觀一評(píng)估和使用,因?yàn)槎卟o(wú)本質(zhì)區(qū)別。
更多學(xué)習(xí)資源
[1] MINA和Netty的源碼在線學(xué)習(xí)和查閱:
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] 更多有關(guān)NIO編程的資料:
請(qǐng)進(jìn)入精華資料專輯:http://www.52im.net/forum.php?mod=collection&action=view&ctid=9
[4] 有關(guān)IM聊天應(yīng)用、消息推送技術(shù)的資料:
請(qǐng)進(jìn)入精華資料專輯:http://www.52im.net/forum.php?mod=collection&op=all
[5] 技術(shù)交流和學(xué)習(xí):
可直接進(jìn)入 即時(shí)通訊開(kāi)發(fā)者社區(qū) 討論和學(xué)習(xí)網(wǎng)絡(luò)編程、IM聊天應(yīng)用、消息推送應(yīng)用的開(kāi)發(fā)。
完整源碼工程下載
如需完整Eclipse源碼工程請(qǐng)聯(lián)系作者,或者進(jìn)入鏈接 http://www.52im.net/thread-373-1-1.html 自行下載。
完整源碼工程截圖如下:

(本文同步發(fā)布于:http://www.52im.net/thread-373-1-1.html)