Apache Mina是一个能够帮助用户开发高性能和高伸缩性网络应用程序的框架。它通过Java nio技术基于TCP/IP和UDP/IP协议提供了抽象的、事件驱动的、异步的API。是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的 MINA 版本支持基于 Java NIO 技术的 TCP/UDP 应用程序开发、串口通讯程序(只在最新的预览版中提供),MINA 所支持的功能也在进一步的扩展中。目前正在使用 MINA 的软件包括有:Apache Directory Project、AsyncWeb、AMQP(Advanced Message Queuing Protocol)、RED5 Server(Macromedia Flash Media RTMP)、ObjectRADIUS、Openfire 等等。
这个是比较官方的说法,在实际开发中,比如游戏开发,智能家居中要求加入及时通信功能,这样就能达到了直接通过手机发送报文信息给这些可爱的电器们。首先我们要介绍一下mina的相关知识,篇幅有点长,如果直接想例子的话,可以跳过这一段。
一、Mina包几个比较重要的接口:
- IoServiece :这个接口在一个线程上负责套接字的建立,拥有自己的 Selector,监听是否有连接被建立。
- IoProcessor :这个接口在另一个线程上负责检查是否有数据在通道上读写,也就是说它也拥有自己的 Selector,这是与我们使用 JAVA NIO 编码时的一个不同之处,通常在 JAVA NIO 编码中,我们都是使用一个 Selector,也就是不区分 IoService与 IoProcessor 两个功能接口。另外,IoProcessor 负责调用注册在 IoService 上的过滤器,并在过滤器链之后调用 IoHandler。
- IoAccepter :相当于网络应用程序中的服务器端
- IoConnector :相当于客户端
- IoSession :当前客户端到服务器端的一个连接实例
- IoHandler :这个接口负责编写业务逻辑,也就是接收、发送数据的地方。这也是实际开发过程中需要用户自己编写的部分代码。
- IoFilter :过滤器用于悬接通讯层接口与业务层接口,这个接口定义一组拦截器,这些拦截器可以包括日志输出、黑名单过滤、数据的编码(write 方向)与解码(read 方向)等功能,其中数据的 encode与 decode是最为重要的、也是你在使用 Mina时最主要关注的地方。
MIINA架构图
简单地来讲,就分为三层:
- I/O Service :负责处理I/O。
- I/O Filter Chain :负责编码处理,字节到数据结构或数据结构到字节的转换等,即非业务逻辑的操作。
- I/O Handler :负责处理业务逻辑。
客户端的通信过程:
- 通过SocketConnector同服务器端建立连接。
- 链接建立之后I/O的读写交给了I/O Processor线程,I/O Processor是多线程的。
- 通过I/O Processor读取的数据经过IoFilterChain里所有配置的IoFilter,IoFilter进行消息的过滤,格式的转换,在这个层面可以制定一些自定义的协议。
- 最后IoFilter将数据交给Handler进行业务处理,完成了整个读取的过程。
- 写入过程也是类似,只是刚好倒过来,通过IoSession.write写出数据,然后Handler进行写入的业务处理,处理完成后交给IoFilterChain,进行消息过滤和协议的转换,最后通过I/O Processor将数据写出到socket通道。
IoFilterChain作为消息过滤链
- 读取的时候是从低级协议到高级协议的过程,一般来说从byte字节逐渐转换成业务对象的过程。
- 写入的时候一般是从业务对象到字节byte的过程。
客户端通信过程 IoSession贯穿整个通信过程的始终
二、通过网络调试助手当成客户端发送消息给服务器端。
这里服务器端采用的Struts2+Spring4+Mybatis3+Mina2,客户端先用网络调试助手,后面用android移动终端。
(1)Apache官方网站:http://mina.apache.org/downloads.html,大家直接下载即可。
(2)服务器端一共要用到四个jar包,包括一个日志包。将他们放在lib中,并加载进去
mina-core-2.0.7.jar slf4j-log4j12-1.7.6.jar slf4j-api-1.7.6.jar log4j-1.2.14.jar (日志管理包)
(3)如果要使用日志的jar包,则要在项目的src目录下新建一个log4j.properties,添加内容如下:
log4j.rootCategory=INFO, stdout , R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[QC] %p [%t] %C.%M(%L) | %m%n
log4j.appender.R=org.apache.log4j.DailyRollingFileAppender
log4j.appender.R.File=D:\\Tomcat 5.5\\logs\\qc.log
log4j.appender.R.layout=org.apache.log4j.PatternLayout
1log4j.appender.R.layout.ConversionPattern=%d-[TS] %p %t %c - %m%n
log4j.logger.com.neusoft=DEBUG
log4j.logger.com.opensymphony.oscache=ERROR
log4j.logger.net.sf.navigator=ERROR
log4j.logger.org.apache.commons=ERROR
log4j.logger.org.apache.struts=WARN
log4j.logger.org.displaytag=ERROR
log4j.logger.org.springframework=DEBUG
log4j.logger.com.ibatis.db=WARN
log4j.logger.org.apache.velocity=FATAL
log4j.logger.com.canoo.webtest=WARN
log4j.logger.org.hibernate.ps.PreparedStatementCache=WARN
log4j.logger.org.hibernate=DEBUG
log4j.logger.org.logicalcobwebs=WARN
log4j.rootCategory=INFO, stdout , R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[QC] %p [%t] %C.%M(%L) | %m%n
log4j.appender.R=org.apache.log4j.DailyRollingFileAppender
log4j.appender.R.File=D:\\Tomcat 5.5\\logs\\qc.log
log4j.appender.R.layout=org.apache.log4j.PatternLayout
1log4j.appender.R.layout.ConversionPattern=%d-[TS] %p %t %c - %m%n
log4j.logger.com.neusoft=DEBUG
log4j.logger.com.opensymphony.oscache=ERROR
log4j.logger.net.sf.navigator=ERROR
log4j.logger.org.apache.commons=ERROR
log4j.logger.org.apache.struts=WARN
log4j.logger.org.displaytag=ERROR
log4j.logger.org.springframework=DEBUG
log4j.logger.com.ibatis.db=WARN
log4j.logger.org.apache.velocity=FATAL
log4j.logger.com.canoo.webtest=WARN
log4j.logger.org.hibernate.ps.PreparedStatementCache=WARN
log4j.logger.org.hibernate=DEBUG
log4j.logger.org.logicalcobwebs=WARN
(4)服务器端程序
1、mina2与spring整合
<!-- 字符编 码过滤器 不发中文不需要-->
<bean id="codecFilter" class="org.apache.mina.filter.codec.ProtocolCodecFilter">
<constructor-arg>
<bean class="com.th.ssi.mina.util.TextLineChineseCodecFactory"></bean>
</constructor-arg>
</bean>
<!-- 日志过滤器 -->
<bean id="loggingFilter" class="org.apache.mina.filter.logging.LoggingFilter" />
<!-- 过滤器链 -->
<bean id="filterChainBuilder"
class="org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder">
<property name="filters">
<map>
<entry key="loggingFilter" value-ref="loggingFilter" />
<entry key="codecFilter" value-ref="codecFilter" />
</map>
</property>
</bean>
<!-- 设置 I/O 接受器,并指定接收到请求后交给 myHandler 进行处理 -->
<bean id="customEditorConfigurer" class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="java.net.SocketAddress" value="org.apache.mina.integration.beans.InetSocketAddressEditor"/>
</map>
</property>
</bean>
<!-- 定义数据处理Bean -->
<bean id="myHandler" class="com.th.ssi.mina.handler.HelloWorldServerHandler" />
<!-- IoAccepter,绑定到1234端口 -->
<bean id="ioAcceptor" class="org.apache.mina.transport.socket.nio.NioSocketAcceptor" init-method="bind" destroy-method="unbind">
<property name="defaultLocalAddress" value=":1234" />
<property name="handler" ref="myHandler" />
<property name="reuseAddress" value="true" />
<property name="filterChainBuilder" ref="filterChainBuilder" />
</bean>
这里绑定的端口号是1234,handler由HelloWorldServerHandler类处理。字符编码过滤器由TextLineChineseCodecFactory处理。
2、HelloWorldServerHandler类
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
public class HelloWorldServerHandler extends IoHandlerAdapter{
@Override
public void messageReceived(IoSession session, Object message)throws Exception {
System.out.println(message.toString()+"=====");
}
//创建新的连接
@Override
public void sessionOpened(IoSession session) throws Exception {
System.out.println("Open Session:"+session.getId()+"成功连接了一个");
System.out.println(session.getLastReadTime());
}
//关闭连接
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("Close Session:"+session.getId());
}
//输出错误的堆栈信息并关闭会话
@Override
public void exceptionCaught( IoSession session, Throwable cause ) throws Exception
{
//打印异常信息
cause.printStackTrace();
System.out.println("Session id >> "+session.getId()+" << " +cause.getMessage());
}
//会话空闲的时间达到设定的时间时被调用
@Override
public void sessionIdle( IoSession session, IdleStatus status ) throws Exception
{
//System.out.println( "IDLE " + session.getIdleCount( status ));
}
}
TextLineChineseCodecFactory类
public class TextLineChineseCodecFactory implements ProtocolCodecFactory {
private final TextLineEncoder encoder;
private final TextLineChineseDecoder decoder;
/**
* Creates a new instance with the current default {@link Charset}.
*/
public TextLineChineseCodecFactory() {
//this(Charset.defaultCharset());
this(Charset.forName( "UTF-8" ));
}
/**
* Creates a new instance with the specified {@link Charset}. The
* encoder uses a UNIX {@link LineDelimiter} and the decoder uses
* the AUTO {@link LineDelimiter}.
*
* @param charset
* The charset to use in the encoding and decoding
*/
public TextLineChineseCodecFactory(Charset charset) {
encoder = new TextLineEncoder(charset, LineDelimiter.UNIX);
decoder = new TextLineChineseDecoder(charset, LineDelimiter.AUTO);
}
/**
* Creates a new instance of TextLineCodecFactory. This constructor
* provides more flexibility for the developer.
*
* @param charset
* The charset to use in the encoding and decoding
* @param encodingDelimiter
* The line delimeter for the encoder
* @param decodingDelimiter
* The line delimeter for the decoder
*/
public TextLineChineseCodecFactory(Charset charset, String encodingDelimiter, String decodingDelimiter) {
encoder = new TextLineEncoder(charset, encodingDelimiter);
decoder = new TextLineChineseDecoder(charset, decodingDelimiter);
}
/**
* Creates a new instance of TextLineCodecFactory. This constructor
* provides more flexibility for the developer.
*
* @param charset
* The charset to use in the encoding and decoding
* @param encodingDelimiter
* The line delimeter for the encoder
* @param decodingDelimiter
* The line delimeter for the decoder
*/
public TextLineChineseCodecFactory(Charset charset, LineDelimiter encodingDelimiter, LineDelimiter decodingDelimiter) {
encoder = new TextLineEncoder(charset, encodingDelimiter);
decoder = new TextLineChineseDecoder(charset, decodingDelimiter);
}
public ProtocolEncoder getEncoder(IoSession session) {
return encoder;
}
public ProtocolDecoder getDecoder(IoSession session) {
return decoder;
}
/**
* Returns the allowed maximum size of the encoded line.
* If the size of the encoded line exceeds this value, the encoder
* will throw a {@link IllegalArgumentException}. The default value
* is {@link Integer#MAX_VALUE}.
* <p>
* This method does the same job with {@link TextLineEncoder#getMaxLineLength()}.
*/
public int getEncoderMaxLineLength() {
return encoder.getMaxLineLength();
}
/**
* Sets the allowed maximum size of the encoded line.
* If the size of the encoded line exceeds this value, the encoder
* will throw a {@link IllegalArgumentException}. The default value
* is {@link Integer#MAX_VALUE}.
* <p>
* This method does the same job with {@link TextLineEncoder#setMaxLineLength(int)}.
*/
public void setEncoderMaxLineLength(int maxLineLength) {
encoder.setMaxLineLength(maxLineLength);
}
/**
* Returns the allowed maximum size of the line to be decoded.
* If the size of the line to be decoded exceeds this value, the
* decoder will throw a {@link BufferDataException}. The default
* value is <tt>1024</tt> (1KB).
* <p>
* This method does the same job with {@link TextLineDecoder#getMaxLineLength()}.
*/
public int getDecoderMaxLineLength() {
return decoder.getMaxLineLength();
}
/**
* Sets the allowed maximum size of the line to be decoded.
* If the size of the line to be decoded exceeds this value, the
* decoder will throw a {@link BufferDataException}. The default
* value is <tt>1024</tt> (1KB).
* <p>
* This method does the same job with {@link TextLineDecoder#setMaxLineLength(int)}.
*/
public void setDecoderMaxLineLength(int maxLineLength) {
decoder.setMaxLineLength(maxLineLength);
}
}
服务器端这边程序已经写完了,下面用网络调试助手来测试。
这里需要注意的是发送消息的是采用16进制数据在末尾点击 0D 0A 表示换行,网络调试助手才认为你已经输入消息输完了,服务器端才能获取数据。消息如下:
2015-06-10 08:55:42.096 [INFO ] org.apache.mina.filter.logging.LoggingFilter {LoggingFilter.java:157} - RECEIVED: HeapBuffer[pos=0 lim=4 cap=512: 31 23 0D 0A]
1#=====
三、通过安卓客户端发送消息。
需要两个jar包, mina-core-2.0.7.jar slf4j-android-1.6.1-RC1.jar 。百度直接搜索下载即可。
由于接受消息会阻塞Android的进程,所以我把它开在了子线程中。
package com.xby.minatest;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
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.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import android.util.Log;
public class MinaThread extends Thread{
IoSession session = null;
String message ;
public MinaThread(String message){
this.message = message;
}
@Override
public void run() {
Log.e("TEST", "客户端连接开始……");
IoConnector connector = new NioSocketConnector();
connector.setConnectTimeoutMillis(30000);
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(
new TextLineCodecFactory(Charset.forName("UTF-8"),
LineDelimiter.WINDOWS.getValue(), LineDelimiter.WINDOWS.getValue())));
connector.setHandler(new MinaClientHandler());
try {
ConnectFuture future = connector.connect(new InetSocketAddress(ConstantUtil.WEB_MATCH_PATH,ConstantUtil.WEB_MATCH_PORT));
future.awaitUninterruptibly();
session = future.getSession();
session.write(message);
} catch (Exception e) {
Log.d("TEST","客户端链接异常...");
}
session.getCloseFuture().awaitUninterruptibly();
Log.d("TEST","客户端断开...");
connector.dispose();
super.run();
}
}
需要注意的是connector.setHandler()这个方法,发送消息由MinaClientHandler类处理。
MinaClientHandler类
package com.xby.minatest;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
public class MinaClientHandler extends IoHandlerAdapter{
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
super.exceptionCaught(session, cause);
}
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
// TODO Auto-generated method stub
System.out.println(message.toString()+"=====");
super.messageReceived(session, message);
}
@Override
public void messageSent(IoSession session, Object message) throws Exception {
// TODO Auto-generated method stub
System.out.println("====="+message.toString());
super.messageSent(session, message);
}
}
是不是跟服务器端很相似,就那么几个方法。
接下在MainActivity中调用即可。
package com.xby.minatest;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends Activity implements OnClickListener{
private EditText text;
private Button sendBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text = (EditText) findViewById(R.id.edit_text);
sendBtn = (Button) findViewById(R.id.sendBtn);
sendBtn.setOnClickListener(this);
}
@Override
public void onClick(View v) {
String message = text.getText().toString();
//启动线程
new MinaThread(message).start();
}
}
布局文件很简单,就一个文本输入框和一个按钮。就不贴了。
本文到这里就结束了。