nio框架的流行,使得开发大并发、高性能的互联网服务端成为可能。这其中最流行的无非就是mina和netty了,mina目前的主要版本是、而netty的主要版本是和(netty5已经被取消开发了:)。
本文将演示的是一个基于mina2的udp服务端和一个标准udp客户端(java实现)双向通信的完整例子。
实际上,mina2的官方代码里有完整的udp通信demo代码,但demo里客户端是需要依赖mina2的客户端库的,而如果简单地去掉mina2的lib依赖,改为标准的java udp代码,则服务端和客户端的数据收发都会存在未知多余字节等。当然,本文已解决了这些问题,请继续往下阅读。
有关mina和netty的入门文章很多,但多数都是复制、粘贴的未经证实的来路不明内容,对于初次接触的人来说,一个可以运行且编码规范的demo,显然要比各种“详解”、“深入分析”之类的要来的直接和有意义。本系列入门文章正是基于此种考虑而写,虽无精深内容,但至少希望对初次接触mina、netty的人有所启发,起到抛砖引玉的作用。
本文是《nio框架入门》系列文章中的第2篇,目录如下:
- 《》(前篇)
- 《》(本文)
- 《nio框架入门(三):ios与mina2、netty4的跨平台udp双向通信实战》
- 《nio框架入门(四):android与mina2、netty4的跨平台udp双向通信实战》
- demo中的代码源自作者的开源工程,有实用价值:
源码均修改自作者的即时通讯开源工程 ,只是为了方便学习理解而作了简化,有一定的实用价值; - 本例中客户端udp实现无需第3方依赖:
通常mina或netty的例子里,客户端也都是用的mina或netty的客户端lib,本文将直接使用标准udp代码(便于跨平台实现,比如ios平台),不依赖于第3方包; - 完整可执行源码、方便学习:
完整的demo源码,适合新手直接运行,便于学习和研究。
言归正传,本文要演示的demo包含两部分,java udp客户端和mina2 udp服务端,客户端将每隔3秒向服务端发送一条消息,而服务端在收到消息后马上回复一条消息给客户端。
也就是说,服务端和客户端都要实现消息的发送和接收,这也就实现了双向通信。如果有心的话,稍加改造,也就很容易实现一个简陋的聊天程序了。下节将将给出真正的实现代码。
补充一句,本文中的客户端代码跟系列文章中的上篇《》,用的是相同的代码,如果您正在评估min2和netty4的udp服务端区别,可对照着上篇,进行详细对照和分析哦。
[1] 第一步:下载mina2
mina2的官方网站是:,最新版本是 mina 2.0.13,如下图:
补充说明:建议2进制和源码都下载下来,2进制制包中是编译好的jar包,源码包中包含了官方的demo源代码,需要的时候可以对照着看看,对于最佳实践来说,有一定的参考价值。
[2] 第二步:找到mina2的核心库文件
一般情况下,只需要mina-core-2.0.13.jar包就行了,见下图:
补充说明:因为mina2中使用了slf日框架,需要找到相应的lib才能最终运行,具体下一段再说明。
[3] 第三步:建好mina2的服务端java工程,准备开撸
个人习惯用eclipse,你如用netbeans或intelij也没问题,具体建立过程不熟练的请自行百度吧,唯一注意的是把mina2的lib包引用进来,我的见下图:
补充说明:mina2中需要的slf日志框架的jar包,主要额外找到并导进来,不然不能最终运行哦。你也可以下载本demo中的服务端源码,直接用就是了。
[1] 服务端主类 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设置
8 niodatagramacceptor acceptor = new niodatagramacceptor();
9 // 此行代码能让你的程序整体性能提升10倍
10 acceptor.getfilterchain()
11 .addlast("threadpool", new executorfilter(executors.newcachedthreadpool()));
12 // 设置mina2的iohandler实现类
13 acceptor.sethandler(new echoseverhandler());
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服务端请见:),实际上mina2的代码风格更符合一般程序员的编码习惯,更好懂一些,而netty4因历经多个大版本的进化,看起来非常简洁,但实现上并没有mina2看起来那么直观。当然,仅是个人理解,不当之外请见谅。
[2] 服务端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的异常回调方法。
8 *
9 * 本类中将在异常发生时,立即close当前会话。
10 *
11 * @param session 发生异常的会话
12 * @param cause 异常内容
13 * @see iosession#close(boolean)
14 */
15 @override
16 public void exceptioncaught(iosession session, throwable cause) throws exception
17 {
18 logger.error("[imcore]exceptioncaught捕获到错了,原因是:" cause.getmessage(), cause);
19 session.close(true);
20 }
21
22 /**
23 * mina框架中收到客户端消息的回调方法。
24 *
25 * 本类将在此方法中实现完整的即时通讯数据交互和处理策略。
26 *
27 * 为了提升并发性能,本方法将运行在独立于mina的ioprocessor之外的线程池中,
28 * 详见 {@link serverlauncher#initacceptor()}中的mina设置代码 。
29 *
30 * @param session 收到消息对应的会话引用
31 * @param message 收到的mina的原始消息封装对象,本类中是 {@link iobuffer}对象
32 * @throws exception 当有错误发生时将抛出异常
33 */
34 @override
35 public void messagereceived(iosession session, object message)throws exception
36 {
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类(实际上这两个同名类是简化自工程哦)。
[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 // 启动本地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平台,代码也几乎不用改就能用),部分代码修改自 (去掉了很多保证健壮性代码,现在看起来要简洁的多,便于初学者学习)。
[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本地监听端口(如果为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 }
[3] 数据接收类 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监听停止了(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 }
[1] 客户端运行结果:
[2] 服务端运行结果:
补充说明:服务端的运行结果中,控制台下每次多出的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端。实际上,上面的客户端代码就是从 的java端扒出来的,有兴趣的也可以看看它的、、,简化一下可以用作你自已的各种用途。
如果你阅读过本系列的上一篇《》,应该能明显地感觉的出来mina2的udp服务端api接口使用要是netty4的繁琐,而且mina2还存在独立客户端(非依赖于mina2客户端)实现时的多余字节和乱码问题。但个人认为mina2的代码风格更符合一般程序员的编码习惯,更好懂一些,而netty4因历经多个大版本的进化,虽起来非常简洁,但实现并不是那么直观。当然,至于mina还是netty,请客观一评估和使用,因为二者并无本质区别。
[1] mina和netty的源码在线学习和查阅:
mina-2.x地址是:
mina-1.x地址是:
netty-4.x地址是:
netty-3.x地址是:
[2] mina和netty的api文档在线查阅:
mina-2.x api文档(在线版):
mina-1.x api文档(在线版):
netty-4.x api文档(在线版):
netty-3.x api文档(在线版):
[3] 更多有关nio编程的资料:
请进入精华资料专辑:
[4] 有关im聊天应用、消息推送技术的资料:
请进入精华资料专辑:
[5] 技术交流和学习:
可直接进入 讨论和学习网络编程、im聊天应用、消息推送应用的开发。
如需完整eclipse源码工程请联系作者,或者进入链接 自行下载。
完整源码工程截图如下:
(本文同步发布于:)
作者: (点击作者姓名进入github)
出处:
交流:欢迎加入即时通讯开发交流群
讨论:
jack jiang同时是和的作者,可前往下载交流。
本博文
欢迎转载,转载请注明出处(也可前往 找到我)。