申明:本文由作者基于日常實(shí)踐整理,希望對(duì)初次接觸MINA、Netty的人有所啟發(fā)。如需與作者交流,見文簽名,互相學(xué)習(xí)。
學(xué)習(xí)交流
前言
NIO框架的流行,使得開發(fā)大并發(fā)、高性能的互聯(lián)網(wǎng)服務(wù)端成為可能。這其中最流行的無非就是MINA和Netty了,MINA目前的主要版本是MINA2、而Netty的主要版本是Netty3和Netty4(Netty5已經(jīng)被取消開發(fā)了:詳見此文)。
本文將演示的是一個(gè)基于Netty4的UDP服務(wù)端和一個(gè)標(biāo)準(zhǔn)UDP客戶端(Java實(shí)現(xiàn))雙向通信的完整例子。實(shí)際上,Netty4的UDP例子非常難找(恕我愚鈍,找遍全網(wǎng)也沒有有價(jià)值的代碼,有也是Netty3,而Netty3和Netty4的風(fēng)格差的不是一點(diǎn)點(diǎn),參考意義不大),官方的代碼演示里只有一個(gè)簡單的UDP廣播例子,不足以用于演示Netty4的UDP通信最佳實(shí)踐。
本文亮點(diǎn)
[1] Netty4的UDP例子太難找:
Netty4的完整雙向UDP通信例子很難找(官方?jīng)]有),本文就是要用代碼來演示這個(gè);
[2] 本例中客戶端UDP實(shí)現(xiàn)無需第3方依賴:
通常MINA或Netty的例子里,客戶端也都是用的MINA或Netty的客戶端lib,本文將直接使用標(biāo)準(zhǔn)UDP代碼(便于跨平臺(tái)實(shí)現(xiàn),比如iOS平臺(tái)),不依賴于第3方包;
[3] 完整可執(zhí)行源碼、方便學(xué)習(xí):
完整的Demo源碼,適合新手直接運(yùn)行,便于學(xué)習(xí)和研究。
題外話
社區(qū)里經(jīng)常在爭論到底該用MINA還是Netty,比如這幾篇文章:《開源NIO框架八卦——到底是先有MINA還是先有Netty?》、《有關(guān)“為何選擇Netty”的11個(gè)疑問及解答》、《選Netty還是Mina:深入研究與對(duì)比(一)》,哈哈這確實(shí)挺難抉擇的。不過,個(gè)人觀點(diǎn)是,對(duì)那個(gè)熟悉就用哪個(gè)吧,沒什么好糾結(jié)的,必竟二者并比本質(zhì)區(qū)別,也都是出自同一作者之手。
寫個(gè)什么樣的Demo?
言歸正傳,本文要演示的Demo包含兩部分,Java UDP客戶端和Netty4 UDP服務(wù)端,客戶端將每隔3秒向服務(wù)端發(fā)送一條消息,而服務(wù)端在收到消息后馬上回復(fù)一條消息給客戶端。
也就是說,服務(wù)端和客戶端都要實(shí)現(xiàn)消息的發(fā)送和接收,這也就實(shí)現(xiàn)了雙向通信。如果有心的話,稍加改造,也就很容易實(shí)現(xiàn)一個(gè)簡陋的聊天程序了。下節(jié)將將給出真正的實(shí)現(xiàn)代碼。
Netty4 服務(wù)端準(zhǔn)備工作
[1] 第一步:下載Netty4
Netty4的官方網(wǎng)站是:http://netty.io/,直接下最新的Netty 4.1吧,如下圖:

[2] 第二步:找到Netty4的核心庫文件
直接用這個(gè)all in one的jar包吧,反正用在服務(wù)端,2M大小的東西無所謂,方便管理,見下圖:

[3] 第三步:建好Java工程準(zhǔn)備準(zhǔn)備開擼
個(gè)人習(xí)慣用Eclipse,你用NetBeans或InteliJ也都沒問題,具體建立過程不熟練的衣自行百度吧,唯一注意的是把Netty4的lib包引用進(jìn)來就行了,我的見下圖:

服務(wù)端代碼
[1] 服務(wù)端主類 EchoServer.java
1 public class EchoServer
2 {
3 public static void main(String[] args) throws InterruptedException
4 {
5 Bootstrap b = new Bootstrap();
6 EventLoopGroup group = new NioEventLoopGroup();
7 b.group(group)
8 .channel(NioDatagramChannel.class)
9 .handler(new EchoSeverHandler());
10
11 // 服務(wù)端監(jiān)聽在9999端口
12 b.bind(9999).sync().channel().closeFuture().await();
13 }
14 }
如上述代碼所示:不得不說,Netty4的UDP服務(wù)端代碼實(shí)現(xiàn)起來確實(shí)很簡單,一個(gè) Bootstrap、一個(gè) EventLoopGroup、外加一個(gè)SimpleChannelInboundHandler,就這么被Netty4輕松搞定(準(zhǔn)確地說Netty4是在Java NIO上的封裝而已,但最終API對(duì)開發(fā)者來說確實(shí)很友好)。
吐個(gè)槽:話說Netty4的代碼跟MINA相比,風(fēng)格確實(shí)大不相同,雖說兩者有很深的源源,但經(jīng)過Netty3、Netty4的進(jìn)化,兩者的差異(至少代碼看起來是這樣)還是很明顯的。另外,b.bind(9999).sync().channel().closeFuture().await() 這一大串的連續(xù)方法調(diào)用,看起來很詭異,不小心調(diào)錯(cuò)的話,服務(wù)器會(huì)不會(huì)爆炸?
[2] 服務(wù)端Handler類 EchoSeverHandler.java
1 public class EchoSeverHandler extends SimpleChannelInboundHandler<DatagramPacket>
2 {
3 @Override
4 protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet)
5 throws Exception
6 {
7 // 讀取收到的數(shù)據(jù)
8 ByteBuf buf = (ByteBuf) packet.copy().content();
9 byte[] req = new byte[buf.readableBytes()];
10 buf.readBytes(req);
11 String body = new String(req, CharsetUtil.UTF_8);
12 System.out.println("【NOTE】>>>>>> 收到客戶端的數(shù)據(jù):"+body);
13
14 // 回復(fù)一條信息給客戶端
15 ctx.writeAndFlush(new DatagramPacket(
16 Unpooled.copiedBuffer("Hello,我是Server,我的時(shí)間戳是"+System.currentTimeMillis()
17 , CharsetUtil.UTF_8)
18 , packet.sender())).sync();
19 }
20 }
如上所示,這個(gè)Handler看起來比MINA的類似實(shí)現(xiàn),要簡潔不少。
客戶端代碼
為了讓客戶端代碼看起來更簡潔,我把Socket管理代碼提煉到 LocalUDPSocketProvider類、把UDP數(shù)據(jù)監(jiān)聽和接收提煉到了 LocalUDPDataReciever類(實(shí)際上這兩個(gè)同名類是簡化自MobileIMSDK工程哦)。
[1] 客戶端主類 EchoClient.java
public class EchoClient
{
public static void main(String[] args) throws Exception
{
// 初始化本地UDP的Socket
LocalUDPSocketProvider.getInstance().initSocket();
// 啟動(dòng)本地UDP監(jiān)聽(接收數(shù)據(jù)用的)
LocalUDPDataReciever.getInstance().startup();
// 循環(huán)發(fā)送數(shù)據(jù)給服務(wù)端
while(true)
{
// 要發(fā)送的數(shù)據(jù)
String toServer = "Hi,我是客戶端,我的時(shí)間戳"+System.currentTimeMillis();
byte[] soServerBytes = toServer.getBytes("UTF-8");
// 開始發(fā)送
boolean ok = UDPUtils.send(soServerBytes, soServerBytes.length);
if(ok)
Log.d("EchoClient", "發(fā)往服務(wù)端的信息已送出.");
else
Log.e("EchoClient", "發(fā)往服務(wù)端的信息沒有成功發(fā)出!!!");
// 3000秒后進(jìn)入下一次循環(huán)
Thread.sleep(3000);
}
}
}
補(bǔ)充說明:客戶端代碼沒有使用任何依賴,純Java UDP代碼實(shí)現(xiàn)(如果是Andriod平臺(tái),代碼也幾乎不用改就能用),部分代碼修改自 開源即時(shí)通訊框架 MobileIMSDK(去掉了很多保證健壯性代碼,現(xià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)聽端口(如果為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)該這樣,但沒有找到官方明確的說明
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端口偵聽中,端口=" + ConfigEntity.localUDPPort + "...");
23
24 //開始偵聽
25 LocalUDPDataReciever.this.udpListeningImpl();
26 }
27 catch (Exception eee)
28 {
29 Log.w(LocalUDPDataReciever.TAG, "本地UDP監(jiān)聽停止了(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ā)過來的數(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 }
運(yùn)行效果
客戶端運(yùn)行結(jié)果:

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

補(bǔ)充說明
客戶端代碼沒什么特別,就是標(biāo)準(zhǔn)的Java UDP代碼,看起來跟其它例子不一樣的原因只是我把它提煉了一下,沒本質(zhì)區(qū)別。同樣的代碼改改也可以很好的用在Android端。實(shí)際上,上面的客戶端代碼就是從開源 MobileIMSDK 的Java端扒出來的,有興趣的也可以看看它的Android端、Server端、iOS端,簡化一下可以用作你自已的各種用途。
更多學(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í)通訊開發(fā)者社區(qū) 討論和學(xué)習(xí)網(wǎng)絡(luò)編程、IM聊天應(yīng)用、消息推送應(yīng)用的開發(fā)。
完整源碼工程下載
不知如何上傳附件,如需完整Eclipse源碼工程請(qǐng)聯(lián)系作者,或者進(jìn)入鏈接 http://www.52im.net/thread-367-1-1.html 自行下載。
完整源碼工程截圖如下:

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