一. Mina入门
先用Mina做一个简单的应用程序。
第一步.下载使用的Jar包
a. 登录http://mina.apache.org/downloads.html 下载 mina2.0.1.zip,解压获得mina-core-2.0.0-M1.jar
b. 登录 http://www.slf4j.org/download.html 下载slf4j1.5.2.zip,解压获得slf4j-api-1.5.2.jar 与 slf4j-log4j12-1.5.2.jar
c. 添加Log4j的jar包,注意如果使用slf4j-log4j12-XXX.jar,就需要添加log4j1.2.X。我这里使用的是log4j-1.2.14.jar (Logger配置详情参见
http://mina.apache.org/first-steps.html )
OK,4个jar都完备了。
第二步.工程创建配置
创建一个Java Project(默认使用UTF-8编码格式),添加log4j.properties
log4j.rootLogger=DEBUG,MINA,file
log4j.appender.MINA=org.apache.log4j.ConsoleAppender
log4j.appender.MINA.layout=org.apache.log4j.PatternLayout
log4j.appender.MINA.layout.ConversionPattern=%d{yyyy-MM-dd HH\:mm\:ss,SSS} %-5p %c{1} %x - %m%n
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/minademos.log
log4j.appender.file.MaxFileSize=5120KB
log4j.appender.file.MaxBackupIndex=10
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=VAMS %p | %m | [%t] %C.%M(%L)%n
第三步.服务端程序
创建一个简单的服务端程序:(服务端绑定3005端口)
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换行符(\r\n)就认为是一个消息了,而发送给客户端的消息,都会在消息末尾添加上\r\n文本换行符;
业务逻辑处理器是Demo1ServerHandler---看它的具体实现:
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();
}
Date date = new Date();
session.write(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() ---- 服务端接收到一个消息后进行业务处理的方法;看代码:
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
String msg = message.toString();
logger.info("服务端接收到的数据为:" + msg);
if ("bye".equals(msg)) { // 服务端断开连接的条件
session.close();
}
Date date = new Date();
session.write(date);
}
接受到客户端信息,并返回给客户端一个日期;如果客户端传递的消息为“bye”,就是客户端告诉服务端,可以终止通话了,关闭与客户端的连接。
使用命令行的telnet来测试下服务端程序!
a. 启动服务端程序;
b. Windows下开始菜单,运行,输入cmd,回车;
c. 输入:telnet 127.0.0.1 3005 回车
d. 连接成功后,服务端程序的后台会打印如下信息:这个就是业务逻辑逻辑处理器打印的:
e. telnet中随便输入一个字符串,回车;则可以看到返回的日期;
f. 输入bye,回车,提示服务端断开连接
如果需要重新测试,则需要再次重复C的步骤;
注意:
不要输入中文字符,windows不会把中文字符用utf-8编码再发送给服务端的;
第四步.客户端程序
Mina能做服务端程序,自然也可以做客户端程度啦。最重要的是,客户端程序和服务端程序写法基本一致,很简单的。
客户端代码:
public class MinaClient01 {
private static Logger logger = Logger.getLogger(MinaClient01.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对象;同样需要添加编码解码过滤器和业务逻辑过滤器;
业务逻辑过滤器代码:
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方法,这里的处理是接收打印了服务端返回的信息;另一个覆盖的方法是异常信息捕获的方法;
测试服务端与客户端程序!
a. 启动服务端,然后再启动客户端(客户端发送的消息是“我爱你mina”)
b. 服务端接收消息并处理成功;
客户端接收响应结果
第五步.长连接VS短连接
此时,查看windows的任务管理器,会发现:当前操作系统中启动了3个java进程(注意其中一个进程是myEclipse的)。
我们知道,java应用程序的入口是main()方法,启动一个main()方法相当于开始运行一个java应用程序,此时会运行一个Java虚拟机,操作系统中会启动一个进程,就是刚刚看到的“javaw.exe”。也就是每启动一个Java应用程序就是多出一个Java进程。因为启动了Mina服务端和客户端2个服务端程序,所有其他2个进程的出现。
测试一下:再次启动一个客户端程序,查看任务管理器,会发现进程又多出一个,这个是刚刚启动的客户端进程,它和前一个客户端进程一直存在。这就是一个典型的长连接。
长连接的现象在网络中非常普遍,比如我们的QQ客户端程序,登录成功后与腾讯的服务器建立的就是长连接;除非主动关闭掉QQ客户端,或者是QQ服务端挂了,才会断开连接;看我们的服务端程序,就有关闭连接的条件:如果客户端发送信息“bye”,服务端就会主动断开连接!
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
String msg = message.toString();
logger.info("服务端接收到的数据为:" + msg);
if ("bye".equals(msg)) { // 服务端断开连接的条件
session.close();
}
Date date = new Date();
session.write(date);
}
与长连接相对应的是短连接,比如常说的请求/响应模式(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(); //发送成功后主动断开与客户端的连接
logger.info("服务端发送信息成功...");
}
这时候客户端与服务端就是典型的短连接了;再次测试,会发现客户端发送请求,接收成功后就自动关闭了,进程只剩下服务端了!
到此为止,我们已经可以运行一个完整的基于TCP/IP协议的应用程序啦!
总结:
服务端程序或客户端程序创建过程:
创建连接---添加消息过滤器(编码解码等)——>添加业务处理