Mina 快速入门
引言
最近使用 Mina 开发一个 Java 的 NIO 服务端程序,因此也特意学习了 Apache 的这个 Mina 框架。
首先,Mina 是个什么东西?看下官方网站 (http://mina.apache.org/) 对它的解释:
Apache 的 Mina(Multipurpose Infrastructure Networked Applications) 是一个网络应用框架,可以帮助用户开发高性能和高扩展性的网络应用程序;它提供了一个抽象的、事件驱动的异步 API,使 Java NIO 在各种传输协议(如TCP/IP,UDP/IP协议等)下快速高效开发。
Apache Mina也称为:
- NIO框架
- 客户端/服务端框架(典型的C/S架构)
- 网络套接字(networking socket)类库
总之:我们简单理解它是一个封装底层IO操作,提供高级操作API的通讯框架!
(本文所有内容仅针对Mina2.0在TCP/IP协议下的应用开发)
Mina 入门
1. 环境准备
<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
<version>2.0.7</version>
</dependency>
2. 服务端程序
import org.apache.log4j.Logger;
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.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
public class Demo1Server {
private static Logger logger = Logger.getLogger(Demo1Server.class);
private static int PORT = 3005;
public static void main(String[] args) {
IoAcceptor acceptor = null;
try {
// 创建一个非阻塞的server端的Socket
acceptor = new NioSocketAcceptor();
// 设置过滤器(使用Mina提供的文本换行符编解码器)
acceptor.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(new TextLineCodecFactory(
Charset.forName("UTF-8"),
LineDelimiter.WINDOWS.getValue(),
LineDelimiter.WINDOWS.getValue())));
// 设置读取数据的缓冲区大小
acceptor.getSessionConfig().setReadBufferSize(2048);
// 读写通道10秒内无操作进入空闲状态
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
// 绑定逻辑处理器
acceptor.setHandler(new Demo1ServerHandler());
// 绑定端口
acceptor.bind(new InetSocketAddress(PORT));
logger.info("服务端启动成功... 端口号为:" + PORT);
} catch (Exception e) {
logger.error("服务端启动异常....", e);
e.printStackTrace();
}
}
}
无需解释,大家看上面的注释也许就了解一二了。
注意:创建服务端最主要的就是绑定服务端的消息编码解码过滤器和业务逻辑处理器:
什么是编码与解码哪?大家知道,网络传输的数据都是二进制数据,而我们的程序不可能直接去操作二进制数据;这时候我们就需要来把接收到的字节数组转换为字符串,当然完全可以转换为任何一个java基本数据类型或对象,这就是解码!而编码恰好相反,就是把要传输的字符串转换为字节!
比如上面使用的 Mina 自带的根据文本换行符编解码的 TextLineCodec 过滤器 ------ 指定参数为根据 windows 的换行符编解码,遇到客户端发送来的消息,看到 windows 换行符( )就认为是一个消息了,而发送给客户端的消息,都会在消息末尾添加上 文本换行符;
业务逻辑处理器是 Demo1ServerHandler,看它的具体实现:
import org.apache.log4j.Logger;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import java.util.Date;
public class Demo1ServerHandler extends IoHandlerAdapter {
public static Logger logger = Logger.getLogger(Demo1ServerHandler.class);
@Override
public void sessionCreated(IoSession session) throws Exception {
logger.info("服务端与客户端创建连接...");
}
@Override
public void sessionOpened(IoSession session) throws Exception {
logger.info("服务端与客户端连接打开...");
}
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
String msg = message.toString();
logger.info("服务端接收到的数据为:" + msg);
if ("bye".equals(msg)) { // 服务端断开连接的条件
session.close();
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
session.write(dateFormat.format(new Date()));
}
@Override
public void messageSent(IoSession session, Object message) throws Exception {
logger.info("服务端发送信息成功...");
}
@Override
public void sessionClosed(IoSession session) throws Exception {
}
@Override
public void sessionIdle(IoSession session, IdleStatus status)
throws Exception {
logger.info("服务端进入空闲状态...");
}
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
logger.error("服务端发送异常...", cause);
}
}
自定义的业务逻辑处理器继承了 IoHandlerAdapter 类,它默认覆盖了父类的 7 个方法,其实我们最关心最常用的只有一个方法:messageReceived() ---- 服务端接收到一个消息后进行业务处理的方法;看代码:
public void messageReceived(IoSession session, Object message) throws Exception {
String msg = message.toString();
logger.info("服务端接收到的数据为:" + msg);
if ("bye".equals(msg)) { // 服务端断开连接的条件
session.close();
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
session.write(dateFormat.format(new Date()));
}
接受到客户端信息,并返回给客户端一个日期;如果客户端传递的消息为 bye,就是客户端告诉服务端,可以终止通话了,关闭与客户端的连接。
使用命令行的 telnet 来测试下服务端程序!
2.1 telnet 测试
(1) Windows 下开始菜单 -> 运行 -> 输入 cmd
(2) 输入 telnet 127.0.0.1 3005 回车
(3) 连接成功后,服务端程序的后台会打印如下信息:
(4) telnet 中随便输入一个字符串,回车;则可以看到返回的日期
(5) 输入 bye,回车,提示服务端断开连接
3. 客户端程序
Mina 能做服务端程序,自然也可以做客户端程度啦。最重要的是,客户端程序和服务端程序写法基本一致,很简单的。
import org.apache.log4j.Logger;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.session.IdleStatus;
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.NioSocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
public class Demo1Client {
private static Logger logger = Logger.getLogger(Demo1Client.class);
private static String HOST = "127.0.0.1";
private static int PORT = 3005;
public static void main(String[] args) {
// 创建一个非阻塞的客户端程序
IoConnector connector = new NioSocketConnector();
// 设置链接超时时间
connector.setConnectTimeout(30000);
// 添加过滤器
connector.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(new TextLineCodecFactory(
Charset.forName("UTF-8"),
LineDelimiter.WINDOWS.getValue(),
LineDelimiter.WINDOWS.getValue())));
// 添加业务逻辑处理器类
connector.setHandler(new Demo1ClientHandler());
IoSession session = null;
try {
ConnectFuture future = connector.connect(
new InetSocketAddress(HOST, PORT)); // 创建连接
future.awaitUninterruptibly(); // 等待连接创建完成
session = future.getSession(); // 获得session
session.write("我爱你mina"); // 发送消息
} catch (Exception e) {
logger.error("客户端链接异常...", e);
}
session.getCloseFuture().awaitUninterruptibly();// 等待连接断开
connector.dispose();
}
}
和服务端代码极其相似,不同的是服务端是创建 NioSocketAcceptor 对象,而客户端是创建 NioSocketConnector 对象,同样需要添加编码解码过滤器和业务逻辑过滤器;
业务逻辑过滤器代码:
import org.apache.log4j.Logger;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
public class Demo1ClientHandler extends IoHandlerAdapter {
private static Logger logger = Logger.getLogger(Demo1ClientHandler.class);
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
String msg = message.toString();
logger.info("客户端接收到的信息为:" + msg);
}
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
logger.error("客户端发生异常...", cause);
}
}
它和服务端的业务逻辑处理类一样,继承了 IoHandlerAdapter 类,因此同样可以覆盖父类的7个方法,同样最关心 messageReceived 方法,这里的处理是接收打印了服务端返回的信息;另一个覆盖的方法是异常信息捕获的方法;
测试服务端与客户端程序!
4. 长连接 VS 短连接
上面的案例是一个典型的长连接,与长连接相对应的是短连接,比如常说的请求/响应模式(HTTP协议)。客户端向服务端发送一个请求,建立连接后,服务端处理并响应成功,此时就主动断开连接了!
短连接是一个简单而有效的处理方式,也是应用最广的。问题是哪一方先断开连接呢?可以在服务端,也可以在客户端,但是提倡在服务端主动断开;
Mina 的服务端业务逻辑处理类中有一个方法 messageSent,他是在服务端发送信息成功后调用的:
@Override
public void messageSent(IoSession session, Object message) throws Exception {
logger.info("服务端发送信息成功...");
}
修改后为:
@Override
public void messageSent(IoSession session, Object message) throws Exception {
session.close(true); // 发送成功后主动断开与客户端的连接
logger.info("服务端发送信息成功...");
}
这时候客户端与服务端就是典型的短连接了;再次测试,会发现客户端发送请求,接收成功后就自动关闭了,进程只剩下服务端了!
到此为止,我们已经可以运行一个完整的基于 TCP/IP 协议的应用程序啦!