前言
NIO框架的流行,使得开发大并发、高性能的互联网服务端成为可能。这其中最流行的无非就是MINA和Netty了,MINA目前的主要版本是MINA2、而Netty的主要版本是Netty3和Netty4(Netty5已经被取消开发了:详见此文)。
本文将演示的是一个基于MINA2的UDP服务端和一个标准UDP客户端(Java实现)双向通信的完整例子。
实际上,MINA2的官方代码里有完整的UDP通信Demo代码,但Demo里客户端是需要依赖MINA2的客户端库的,而如果简单地去掉MINA2的lib依赖,改为标准的Java UDP代码,则服务端和客户端的数据收发都会存在未知多余字节等。当然,本文已解决了这些问题,请继续往下阅读。
学习交流
- 更多即时通讯技术资料:http://www.52im.net/forum.php?mod=collection&op=all
- 移动端即时通讯交流群:215891622 推荐
《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,如下图:
补充说明:建议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:
public class EchoServer
{
private static Logger logger = LoggerFactory.getLogger(EchoServer.class); public static void main(String[] args) throws Exception
{
// ** Acceptor设置
NioDatagramAcceptor acceptor = new NioDatagramAcceptor();
// 此行代码能让你的程序整体性能提升10倍
acceptor.getFilterChain()
.addLast("threadPool", new ExecutorFilter(Executors.newCachedThreadPool()));
// 设置MINA2的IoHandler实现类
acceptor.setHandler(new EchoSeverHandler());
// 设置会话超时时间(单位:毫秒),不设置则默认是10秒,请按需设置
acceptor.setSessionRecycler(new ExpiringSessionRecycler(15 * 1000)); // ** UDP通信配置
DatagramSessionConfig dcfg = acceptor.getSessionConfig();
dcfg.setReuseAddress(true);
// 设置输入缓冲区的大小,压力测试表明:调整到2048后性能反而降低
dcfg.setReceiveBufferSize(1024);
// 设置输出缓冲区的大小,压力测试表明:调整到2048后性能反而降低
dcfg.setSendBufferSize(1024); // ** UDP服务端开始侦听
acceptor.bind(new InetSocketAddress(9999)); logger.info("[IMCORE]UDP服务器正在端口 9999 上监听中...");
}
}
补充说明:MINA2的UDP服务端看起来比Netty4的要繁琐一些(有关Netty4的UDP服务端请见:http://www.52im.net/thread-367-1-1.html),实际上MINA2的代码风格更符合一般程序员的编码习惯,更好懂一些,而Netty4因历经多个大版本的进化,看起来非常简洁,但实现上并没有MINA2看起来那么直观。当然,仅是个人理解,不当之外请见谅。
[2] 服务端Handler类 EchoSeverHandler.java:
public class EchoSeverHandler extends IoHandlerAdapter
{
private static Logger logger = LoggerFactory.getLogger(EchoSeverHandler.class);
public static final CharsetDecoder decoder = (Charset.forName("UTF-8")).newDecoder(); /**
* MINA的异常回调方法。
* <p>
* 本类中将在异常发生时,立即close当前会话。
*
* @param session 发生异常的会话
* @param cause 异常内容
* @see IoSession#close(boolean)
*/
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
logger.error("[IMCORE]exceptionCaught捕获到错了,原因是:"+cause.getMessage(), cause);
session.close(true);
} /**
* MINA框架中收到客户端消息的回调方法。
* <p>
* 本类将在此方法中实现完整的即时通讯数据交互和处理策略。
* <p>
* 为了提升并发性能,本方法将运行在独立于MINA的IoProcessor之外的线程池中,
* 详见 {@link ServerLauncher#initAcceptor()}中的MINA设置代码 。
*
* @param session 收到消息对应的会话引用
* @param message 收到的MINA的原始消息封装对象,本类中是 {@link IoBuffer}对象
* @throws Exception 当有错误发生时将抛出异常
*/
@Override
public void messageReceived(IoSession session, Object message)throws Exception
{
//*********************************************** 接收数据
// 读取收到的数据
IoBuffer buffer = (IoBuffer) message;
String body = buffer.getString(decoder);
// 注意:当客户使用不依赖于MINA库的情况下,以下官方推
// 荐的读取方法会在数据首部出现几个字节的未知乱码
// message.toString()
logger.debug("【NOTE】>>>>>> 收到客户端的数据:"+body); //*********************************************** 回复数据
String strToClient = "Hello,我是Server,我的时间戳是"+System.currentTimeMillis();
byte[] res = strToClient.getBytes("UTF-8");
// 组织IoBuffer数据包的方法:本方法才可以正确地让客户端UDP收到byte数组
IoBuffer buf = IoBuffer.wrap(res); // 向客户端写数据
WriteFuture future = session.write(buf);
// 在100毫秒超时间内等待写完成
future.awaitUninterruptibly(100);
// The message has been written successfully
if( future.isWritten() )
{
// send sucess!
}
// The messsage couldn't be written out completely for some reason.
// (e.g. Connection is closed)
else
{
logger.warn("[IMCORE]回复给客户端的数据发送失败!");
}
}
}
[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:
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(去掉了很多保证健壮性代码,现在看起来要简洁的多,便于初学者学习)。
[2] Socket操作类 LocalUDPSocketProvider.java:
public class LocalUDPSocketProvider
{
private static final String TAG = LocalUDPSocketProvider.class.getSimpleName();
private static LocalUDPSocketProvider instance = null;
private DatagramSocket localUDPSocket = null; public static LocalUDPSocketProvider getInstance()
{
if (instance == null)
instance = new LocalUDPSocketProvider();
return instance;
} public void initSocket()
{
try
{
// UDP本地监听端口(如果为0将表示由系统分配,否则使用指定端口)
this.localUDPSocket = new DatagramSocket(ConfigEntity.localUDPPort);
// 调用connect之后,每次send时DatagramPacket就不需要设计目标主机的ip和port了
// * 注意:connect方法一定要在DatagramSocket.receive()方法之前调用,
// * 不然整send数据将会被错误地阻塞。这或许是官方API的bug,也或许是调
// * 用规范就应该这样,但没有找到官方明确的说明
this.localUDPSocket.connect(
InetAddress.getByName(ConfigEntity.serverIP), ConfigEntity.serverUDPPort);
this.localUDPSocket.setReuseAddress(true);
Log.d(TAG, "new DatagramSocket()已成功完成.");
}
catch (Exception e)
{
Log.w(TAG, "localUDPSocket创建时出错,原因是:" + e.getMessage(), e);
}
} public DatagramSocket getLocalUDPSocket()
{
return this.localUDPSocket;
}
}
[3] 数据接收类 LocalUDPDataReciever.java:
public class LocalUDPDataReciever
{
private static final String TAG = LocalUDPDataReciever.class.getSimpleName();
private static LocalUDPDataReciever instance = null;
private Thread thread = null; public static LocalUDPDataReciever getInstance()
{
if (instance == null)
instance = new LocalUDPDataReciever();
return instance;
} public void startup()
{
this.thread = new Thread(new Runnable()
{
public void run()
{
try
{
Log.d(LocalUDPDataReciever.TAG, "本地UDP端口侦听中,端口=" + ConfigEntity.localUDPPort + "..."); //开始侦听
LocalUDPDataReciever.this.udpListeningImpl();
}
catch (Exception eee)
{
Log.w(LocalUDPDataReciever.TAG, "本地UDP监听停止了(socket被关闭了?)," + eee.getMessage(), eee);
}
}
});
this.thread.start();
} private void udpListeningImpl() throws Exception
{
while (true)
{
byte[] data = new byte[1024];
// 接收数据报的包
DatagramPacket packet = new DatagramPacket(data, data.length); DatagramSocket localUDPSocket = LocalUDPSocketProvider.getInstance().getLocalUDPSocket();
if ((localUDPSocket == null) || (localUDPSocket.isClosed()))
continue; // 阻塞直到收到数据
localUDPSocket.receive(packet); // 解析服务端发过来的数据
String pFromServer = new String(packet.getData(), 0 , packet.getLength(), "UTF-8");
Log.w(LocalUDPDataReciever.TAG, "【NOTE】>>>>>> 收到服务端的消息:"+pFromServer);
}
}
}
Demo执行效果
[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端。实际上,上面的客户端代码就是从开源 MobileIMSDK 的Java端扒出来的,有兴趣的也可以看看它的Android端、Server端、iOS端,简化一下可以用作你自已的各种用途。
如果你阅读过本系列的上一篇《NIO框架入门(一):服务端基于Netty4的UDP双向通信Demo演示》,应该能明显地感觉的出来MINA2的UDP服务端API接口使用要是Netty4的繁琐,而且MINA2还存在独立客户端(非依赖于MINA2客户端)实现时的多余字节和乱码问题。但个人认为MINA2的代码风格更符合一般程序员的编码习惯,更好懂一些,而Netty4因历经多个大版本的进化,虽起来非常简洁,但实现并不是那么直观。当然,至于MINA还是Netty,请客观一评估和使用,因为二者并无本质区别。
更多学习资源
[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-373-1-1.html 自行下载。
完整源码工程截图如下:
(本文同步发布于:http://www.52im.net/thread-373-1-1.html)
【原创】NIO框架入门(二):服务端基于MINA2的UDP双向通信Demo演示的更多相关文章
-
【原创】NIO框架入门(一):服务端基于Netty4的UDP双向通信Demo演示
申明:本文由作者基于日常实践整理,希望对初次接触MINA.Netty的人有所启发.如需与作者交流,见文签名,互相学习. 学习交流 更多学习资料:点此进入 推荐 移动端即时通讯交流: 215891622 ...
-
【原创】NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战
概述 本文演示的是一个Android客户端程序,通过UDP协议与两个典型的NIO框架服务端,实现跨平台双向通信的完整Demo. 当前由于NIO框架的流行,使得开发大并发.高性能的互联网服务端成为可能. ...
-
【原创】NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通信实战
前言 本文将演示一个iOS客户端程序,通过UDP协议与两个典型的NIO框架服务端,实现跨平台双向通信的完整Demo.服务端将分别用MINA2和Netty4进行实现,而通信时服务端你只需选其一就行了.同 ...
-
部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用
前面介绍了很多关于ABP框架的后台Web API 服务端,以及基于Vue+Element前端应用,本篇针对两者的联合部署,以及对部署中遇到的问题进行处理.ABP框架的后端是基于.net core5.0 ...
-
gRPC (1):入门及服务端创建和调用原理
1. RPC 入门 1.1 RPC 框架原理 RPC 框架的目标就是让远程服务调用更加简单.透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP).序列化方式(XML/Json/ 二进制)和 ...
-
day112:MoFang:种植园使用websocket代替http&;服务端基于flask-socketio提供服务&;服务端响应信息&;种植园页面显示初始化
目录 1.种植园使用websocket代替http 2.服务端基于socket提供服务 3.服务端响应信息 4.种植园页面展示 1.种植园使用websocket代替http 我们需要完成的种植园,是一 ...
-
前端学习 node 快速入门 系列 —— 服务端渲染
其他章节请看: 前端学习 node 快速入门 系列 服务端渲染 在简易版 Apache一文中,我们用 node 做了一个简单的服务器,能提供静态资源访问的能力. 对于真正的网站,页面中的数据应该来自服 ...
-
基于vue的nuxt框架cnode社区服务端渲染
nuxt-cnode 基于vue的nuxt框架仿的cnode社区服务端渲染,主要是为了seo优化以及首屏加载速度 线上地址 http://nuxt-cnode.foreversnsd.cngithub ...
-
基于netty实现rpc框架-spring boot服务端
demo地址 https://gitee.com/syher/grave-netty RPC介绍 首先了解一下RPC:远程过程调用.简单点说就是本地应用可以调用远程服务器的接口.那么通过什么方式调用远 ...
随机推荐
-
React + Redux 入坑指南
Redux 原理 1. 单一数据源 all states ==>Store 随着组件的复杂度上升(包括交互逻辑和业务逻辑),数据来源逐渐混乱,导致组件内部数据调用十分复杂,会产生数据冗余或者混用 ...
-
不一样的角度 解读微信小程序
不一样的角度 解读微信小程序 七月在夏天· 2 天前 前段时间看完了雨果奖中短篇获奖小说<北京折叠>.很有意思的是,张小龙最近也要把应用折叠到微信里,这些应用被他称为:小程序. 含着金钥匙 ...
-
【poj2186】 Popular Cows
http://poj.org/problem?id=2186 (题目链接) 题意 给出一个n个点m条边的有向图,求其中没有出度强连通分量所包含的点有几个 Solution 其实这道题的题解已经在“题意 ...
-
常用linux小工具介绍
1.ctags(Generate tag files for source code)是vim下方便代码阅读的工具.尽管ctags也可以支持其它编辑器,但是它正式支持的只有VIM. ctags 最先是 ...
-
微信小程序测试方法总结
最近的新项目是小程序加web端后台管理 主要找了些文章方便自己使用也分享给大家: 小程序官方文档 https://developers.weixin.qq.com/miniprogram/design ...
-
Mongo安装与使用
MongoDB[1] 是一个基于分布式文件存储的数据库.由C++语言编写.旨在为WEB应用提供可扩展的高性能数据存储解决方案. mongoDB MongoDB[2] 是一个介于关系数据库和非关系数 ...
-
Python入门-编写抓取网站图片的爬虫-正则表达式
//生命太短 我用Python! //Python真是让一直用c++的村里孩子长知识了! 这个仅仅是一个测试,成功抓取了某网站1000多张图片. 下一步要做一个大新闻 大工程 #config = ut ...
-
0 ardunio processing 显示三轴数据
http://blog.csdn.net/Fortware/article/details/38307799 /** * Mixture * by Simon Greenwold. * * Displ ...
-
POJ 1860&;&;3259&;&;1062&;&;2253&;&;1125&;&;2240
六道烦人的最短路(然而都是水题) 我也不准备翻译题目了(笑) 一些是最短路的变形(比如最长路,最短路中的最长边,判环),还有一些就是裸题了. 1860:找环,只需要把SPFA的松弛条件改一下即可,这里 ...
-
nodejs文件上传报错总结
语法: fs.rename(oldPath,newPath,callback) 今天在使用formidable模块做图片上传处理的时候,fs.rename方法的报了一个这样的错:cross-devic ...