申明:本文由做者基于平常实践整理,但愿对初次接触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)