java mina udp_【原创】NIO框架入门(二):服务端基于MINA2的UDP双向通信Demo演示

前言

NIO框架的流行,使得开发大并发、高性能的互联网服务端成为可能。这其中最流行的无非就是MINA和Netty了,MINA目前的主要版本是MINA2、而Netty的主要版本是Netty3和Netty4(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,如下图:

df42b7b3199c48d78c42a2173ae90ba8.png

补充说明:建议2进制和源码都下载下来,2进制制包中是编译好的jar包,源码包中包含了官方的Demo源代码,需要的时候可以对照着看看,对于最佳实践来说,有一定的参考价值。

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

一般情况下,只需要mina-core-2.0.13.jar包就行了,见下图:

479130e473bf7a753b0121e822952b88.png

补充说明:因为MINA2中使用了slf日框架,需要找到相应的lib才能最终运行,具体下一段再说明。

[3] 第三步:建好MINA2的服务端Java工程,准备开撸

个人习惯用Eclipse,你如用NetBeans或InteliJ也没问题,具体建立过程不熟练的请自行百度吧,唯一注意的是把MINA2的lib包引用进来,我的见下图:

d66792dfc6845db54b5f75ec933cf691.png

补充说明:MINA2中需要的slf日志框架的jar包,主要额外找到并导进来,不然不能最终运行哦。你也可以下载本Demo中的服务端源码,直接用就是了。

服务端代码实现

[1] 服务端主类 EchoServer.java:

1 public classEchoServer2 {3 private static Logger logger = LoggerFactory.getLogger(EchoServer.class);4

5 public static void main(String[] args) throwsException6 {7 //** Acceptor设置

8 NioDatagramAcceptor acceptor = newNioDatagramAcceptor();9 //此行代码能让你的程序整体性能提升10倍

10 acceptor.getFilterChain()11 .addLast("threadPool", newExecutorFilter(Executors.newCachedThreadPool()));12 //设置MINA2的IoHandler实现类

13 acceptor.setHandler(newEchoSeverHandler());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 extendsIoHandlerAdapter2 {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 *

9 * 本类中将在异常发生时,立即close当前会话。10 *11 *@paramsession 发生异常的会话12 *@paramcause 异常内容13 *@seeIoSession#close(boolean)14 */

15 @Override16 public void exceptionCaught(IoSession session, Throwable cause) throwsException17 {18 logger.error("[IMCORE]exceptionCaught捕获到错了,原因是:"+cause.getMessage(), cause);19 session.close(true);20 }21

22 /**

23 * MINA框架中收到客户端消息的回调方法。24 *

25 * 本类将在此方法中实现完整的即时通讯数据交互和处理策略。26 *

27 * 为了提升并发性能,本方法将运行在独立于MINA的IoProcessor之外的线程池中,28 * 详见 {@linkServerLauncher#initAcceptor()}中的MINA设置代码 。29 *30 *@paramsession 收到消息对应的会话引用31 *@parammessage 收到的MINA的原始消息封装对象,本类中是 {@linkIoBuffer}对象32 *@throwsException 当有错误发生时将抛出异常33 */

34 @Override35 public void messageReceived(IoSession session, Object message)throwsException36 {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 classEchoClient2 {3 public static void main(String[] args) throwsException4 {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 classLocalUDPSocketProvider2 {3 private static final String TAG = LocalUDPSocketProvider.class.getSimpleName();4 private static LocalUDPSocketProvider instance = null;5 private DatagramSocket localUDPSocket = null;6

7 public staticLocalUDPSocketProvider getInstance()8 {9 if (instance == null)10 instance = newLocalUDPSocketProvider();11 returninstance;12 }13

14 public voidinitSocket()15 {16 try

17 {18 //UDP本地监听端口(如果为0将表示由系统分配,否则使用指定端口)

19 this.localUDPSocket = newDatagramSocket(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 publicDatagramSocket getLocalUDPSocket()36 {37 return this.localUDPSocket;38 }39 }

[3] 数据接收类 LocalUDPDataReciever.java:

1 public classLocalUDPDataReciever2 {3 private static final String TAG = LocalUDPDataReciever.class.getSimpleName();4 private static LocalUDPDataReciever instance = null;5 private Thread thread = null;6

7 public staticLocalUDPDataReciever getInstance()8 {9 if (instance == null)10 instance = newLocalUDPDataReciever();11 returninstance;12 }13

14 public voidstartup()15 {16 this.thread = new Thread(newRunnable()17 {18 public voidrun()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() throwsException37 {38 while (true)39 {40 byte[] data = new byte[1024];41 //接收数据报的包

42 DatagramPacket packet = newDatagramPacket(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] 客户端运行结果:

721a54c0b0a4010a1fe76016350aa32e.png

[2] 服务端运行结果:

a9e5f8a70dd730af28982a92be65c692.png

补充说明:服务端的运行结果中,控制台下每次多出的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,请客观一评估和使用,因为二者并无本质区别。

更多学习资源

完整源码工程下载

博客园上不知如何上传附件,如需完整Eclipse源码工程请联系作者,或者进入链接 http://www.52im.net/thread-373-1-1.html 自行下载。

完整源码工程截图如下:

3c90ba7063455f51717c7c04b257b56c.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值