申明:本文由做者基於平常實踐整理,但願對初次接觸MINA、Netty的人有所啓發。如需與做者交流,見文簽名,互相學習。 php
學習交流html
NIO框架的流行,使得開發大併發、高性能的互聯網服務端成爲可能。這其中最流行的無非就是MINA和Netty了,MINA目前的主要版本是MINA2、而Netty的主要版本是Netty3和Netty4(Netty5已經被取消開發了:詳見此文)。
本文將演示的是一個基於Netty4的UDP服務端和一個標準UDP客戶端(Java實現)雙向通訊的完整例子。實際上,Netty4的UDP例子很是難找(恕我愚鈍,找遍全網也沒有有價值的代碼,有也是Netty3,而Netty3和Netty4的風格差的不是一點點,參考意義不大),官方的代碼演示裏只有一個簡單的UDP廣播例子,不足以用於演示Netty4的UDP通訊最佳實踐。java
- 更多即時通信技術資料:http://www.52im.net/forum.php?mod=collection&op=all
git
- 移動端即時通信交流羣:215891622 推薦github
有關MINA和Netty的入門文章不少,但多數都是複製、粘貼的未經證明的來路不明內容,對於初次接觸的人來講,一個能夠運行且編碼規範的Demo,顯然要比各類「詳解」、「深刻分析」之類的要來的直接和有意義。本系列入門文章正是基於此種考慮而寫,雖無精深內容,但至少但願對初次接觸MINA、Netty的人有所啓發,起到拋磚引玉的做用。
本文是《NIO框架入門》系列文章中的第 4 篇,目錄以下:編程
[1] Netty4的UDP例子太難找:bootstrap
Netty4的完整雙向UDP通訊例子很難找(官方沒有),本文就是要用代碼來演示這個;api
[2] 本例中客戶端UDP實現無需第3方依賴:服務器
一般MINA或Netty的例子裏,客戶端也都是用的MINA或Netty的客戶端lib,本文將直接使用標準UDP代碼(便於跨平臺實現,好比iOS平臺),不依賴於第3方包;網絡
[3] 完整可執行源碼、方便學習:
完整的Demo源碼,適合新手直接運行,便於學習和研究。
社區裏常常在爭論到底該用MINA仍是Netty,好比這幾篇文章:《開源NIO框架八卦——究竟是先有MINA仍是先有Netty?》、《有關「爲什麼選擇Netty」的11個疑問及解答》、《選Netty仍是Mina:深刻研究與對比(一)》,哈哈這確實挺難抉擇的。不過,我的觀點是,對那個熟悉就用哪一個吧,沒什麼好糾結的,必竟兩者並比本質區別,也都是出自同一做者之手。
言歸正傳,本文要演示的Demo包含兩部分,Java UDP客戶端和Netty4 UDP服務端,客戶端將每隔3秒向服務端發送一條消息,而服務端在收到消息後立刻回覆一條消息給客戶端。
也就是說,服務端和客戶端都要實現消息的發送和接收,這也就實現了雙向通訊。若是有心的話,稍加改造,也就很容易實現一個簡陋的聊天程序了。下節將將給出真正的實現代碼。
Netty4的官方網站是:http://netty.io/,直接下最新的Netty 4.1吧,以下圖:
直接用這個all in one的jar包吧,反正用在服務端,2M大小的東西無所謂,方便管理,見下圖:
我的習慣用Eclipse,你用NetBeans或InteliJ也都沒問題,具體創建過程不熟練的衣自行百度吧,惟一注意的是把Netty4的lib包引用進來就好了,個人見下圖:
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 // 服務端監聽在9999端口 12 b.bind(9999).sync().channel().closeFuture().await(); 13 } 14 }
如上述代碼所示:不得不說,Netty4的UDP服務端代碼實現起來確實很簡單,一個 Bootstrap、一個 EventLoopGroup、外加一個SimpleChannelInboundHandler,就這麼被Netty4輕鬆搞定(準確地說Netty4是在Java NIO上的封裝而已,但最終API對開發者來講確實很友好)。
吐個槽:話說Netty4的代碼跟MINA相比,風格確實大不相同,雖然說二者有很深的源源,但通過Netty三、Netty4的進化,二者的差別(至少代碼看起來是這樣)仍是很明顯的。另外,b.bind(9999).sync().channel().closeFuture().await() 這一大串的連續方法調用,看起來很詭異,不當心調錯的話,服務器會不會爆炸?
1 public class EchoSeverHandler extends SimpleChannelInboundHandler<DatagramPacket> 2 { 3 @Override 4 protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) 5 throws Exception 6 { 7 // 讀取收到的數據 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】>>>>>> 收到客戶端的數據:"+body); 13 14 // 回覆一條信息給客戶端 15 ctx.writeAndFlush(new DatagramPacket( 16 Unpooled.copiedBuffer("Hello,我是Server,個人時間戳是"+System.currentTimeMillis() 17 , CharsetUtil.UTF_8) 18 , packet.sender())).sync(); 19 } 20 }
如上所示,這個Handler看起來比MINA的相似實現,要簡潔很多。
爲了讓客戶端代碼看起來更簡潔,我把Socket管理代碼提煉到 LocalUDPSocketProvider類、把UDP數據監聽和接收提煉到了 LocalUDPDataReciever類(實際上這兩個同名類是簡化自MobileIMSDK工程哦)。
public class EchoClient { public static void main(String[] args) throws Exception { // 初始化本地UDP的Socket LocalUDPSocketProvider.getInstance().initSocket(); // 啓動本地UDP監聽(接收數據用的) LocalUDPDataReciever.getInstance().startup(); // 循環發送數據給服務端 while(true) { // 要發送的數據 String toServer = "Hi,我是客戶端,個人時間戳"+System.currentTimeMillis(); byte[] soServerBytes = toServer.getBytes("UTF-8"); // 開始發送 boolean ok = UDPUtils.send(soServerBytes, soServerBytes.length); if(ok) Log.d("EchoClient", "發往服務端的信息已送出."); else Log.e("EchoClient", "發往服務端的信息沒有成功發出!!!"); // 3000秒後進入下一次循環 Thread.sleep(3000); } } }
補充說明:客戶端代碼沒有使用任何依賴,純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 }
客戶端運行結果:
服務端運行結果:
客戶端代碼沒什麼特別,就是標準的Java UDP代碼,看起來跟其它例子不同的緣由只是我把它提煉了一下,沒本質區別。一樣的代碼改改也能夠很好的用在Android端。實際上,上面的客戶端代碼就是從開源 MobileIMSDK 的Java端扒出來的,有興趣的也能夠看看它的Android端、Server端、iOS端,簡化一下能夠用做你自已的各類用途。
[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-367-1-1.html 自行下載。
完整源碼工程截圖以下:
(本文同步發佈於:http://www.52im.net/thread-367-1-1.html)