Apache MINA 初识
Apache MINA(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个为开发高性能和高可用性的网络应用程序提供了非常便利的框架。
当前发行的 MINA 版本支持基于 Java NIO 技术的 TCP/UDP 应用程序开发、串口通讯程序(只在最新的预览版中提供),MINA 所支持的功能也在进一步的扩展中。
跟MINA类似的框架,迓有著名的Jboss Netty,代码框架非常类似,使用方法也大同小异,可以说Jboss Netty是MINA的改良版
环境准备
1.首先到官方网站下载最新的 MINA 版本,地址是:http://www.apache.org/dyn/closer.cgi/mina/mina/2.0.7/dist/apache-mina-2.0.7-bin.zip
2.下载 MINA 的依赖包 slf4j。下载地址 http://www.slf4j.org/download.html
MINA 使用此项目作为日志信息的输出,而MINA 本身并不附带此项目包,slf4j 项目解压后有很多的文件,本例中只需要其中的slf4j-api-1.7.2.jar 和 slf4j-simple-1.7.2.jar 这两个 jar 文件。如果没有这两个文件就会导致启动例子程序的时候报 org/slf4j/LoggerFactory 类没找到的错误。
MINA 基本类的描述
在介绍架构之前先认识几个接口:
IoAccepter 相当于网络应用程序中的服务器端
IoConnector 相当于客户端
IoSession 当前客户端到服务器端的一个连接实例
IoHandler 业务处理逻辑
IoFilter 过滤器用于悬接通讯层接口与业务层接口
MINA 的架构图
在图中的模块链中,IoService 便是应用程序的入口,IoAccepter 是 IoService 的一个扩展接口。IoService 接口可以用来添加多个 IoFilter,这些 IoFilter 符合责任链模式并由 IoProcessor 线程负责调用。而 IoAccepter 在 ioService 接口的基础上还提供绑定某个通讯端口以及取消绑定的接口。
当客户首次访问采用MINA编写的程序时,IoAcceptor作为线程运行,负责接受来自客户的请求。当有客户请求连接时,创建一个 Session,该Session与IoProcessor、SocketChannel以及IOService联系起来。IoProcessor也作为另外一个线程运行,定时检查客户是否有数据到来,并对客户请求进行处理,依次调用在IOService注册的各个IoFilter,最后调用 IoHandler进行最终的逻辑处理,再将处理后的结果Filter后返回给客户端。
一个 IoHandler 接口中具有如下一些方法
void exceptionCaught(IoSession session, Throwable cause)
当接口中其他方法抛出异常未被捕获时触发此方法
void messageReceived(IoSession session, Object message)
当接收到客户端的请求信息后触发此方法.
void messageSent(IoSession session, Object message)
当信息已经传送给客户端后触发此方法.
void sessionClosed(IoSession session)
当连接被关闭时触发,例如客户端程序意外退出等等.
void sessionCreated(IoSession session)
当一个新客户端连接后触发此方法.
void sessionIdle(IoSession session, IdleStatus status)
当连接空闲时触发此方法.
void sessionOpened(IoSession session)
当连接后打开时触发此方法,一般此方法与 sessionCreated 会被同时触发
前面我们提到 IoService 是负责底层通讯接入,而 IoHandler 是负责业务处理的。那么 MINA 架构图中的 IoFilter 作何用途呢?答案是你想作何用途都可以。但是有一个用途却是必须的,那就是作为 IoService 和 IoHandler 之间的桥梁。IoHandler 接口中最重要的一个方法是 messageReceived,这个方法的第二个参数是一个 Object 型的消息,总所周知,Object 是所有 Java 对象的基础,那到底谁来决定这个消息到底是什么类型呢?答案也就在这个 IoFilter 中。在前面使用的例子中,我们添加了一个 IoFilter 是 new ProtocolCodecFilter(new TextLineCodecFactory()),这个过滤器的作用是将来自客户端输入的信息转换成一行行的文本后传递给 IoHandler,因此我们可以在 messageReceived 中直接将 msg 对象强制转换成 String 对象。
而如果我们不提供任何过滤器的话,那么在 messageReceived 方法中的第二个参数类型就是一个 byte 的缓冲区,对应的类是 org.apache.mina.common.ByteBuffer。虽然你也可以将解析客户端信息放在 IoHandler 中来做,但这并不是推荐的做法,使原来清晰的模型又模糊起来,变得 IoHandler 不只是业务处理,还得充当协议解析的任务。
Mina中自带的一些主要过滤器说明
BlacklistFilter 设置一些IP 地址为黑名单,不允许访问。
BufferedWriteFilter 设置输出时像BufferedOutputStream 一样进行缓冲。
CompressionFilter 设置在输入、输出流时启用JZlib 压缩。
ConnectionThrottleFilter 这个过滤器指定同一个IP 地址(不含端口号)上的请求在多长的毫秒值内可以有一个请求,如果小于指定的时间间隔就有连续两个请求,那么第二个请求将被忽略(IoSession.close())。正如Throttle 的名字一样,调节访问的频率。这个过滤器最好放在过滤器链的前面。
FileRegionWriteFilter 如果你想使用File 对象进行输出,请使用这个过滤器。要注意,你需要使用WriteFuture 或者在messageSent() 方法中关闭File 所关联的FileChannel 通道。
StreamWriteFilter 如果你想使用InputStream 对象进行输出,请使用这个过滤器。要注意,你需要使用WriteFuture 或者在messageSent()方法中关闭File 所关联的FileChannel 通道。
NoopFilter 这个过滤器什么也不做,如果你想测试过滤器链是否起作用,可以用它来测试。
ProfilerTimerFilter 这个过滤器用于检测每个事件方法执行的时间, 所以最好放在过滤器链的前面。
ProxyFilter 这个过滤器在客户端使用ProxyConnector 作为实现时,会自动加入到过滤器链中,用于完成代理功能。
SessionAttributeInitializingFilter 这个过滤器在IoSession 中放入一些属性(Map),通常放在过滤器的前面,用于放置一些初始化的信息。
MdcInjectionFilter 针对日志输出做MDC 操作,可以参考LOG4J 的MDC、NDC 的文档。
WriteRequestFilter CompressionFilter、RequestResponseFilter 的基类,用于包装写请求的过滤器。
还有一些过滤器,这里没有列出,譬如:前面的LoggingFilger 日志过滤器。
实例代码模拟
服务端演示
服务端业务处理
package org.dennisit.mina.demo; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; /** * * HelloHandler 负责业务逻辑处理 * * 功 能: TODO * 类 名: HelloHandler.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-2-21 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * */ public class ServerHandler extends IoHandlerAdapter{ /** * 有新连接时触发 */ @Override public void sessionOpened(IoSession session) throws Exception { System.out.println("服务端, session open for " + session.getRemoteAddress()); } /** * 连接关闭时触发 */ @Override public void sessionClosed(IoSession session) throws Exception { System.out.println("服务端, session closed from " + session.getRemoteAddress()); } /** * 收到来自客户端的消息 */ @Override public void messageReceived(IoSession session, Object message) throws Exception { String clientIP = session.getRemoteAddress().toString(); System.out.println("服务端接收到来自IP:"+clientIP+"的消息:"+message ); } /** * 当有异常发生时触发 */ @Override public void exceptionCaught(IoSession session, Throwable cause) throws Exception { System.out.println("服务端,发生异常" + cause.getMessage()); session.close(); } }
服务端主类
package org.dennisit.mina.demo; import java.io.IOException; import java.net.InetSocketAddress; import org.apache.mina.core.service.IoAcceptor; import org.apache.mina.core.session.IdleStatus; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.textline.TextLineCodecFactory; import org.apache.mina.filter.logging.LoggingFilter; import org.apache.mina.transport.socket.nio.NioSocketAcceptor; /** * * MinaServer * * 功 能: TODO * 类 名: MinaServer.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-2-21 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * */ public class MinaServer { private static final int PORT = 8888; public static void main(String[] args) throws IOException { // 构造接收器 IoAcceptor acceptor = new NioSocketAcceptor(); ServerHandler handler = new ServerHandler(); acceptor.setHandler(handler); // 读写通道10秒内无操作进入空闲状态 acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10); //添加过滤器和日志组件 acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory())); acceptor.getFilterChain().addLast("logging", new LoggingFilter()); // 启动服务 acceptor.bind(new InetSocketAddress(PORT)); System.out.println("MinaServer started on port " + PORT); } }
main方法中定义了整型的端口8888,在实际应用中一般通过antx.properties配置端口,返样就可以把配置的权限交给运维部门。
每一个信息都会通过在IoAcceptor中定义的过滤器链的所有过滤器,完成个性化的日志记录和解码工作。日志过滤器用SL4J库记录信息,而编码过滤器则解码所有收到的信息.
启动后提示:MinaServer started on port 8888表示启动成功,如果启动失败,问题无外乎是类没找到或者端口占用。如果端口被占用的话,换一个,修改 PORT 常量值后再次编译并启动。
测试服务器
打开命令行窗口,输入 telnet localhost 8080 后,发送信息后回车再去看看刚启动的服务程序有何反应。我的反应如下:
表示mina开发的网络程序服务端经成功运行 .
win7下遇到telnet不是内部或外部命令的问题解决
依次点击“开始--->控制面板--->卸载或更改程序--->打开或关闭Windows功能
勾选Telnet服务器和Telnet客户端并确定即可.
然后在windows服务中启动
安装后默认是上面的情况,根据自己的需求设置成启动或者手动启动,笔者改为手动启动.然后启动telnet服务.
客户端演示:
我们使用telnet作为客户端发包给服务器,现在使用Mina框架编写一个简单的客户端
客户端业务处理类
package org.dennisit.mina.demo; import java.util.Date; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; /** * * ClientHandler.java * * 功 能: TODO * 类 名: ClientHandler.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-2-21 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * */ public class ClientHandler extends IoHandlerAdapter{ /** * 在会话打开时向服务端发送当前日期 */ @Override public void sessionOpened(IoSession session) throws Exception { session.write("客户端会话打开时间" +new Date()); } @Override public void messageReceived(IoSession session, Object message) throws Exception { System.out.println("我是客户端,收到响应:" + message.toString()); } @Override public void messageSent(IoSession session, Object message) throws Exception { System.out.println("我是客户端,我发送的消息:" + message); } @Override public void sessionClosed(IoSession session) throws Exception { System.out.println("客户端会话关闭时间"+new Date()); } @Override public void exceptionCaught(IoSession session, Throwable cause) throws Exception { System.out.println("我是客户端,系统出现异常:" + cause.getMessage()); session.close(); } }
客户端主类
package org.dennisit.mina.demo; import java.net.InetSocketAddress; import org.apache.mina.core.filterchain.IoFilter; import org.apache.mina.core.future.ConnectFuture; import org.apache.mina.core.service.IoConnector; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.textline.TextLineCodecFactory; import org.apache.mina.filter.logging.LoggingFilter; import org.apache.mina.transport.socket.nio.NioSocketConnector; /** * * MinaClient.java * * 功 能: TODO * 类 名: MinaClient.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-2-21 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * */ public class MinaClient { private static final String HOST = "127.0.0.1"; private static final int PORT = 8888; public static void main(String[] args) { IoConnector connector = new NioSocketConnector(); connector.setHandler(new ClientHandler()); // 设置连接超时时间 单位毫秒 connector.setConnectTimeout(30000); //添加过滤器和日志组件 IoFilter filter = new ProtocolCodecFilter(new TextLineCodecFactory()); connector.getFilterChain().addLast(" codec", filter); connector.getFilterChain().addLast("logging", new LoggingFilter()); // 创建连接 IoSession session = null; try { ConnectFuture connect = connector.connect(new InetSocketAddress(HOST, PORT)); // 等待连接创建完成 connect.awaitUninterruptibly(); // 获取session session = connect.getSession(); session.write("客户端连接测试"); } catch (Exception e) { System.out.println("客户端连接异常"); } session.getCloseFuture().awaitUninterruptibly(); connector.dispose(); } }
运行效果
可以发现客户端和服务器的代码非常类似,客户端演示的是直接发送String消息,可以使用MINA自带的编码协议,客户端处理器和服务端处理器是很像的,它们都是在收到数据包之后的处理。
客户端业务处理类中比服务端处理器新增一个接口messageSent(…),在实际应用中我们经常需要记录我们发送的日志,方便后期问题追踪。日志的记录需要有唯一性的标记,最好的办法就是把报文的标记在发送之前保存在IoSession,使用session.getAttribute("KEY")取出标记。
在项目开发的实际应用中,我们的通讯报文一般都不会使用String传输,因为系统的运行环境或者编程语言一般都是不同的,最常用的就是以字节传输,报文的格式需要双方约定规范,比如xml格式,或者定长格式或者其它格式,最终都会转换成字节传输。那么这里就需要我们编写自定义的编码和解码方案。
编写编码方案工厂类,注册自定义的编码方案
在Mina 中的协议编解码器通过过滤器 ProtocolCodecFilter 构造,这个过滤器的构造方法需 要一个 ProtocolCodecFactory,这从前面注册 TextLineCodecFactory 的代码就可以看出来。
ProtocolCodecFactory 中有如下两个方法:
public interface ProtocolCodecFactory { ProtocolEncoder getEncoder(IoSession session) throws Exception; ProtocolDecoder getDecoder(IoSession session) throws Exception; }
因此,构建一个 ProtocolCodecFactory 需要 ProtocolEncoder、ProtocolDecoder 两个实例。你可能要问 JAVA 对象和二进制数据之间如何转换呢?这个要依据具体的通信协议,也就是 Server 端要和 Client 端约定网络传输的数据是什么样的格式,譬如:第一个字节表示数据 长度,第二个字节是数据类型,后面的就是真正的数据(有可能是文字、有可能是图片等等), 然后你可以依据长度从第三个字节向后读,直到读取到指定第一个字节指定长度的数据。
简单的说,HTTP 协议就是一种浏览器与 Web 服务器之间约定好的通信协议,双方按照指定 的协议编解码数据。我们再直观一点儿说,前面一直使用的 TextLine 编解码器就是在读取 网络上传递过来的数据时,只要发现哪个字节里存放的是 ASCII 的 10、13 字符(\r、\n), 就认为之前的字节就是一个字符串(默认使用 UTF-8 编码)。
以上所说的就是各种协议实际上就是网络七层结构中的应用层协议,它位于网络层(IP)、 传输层(TCP)之上,Mina 的协议编解码器就是让你实现一套自己的应用层协议栈。
编码器实现
package org.dennisit.mina.codec; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolEncoderAdapter; import org.apache.mina.filter.codec.ProtocolEncoderOutput; /** * * HEncoder.java * * 功 能: 编码器 * 类 名: HEncoder.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-2-21 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * */ public class HEncoder extends ProtocolEncoderAdapter { private final Charset charset; /** * 构造依赖 * @param charset */ public HEncoder(Charset charset){ this.charset = charset; } /** * 编码规则 */ @Override public void encode(IoSession session, Object obj, ProtocolEncoderOutput out) throws Exception { CharsetEncoder encoder = charset.newEncoder(); IoBuffer io = IoBuffer.allocate(100).setAutoExpand(true); io.putString(obj.toString(), encoder); io.put((byte)'\r'); io.put((byte)'\n'); io.flip(); out.write(io); } }
在 Mina 中编写编码器可以实现 ProtocolEncoder,其中有 encode()、dispose()两个方法需 要实现。这里的 dispose()方法用于在销毁编码器时释放关联的资源,由于这个方法一般我 们并不关心,所以通常我们直接继承适配器 ProtocolEncoderAdapter。
解码器实现
package org.dennisit.mina.codec; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.CumulativeProtocolDecoder; import org.apache.mina.filter.codec.ProtocolDecoderOutput; /** * * HDecoder.java * * 功 能: 解码器 * 类 名: HDecoder.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-2-21 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * */ public class HDecoder extends CumulativeProtocolDecoder{ private final Charset charset; /** * 构造依赖 * @param charset */ public HDecoder(Charset charset) { this.charset = charset; } @Override protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { CharsetDecoder decoder = charset.newDecoder(); //设置数据存放的IoBuffer大小 IoBuffer ioBuffer = IoBuffer.allocate(1024).setAutoExpand(true); System.out.println("开始解码"); while(in.hasRemaining()){ byte bte = in.get(); //将数据存放到IoBuffer中 ioBuffer.put(bte); if(bte == '\n'){ ioBuffer.flip(); byte[] bt = new byte[ioBuffer.limit()]; ioBuffer.get(bt); String message = new String (bt,decoder.charset()); //重置ioBuffer ioBuffer = IoBuffer.allocate(100).setAutoExpand(true); out.write(message); } } return true; } }
在 Mina 中编写解码器,可以实现 ProtocolDecoder 接口,其中有 decode()、finishDecode()、 dispose()三个方法。这里的 finishDecode()方法可以用于处理在 IoSession 关闭时剩余的 读取数据,一般这个方法并不会被使用到,除非协议中未定义任何标识数据什么时候截止 的约定,譬如:Http 响应的 Content-Length 未设定,那么在你认为读取完数据后,关闭 TCP 连接(IoSession 的关闭)后,就可以调用这个方法处理剩余的数据,当然你也可以忽略调 剩余的数据。同样的,一般情况下,我们只需要继承适配器 ProtocolDecoderAdapter,关 注 decode()方法即可。
但前面说过解码器相对编码器来说,最麻烦的是数据发送过来的规模,以聊天室为例,一个 TCP 连接建立之后,那么隔一段时间就会有聊天内容发送过来,也就是 decode()方法会被往 复调用,这样处理起来就会非常麻烦。那么 Mina 中幸好提供了 CumulativeProtocolDecoder 类,从名字上可以看出累积性的协议解码器,也就是说只要有数据发送过来,这个类就会去 读取数据,然后累积到内部的 IoBuffer 缓冲区,但是具体的拆包(把累积到缓冲区的数据 解码为 JAVA 对象)交由子类的 doDecode()方法完成,实际上 CumulativeProtocolDecoder 就是在 decode()反复的调用暴漏给子类实现的 doDecode()方法。
具体执行过程如下所示:
A. 你的 doDecode()方法返回 true 时,CumulativeProtocolDecoder 的 decode()方法会首先判断你是否在 doDecode()方法中从内部的 IoBuffer 缓冲区读取了数据,如果没有,ce); buffer.putString(smsContent, ce);buffer.flip();则会抛出非法的状态异常,也就是你的 doDecode()方法返回 true 就表示你已经消费了 本次数据(相当于聊天室中一个完整的消息已经读取完毕),进一步说,也就是此时你 必须已经消费过内部的 IoBuffer 缓冲区的数据(哪怕是消费了一个字节的数据)。如果 验证过通过,那么 CumulativeProtocolDecoder 会检查缓冲区内是否还有数据未读取, 如果有就继续调用 doDecode()方法,没有就停止对 doDecode()方法的调用,直到有新 的数据被缓冲。
B. 当你的 doDecode()方法返回 false 时,CumulativeProtocolDecoder 会停止对 doDecode() 方法的调用,但此时如果本次数据还有未读取完的,就将含有剩余数据的 IoBuffer 缓 冲区保存到 IoSession 中,以便下一次数据到来时可以从 IoSession 中提取合并。如果 发现本次数据全都读取完毕,则清空 IoBuffer 缓冲区。简而言之,当你认为读取到的数据已经够解码了,那么就返回 true,否则就返回 false。这 个 CumulativeProtocolDecoder 其实最重要的工作就是帮你完成了数据的累积,因为这个工 作是很烦琐的。
创建编解码工厂
package org.dennisit.mina.codec; import java.nio.charset.Charset; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFactory; import org.apache.mina.filter.codec.ProtocolDecoder; import org.apache.mina.filter.codec.ProtocolEncoder; /** * * HCoderFactory.java * * 功 能: 编解码工厂 * 类 名: HCoderFactory.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-2-21 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * */ public class HCoderFactory implements ProtocolCodecFactory { private final HEncoder encoder; private final HDecoder decoder; public HCoderFactory() { this(Charset.defaultCharset()); } public HCoderFactory(Charset charSet) { this.encoder = new HEncoder(charSet); this.decoder = new HDecoder(charSet); } @Override public ProtocolDecoder getDecoder(IoSession session) throws Exception { return decoder; } @Override public ProtocolEncoder getEncoder(IoSession session) throws Exception { return encoder; } }
这个工厂类就是包装了编码器、解码器,通过接口中的 getEncoder()、getDecoder() 方法向 ProtocolCodecFilter 过滤器返回编解码器实例,以便在过滤器中对数据进行编解码 处理。
我们创建好了自定义的编解码有关的类后,我们设置Server和Client的编码工厂为我们自定义的编码工厂类:
//使用mina中自带的编解码协议 acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory())); //使用自定义的编解码协议 DefaultIoFilterChainBuilder chain = acceptor.getFilterChain(); chain.addLast("mycoder", new ProtocolCodecFilter(new HCoderFactory(Charset.forName("UTF-8"))));
参考文献: http://blog.sina.com.cn/s/blog_48b9354d0100psts.html
参考文献: http://xiaominghimi.blog.51cto.com/2614927/969791
参考文献: http://qianhao-1987.iteye.com/blog/1476125
转载请注明出处:[http://www.cnblogs.com/dennisit/archive/2013/02/21/2920693.html]