Mina使用起来多么简洁方便呀,就是不具备Java NIO的基础,只要了解了Mina常用的API,就可以灵活使用并完成应用开发。
不过,从学习的角度来说,阿堂还是建议mina学习者了解一些Java NIO的基础知识,毕竟Mina的底层还是封装的Java NIO的。想学习的网友,可以在阿堂的技术博客(
http://blog.sina.com.cn/heyitang
)上看"JAVA新I/O学习系列笔记(1)"和"JAVA新I/O学习系列笔记(2)"两篇学习笔记,相信看完后一定会很有收获的。
首先,看Mina在项目中所处的位置,如下图
Mina 处于中间层,它不关心底层网络数据如何传输,只负责接收底层数据,过滤并转换为Java对象提供给我们的应用程序,然后把应用程序响应值过滤并转换为底层 识别的字节,提供给底层传输; ------总之:Mina是底层数据传输和用户应用程序交互的接口! Mina工作流程图如下:
这个流程图不仅很直观的看出了Mina的工作流程,也涵盖了Mina的三个核心接口:IoService接口,IoFilter接口和IoHandler接口:
第一步 . 创建服务对象(客户端或服务端) ---IoService接口实现
第二步 . 数据过滤(编码解码等) ---IOFilter接口实现
第三步 . 业务处理 ---IoHandler接口实现
Mina 的精髓是IOFilter,它可以进行日志记录,信息过滤,编码解码等操作,把数据接收发送从业务层独立出来。 而创建服务对象,则是把NIO繁琐的部分进行封装,提供简洁的接口。 业务处理是我们最关心的部分,跟普通的应用程序没任何分别。
1.IoService接口
作用:IoService是创建服务的顶层接口,无论客户端还是服务端,都是从它继承实现的。
常用接口为:IoService,IoAcceptor,IoConnector 常用类为:NioSocketAcceptor,NioSocketConnector
在实际应用中,创建服务端和客户端的代码很简单:
创建服务端:
IoAcceptor acceptor = null; try { // 创建一个非阻塞的server端的Socket acceptor = new NioSocketAcceptor();
创建客户端:
// 创建一个非阻塞的客户端程序 IoConnector connector = new NioSocketConnector();
而我们常常关心的就是服务端和客户端的一些参数信息:
服务端和客户端设置的一些常用参数信息:
1.IoSessionConfig getSessionConfig() 获得IoSession的配置对象IoSessionConfig,通过它可以设置Socket连接的一些选项。
a. void setReadBufferSize(int size) 这个方法设置读取缓冲的字节数,但一般不需要调用这个方法,因为IoProcessor 会自动调整缓冲的大小。你可以调用setMinReadBufferSize()、setMaxReadBufferSize()方法,这样无论 IoProcessor 无论如何自动调整,都会在你指定的区间。
b. void setIdleTime(IdleStatus status,int idleTime): 这个方法设置关联在通道上的读、写或者是读写事件在指定时间内未发生,该通道就进入空闲状态。一旦调用这个方法,则每隔idleTime 都会回调过滤器、IoHandler 中的sessionIdle()方法。
c. void setWriteTimeout(int time): 这个方法设置写操作的超时时间。
d. void setUseReadOperation(boolean useReadOperation): 这个方法设置IoSession 的read()方法是否可用,默认是false。
2.IoFilter接口
Mina最主要的工作就是把底层传输的字节码转换为Java对象,提供给应用程序;或者把应用程序返回的结果转换为字节码,交给底层传输。这些都是由IoFilter完成的,因此IoFilter是Mina的精髓所在。
在Mina程序中,IoFilter是必不可少的;有了它,Mina的层次结构才异常清晰:
oFilter ---- 消息过滤
IoHandler ---- 业务处理
Filter,过滤器的意思。IoFilter,I/O操作的过滤器。IoFilter和Servlet中的过滤器一样,主要用于拦截和过滤网络传输中I/O操作的各种消息。
在Mina 的官方文档中已经提到了IoFilter 的作用:
(1)记录事件的日志(Mina默认提供了LoggingFilter)
(2)测量系统性能
(3)信息验证
(4)过载控制
(5)信息的转换(主要就是编码和解码)
(6)和其他更多的信息
IoService实例会绑定一个DefaultIoFilterChainBuil
der ---- 过滤器链,我们把自定义的各种过滤器(IoFilter)自由的插放在这个过滤器链上了,类似于一种可插拔的功能!
常用接口为:IoFilter,IoFilterChainBuilder
常用类为:IoFilterAdapter,DefaultIoFilterChainBuil
der ProtocolCodecFilter,LoggingFilter
在Mina中一个很重要的概念是协议
为什么要制定协议呢?常用的协议制定方法有哪些?
我 们知道,底层传输的都是二进制数据,服务端和客户端建立连接后进行数据的交互,接受这对方发送来的消息,如何判定发送的请求或者响应的数据结束了呢?总不 能一直傻等着,或者随意的就结束消息接收吧。这就需要一个规则!比如QQ聊天工具,当输入完一个消息后,点击发送按钮向对方发送时,此时系统就会在在你的 消息后添加一个文本换行符,接收方看到这个文本换行符就认为这是一个完整的消息,解析成字符串显示出来。而这个规则,就称之为协议!
制定协议的方法:
. 定长消息法 :这种方式是使用长度固定的数据发送,一般适用于指令发送。譬如:数据发送端规定发送的数据都是双字节,AA 表示启动、BB 表示关闭等等。
. 字符定界法 : 这种方式是使用特殊字符作为数据的结束符,一般适用于简单数据的发送。譬如:在消息的结尾自动加上文本换行符(Windows使用\r\n,Linux使 用\n),接收方见到文本换行符就认为是一个完整的消息,结束接收数据开始解析。注意:这个标识结束的特殊字符一定要简单,常常使用ASCII码中的特殊 字符来标识。
. 定长报文头法 : 使用定长报文头,在报文头的某个域指明报文长度。该方法最灵活,使用最广。譬如:协议为 – 协议编号(1字节)+数据长度(4个字节)+真实数据。请求到达后,解析协议编号和数据长度,根据数据长度来判断后面的真实数据是否接收完整。HTTP 协议的消息报头中的Content-Length 也是表示消息正文的长度,这样数据的接收端就知道到底读到多长的字节数就不用再读取数据了。
根据协议,把二进制数据转换成Java对象称为解码(也叫做拆包);把Java对象转换为二进制数据称为编码(也叫做打包);
下面阿堂就和网友们来一起,针对 字符定界法 来看一个非常实用的demo,来剖析它的流程
阿堂这里的测试场景如下
1. 客户端使用telnet来和服务端交互 telnet 127.0.0.1 3005 (简单起见,阿堂就输入 he两个字符,然后按回键)
2. 服务端会不断显示出接收到客户端发送的字符信息内容。(如h,e,\r,\n等)
3. 如果客户端按了回车键后,服务端会将客户端的完整信息会显示出来,并且向客端返回 Come from heyitang info +日期, 然后中断连接(发送成功后主动断开与客户端的连接, 阿堂这里是使用了tcp/ip的短连接。当然了可以用tcp/ip的长连接,打开此语句session.close();就是短连接,关闭此语句就是长连 接。这个很简单,阿堂就不多介绍了,不懂tcp/ip长连接,短连接的朋友可以在网上查询相关资料介绍)。
为此阿堂还绘制出了交互操作的原理图
测试效果图如下
上述测试的主要代码如下
public class MinaServer {
private static Logger logger = Logger.getLogger(MinaServer.class);
private static final int PORT = 3005;
public static void main(String[] args) throws IOException {
new MinaServer().start();
}
public void start() {
IoAcceptor acceptor = null;
try {
// 创建一个非阻塞的 Server 端的 Socket
acceptor = new NioSocketAcceptor();
// 设置过滤器,(使用 MINA 提供的文本换行符编解码器)
acceptor.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(new MyTextLineCodecFactory(Charset .forName("utf-8"), "\r\n")));
// 设置读取缓冲区的大小
acceptor.getSessionConfig().setReadBufferSize(2048);
// 读写通道10秒内无操作进入空闲状态
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
// 绑定逻辑处理器
acceptor.setHandler(new MinaServerHandler());
// 绑定端口
acceptor.bind(new InetSocketAddress(PORT));
logger.info("服务器启动成功.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
--------------------------------------------------------------------------------------------------
public class MyTextLineCodecDecoder implements ProtocolDecoder {
private static Logger logger = Logger.getLogger(MyTextLineCodecDecoder.class);
private Charset charset; // 编码格式
private String delimiter; // 文本分隔符
private IoBuffer delimBuf; // 文本分割符匹配的变量
// 定义常量值,作为每个IoSession中保存解码任务的key值
private static String CONTEXT = MyTextLineCodecDecoder.class.getName() + ".context";
// 构造函数,必须指定Charset和文本分隔符
public MyTextLineCodecDecoder(Charset charset, String delimiter) {
this.charset = charset;
this.delimiter = delimiter;
}
public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out)
throws Exception {
Context ctx = getContext(session);
if (delimiter == null || "".equals(delimiter)) {
// 如果文本换行符未指定,使用默认值
delimiter = "\r\n";
}
if (charset == null) {
charset = Charset.forName("utf-8");
}
decodeNormal(ctx, in, out);
}
private void decodeNormal(Context ctx, IoBuffer in, ProtocolDecoderOutput out) throws CharacterCodingException {
// 取出未完成任务中已经匹配的文本换行符的个数
int matchCount = ctx.getMatchCount();
// 设置匹配文本换行符的IoBuffer变量
if (delimBuf == null) {
IoBuffer tmp = IoBuffer.allocate(2).setAutoExpand(true);
tmp.putString(delimiter, charset.newEncoder());
tmp.flip();
delimBuf = tmp;
}
//解码的IoBuffer中数据的原始信息
int oldPos = in.position(); //输出值为0
int oldLimit = in.limit(); //输出值为1
logger.info("******************************************************************************");
logger.info("开始进入解码方法-----------------------------------------------------------------");
logger.info("");
logger.info("init Start--------------------------------------------------------------------");
logger.info("in.postion() = "+oldPos);
logger.info("in.Limit() = "+oldLimit);
logger.info("in.capacity() = "+in.capacity());
logger.info("matchCount = "+matchCount);
logger.info("init End---------------------------------------------------------------------");
logger.info("");
//变量解码的IoBuffer
while (in.hasRemaining()) {
byte b = in.get();
logger.info("");
logger.info("输入进来的字符为 = "+(char)b+",对应的ascii值 = "+b);
logger.info("in.position() = "+in.position()+",in.limit() = "+in.limit());
logger.info("");
//当b的ascii值为13,10 即为\r,\n时,会进入下述if语句
if (delimBuf.get(matchCount) == b) {
// b='\r'时,matchCount=1, b='\n'时,matchCount=2
matchCount++;
logger.info("matchCount = "+matchCount);
//当前匹配到字节个数与文本换行符字节个数相同,即 b='\n'时
//此时matchCount=2, delimBuf.limit()=2
if (matchCount == delimBuf.limit()) {
// 获得当前匹配到的position(position前所有数据有效)
int pos = in.position(); //值为2
logger.info("pos = "+pos);
in.limit(pos); //值为2
// position回到原始位置
in.position(oldPos); //值为0
// 追加到Context对象未完成数据后面
ctx.append(in); //将 \r\n这两个字符添加到 ctx.getBuf()中
// in中匹配结束后剩余数据
in.limit(oldLimit); //值为2
in.position(pos); //值为2
IoBuffer buf = ctx.getBuf(); //此时是得到 he\r\n
buf.flip(); //此时 buf.position=0,buf.limit()=4
buf.limit(buf.limit() - matchCount); //4-2 = 2
try{
// 输出解码内容 ,即 he
out.write(buf.getString(ctx.getDecoder()));
}
finally {
buf.clear(); // 释放缓存空间
}
matchCount = 0;
}
}else { //h字符,e字符时,均会进入 此else逻辑判断中
//把in中未解码内容放回buf中
//下面会在 输入的字符不是 \r\n时会需要保存使用
in.position(oldPos);
ctx.append(in);
ctx.setMatchCount(matchCount);
}
}
}
// 从IoSession中获取Context对象
private Context getContext(IoSession session) {
Context ctx;
ctx = (Context) session.getAttribute(CONTEXT);
if (ctx == null) {
ctx = new Context();
session.setAttribute(CONTEXT, ctx);
}
return ctx;
}
public void dispose(IoSession arg0) throws Exception {
// TODO Auto-generated method stub
}
public void finishDecode(IoSession arg0, ProtocolDecoderOutput arg1)
throws Exception {
// TODO Auto-generated method stub
}
// 内部类,保存IoSession解码时未完成的任务
private class Context {
private CharsetDecoder decoder;
private IoBuffer buf;
// 保存真实解码内容
private int matchCount = 0; // 匹配到的文本换行符个数
private Context() {
decoder = charset.newDecoder();
buf = IoBuffer.allocate(80).setAutoExpand(true);
}
// 重置
public void reset() {
matchCount = 0;
decoder.reset();
}
// 追加数据
public void append(IoBuffer in) {
getBuf().put(in);
}
public CharsetDecoder getDecoder() {
return decoder;
}
public IoBuffer getBuf() {
return buf;
}
public int getMatchCount() {
return matchCount;
}
public void setMatchCount(int matchCount) {
this.matchCount = matchCount;
}
}
}
-------------------------------------------------------------------------------------------------------------------------
public class MinaServerHandler extends IoHandlerAdapter {
private static Logger logger = Logger.getLogger(MinaServerHandler.class);
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
logger.error("服务端发送异常");
super.exceptionCaught(session, cause);
}
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
super.messageReceived(session, message);
String msg = message.toString();
logger.info("服务端接收到数据: " + msg);
Date date = new Date();
session.write("Come from heyitang info "+date);
}
@Override
public void messageSent(IoSession session, Object message) throws Exception {
logger.info("服务端发送数据 = "+message);
session.close();// 发送成功后主动断开与客户端的连接, 实现短连接
logger.info("服务端发送消息成功");
}
@Override
public void sessionClosed(IoSession session) throws Exception {
logger.info("断开连接");
super.sessionClosed(session);
}
@Override
public void sessionCreated(IoSession session) throws Exception {
logger.info("服务端与客户端创建连接");
super.sessionCreated(session);
}
@Override
public void sessionIdle(IoSession session, IdleStatus status)
throws Exception {
logger.info("服务端进入空闲状态");
super.sessionIdle(session, status);
}
@Override
public void sessionOpened(IoSession session) throws Exception {
logger.info("服务端与客户端的连接打开");
super.sessionOpened(session);
}
}
--------------------------------------------------------------------------------------------------------
public class MyTextLineCodecFactory implements ProtocolCodecFactory {
private Charset charset; // 编码格式
private String delimiter; // 文本分隔符
public MyTextLineCodecFactory(Charset charset, String delimiter) {
this.charset = charset;
this.delimiter = delimiter;
}
public ProtocolDecoder getDecoder(IoSession session) throws Exception {
return new MyTextLineCodecDecoder(charset, delimiter);
}
public ProtocolEncoder getEncoder(IoSession session) throws Exception {
return new MyTextLineCodecEncoder(charset, delimiter);
}
}
-------------------------------------------------------------------------------------------------------------------------
public class MyTextLineCodecEncoder implements ProtocolEncoder {
private static Logger logger = Logger.getLogger(MyTextLineCodecEncoder.class);
private Charset charset; // 编码格式
private String delimiter; // 文本分隔符
public MyTextLineCodecEncoder(Charset charset, String delimiter) {
this.charset = charset;
this.delimiter = delimiter;
}
public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {
logger.info("开始进入编码方法-----------------------------------------------------------------");
// 如果文本换行符未指定,使用默认值
if (delimiter == null || "".equals(delimiter)) {
delimiter = "\r\n";
}
if (charset == null) {
charset = Charset.forName("utf-8");
}
String value = message.toString();
IoBuffer buf = IoBuffer.allocate(value.length()).setAutoExpand(true);
//真实数据
buf.putString(value, charset.newEncoder());
//文本换行符
buf.putString(delimiter, charset.newEncoder());
buf.flip();
out.write(buf);
}
public void dispose(IoSession session) throws Exception {}
}
这个流程图不仅很直观的看出了Mina的工作流程,也涵盖了Mina的三个核心接口:IoService接口,IoFilter接口和IoHandler接口:
第一步 . 创建服务对象(客户端或服务端) ---IoService接口实现
第二步 . 数据过滤(编码解码等) ---IOFilter接口实现
第三步 . 业务处理 ---IoHandler接口实现
1.IoService接口
作用:IoService是创建服务的顶层接口,无论客户端还是服务端,都是从它继承实现的。
常用接口为:IoService,IoAcceptor,IoConnector 常用类为:NioSocketAcceptor,NioSocketConnector
在实际应用中,创建服务端和客户端的代码很简单:
创建服务端:
IoAcceptor acceptor = null; try { // 创建一个非阻塞的server端的Socket acceptor = new NioSocketAcceptor();
创建客户端:
// 创建一个非阻塞的客户端程序 IoConnector connector = new NioSocketConnector();
而我们常常关心的就是服务端和客户端的一些参数信息:
服务端和客户端设置的一些常用参数信息:
1.IoSessionConfig getSessionConfig() 获得IoSession的配置对象IoSessionConfig,通过它可以设置Socket连接的一些选项。
a. void setReadBufferSize(int size) 这个方法设置读取缓冲的字节数,但一般不需要调用这个方法,因为IoProcessor 会自动调整缓冲的大小。你可以调用setMinReadBufferSize()、setMaxReadBufferSize()方法,这样无论 IoProcessor 无论如何自动调整,都会在你指定的区间。
b. void setIdleTime(IdleStatus status,int idleTime): 这个方法设置关联在通道上的读、写或者是读写事件在指定时间内未发生,该通道就进入空闲状态。一旦调用这个方法,则每隔idleTime 都会回调过滤器、IoHandler 中的sessionIdle()方法。
c. void setWriteTimeout(int time): 这个方法设置写操作的超时时间。
d. void setUseReadOperation(boolean useReadOperation): 这个方法设置IoSession 的read()方法是否可用,默认是false。
2.IoFilter接口
在Mina程序中,IoFilter是必不可少的;有了它,Mina的层次结构才异常清晰:
oFilter ---- 消息过滤
IoHandler ---- 业务处理
在Mina 的官方文档中已经提到了IoFilter 的作用:
(1)记录事件的日志(Mina默认提供了LoggingFilter)
(2)测量系统性能
(3)信息验证
(4)过载控制
(5)信息的转换(主要就是编码和解码)
(6)和其他更多的信息
常用接口为:IoFilter,IoFilterChainBuilder
常用类为:IoFilterAdapter,DefaultIoFilterChainBuil
在Mina中一个很重要的概念是协议
制定协议的方法:
. 定长消息法 :这种方式是使用长度固定的数据发送,一般适用于指令发送。譬如:数据发送端规定发送的数据都是双字节,AA 表示启动、BB 表示关闭等等。
. 字符定界法 : 这种方式是使用特殊字符作为数据的结束符,一般适用于简单数据的发送。譬如:在消息的结尾自动加上文本换行符(Windows使用\r\n,Linux使 用\n),接收方见到文本换行符就认为是一个完整的消息,结束接收数据开始解析。注意:这个标识结束的特殊字符一定要简单,常常使用ASCII码中的特殊 字符来标识。
. 定长报文头法 : 使用定长报文头,在报文头的某个域指明报文长度。该方法最灵活,使用最广。譬如:协议为 – 协议编号(1字节)+数据长度(4个字节)+真实数据。请求到达后,解析协议编号和数据长度,根据数据长度来判断后面的真实数据是否接收完整。HTTP 协议的消息报头中的Content-Length 也是表示消息正文的长度,这样数据的接收端就知道到底读到多长的字节数就不用再读取数据了。
根据协议,把二进制数据转换成Java对象称为解码(也叫做拆包);把Java对象转换为二进制数据称为编码(也叫做打包);
下面阿堂就和网友们来一起,针对 字符定界法 来看一个非常实用的demo,来剖析它的流程
阿堂这里的测试场景如下
1. 客户端使用telnet来和服务端交互 telnet 127.0.0.1 3005
2. 服务端会不断显示出接收到客户端发送的字符信息内容。(如h,e,\r,\n等)
3. 如果客户端按了回车键后,服务端会将客户端的完整信息会显示出来,并且向客端返回
为此阿堂还绘制出了交互操作的原理图
测试效果图如下
上述测试的主要代码如下
public class MinaServer {
}
--------------------------------------------------------------------------------------------------
public class MyTextLineCodecDecoder implements ProtocolDecoder {
}
-------------------------------------------------------------------------------------------------------------------------
public class MinaServerHandler extends IoHandlerAdapter {
}
--------------------------------------------------------------------------------------------------------
public class MyTextLineCodecFactory implements ProtocolCodecFactory {
}
-------------------------------------------------------------------------------------------------------------------------
public class MyTextLineCodecEncoder implements ProtocolEncoder {
}