第二章 - 基础
在第一章,我们对Apache MINA有了一个大致的了解。在这一章里我们来研究一下客户端/服务器架构以及如何创建基于MINA的服务器和客户端。
我们还会做一下简单的TCP和UDP协议的服务器和客户端。
基于MINA的应用程序架构
经常有人会问:基于MINA的应用程序是什么样子?在本章中我们会介绍基于MINA的应用程序架构。
鸟瞰图 :
这里,我们可以看见MINA是应用程序(服务器或客户端)和网络层的粘合剂,网络层可以是基于TCP,UDP,In-VM甚至是RS-232C串口协议(客户端)。
你只需要在MINA之上设计你的应用程序,而不用考虑网络层的复杂性。
让我们来深入了解一下细节,下面这张图展现了MINA的内部情况和每个组件做什么事情:
(The image is from Emmanuel Lécharny presentation MINA in real life (ApacheCon EU 2009))
一般来说,基于MINA的应用程序可以分为3层:
- I/O Service - 执行IO操作
- I/O Filter Chain(过滤器链) - 过滤或者转换byte数组到期望的数据结构,或相反
- I/O Handler - 实际的业务逻辑写在这里
所以,要创建一个基于MINA的应用程序,你需要:
- 创建一个I/O service,你可以选用既存的服务类(*Acceptor)或者自己创建一个。
- 创建一个Filter Chain,你可以选用既存的Filter或创建一个自定义的Filter来转换请求/响应
- 创建一个I/O Handler,写业务逻辑来处理不同的消息
在后面的服务器架构和客户端架构中我们将介绍更详细的内容
当然,MINA提供的不止这些,你在做网络应用程序时还可能涉及到一些其他的课题,例如消息的编码和解码,网络配置,提升延展性等等, 你想在后面的章节中了解到这些课题。
服务器架构
一般来说,一个服务器在某个端口上监听请求,处理请求,然后返回结果。同时它需要为每个客户端创建和维护一个会话(无论是基于TCP还是UDP协议),我们会在第4章详细介绍这个。
IOAcceptor 监听网络上过来的连接和数据包
对于每一个新连接会创建一个会话,后面这个IP/端口对应的请求处理都和这个会话绑定在一起。
会话中接收到的数据包会想图中那样经过FilterChain。Filter可以修改数据包的内容(例如转换成对象,添加或删除一些信息等等)。 PacketEncoder/Decoder在原始的byte数组和高级对象之间的转换过程中起到非常重要的作用。
最后原始数据包或者被转换成的对象会在IOHandler里得到处理。IOHandler可以用来执行最终的业务逻辑。
创建会话
任何时候当一个客户端连接上服务器,我们会创建一个新的会话来保存持久化的数据。即时使用非面向连接的协议(如UDP)也会创建Session。
一旦会话建立以后,这个会话里每个新的消息都会唤醒NIO的Selector。
客户端架构
我们已经大致了解了基于MINA的服务器架构,现在来看看客户端是什么样子的。客户端需要连接服务器,发送消息并且处理响应。
- 客户端首先创建一个IOConnector(MINA会创建Socket连接),初始化和服务器的绑定。
- 连接创建以后,会有一个新的会话被创建并且和这个连接绑定在一起。
- 应用程序/客户端往会话里写数据,这些数据在经过一系列Filter的处理后,最终会被发送到服务器
- 所有的响应/从服务器获取的数据,会经过FilterChain处理并最后交由IOHandler处理。
TCP服务器例子
下面我们使用MINA做一个时间服务器。下面的例子需要一下环境:
MINA 2.x 核心包
JDK 1.5 或以上
SLF4J 1.3.0 或以上
我们在Windows2000和Linux上测试过下面的程序。 如果在你的环境中不能运行下面的程序,尽管联系我们。 并且我们使下面的程序不依赖于任何开发环境(IDE,编辑器等),你可以使用你喜欢的任何开发环境来完成下面的例子。我们没有写如果编译和执行下面的程序。如果你不知道该如何编译和运行java,请先查看Java相关的教程。
编写MINA的时间服务器
我们从创建MinaTimeServer.java文件开始, 初始代码如下:
public class MinaTimeServer {
public static void main(String[] args) {
// code will go here next
}
}
这个程序非常简单明了。我们简单的定义了一个main方法,以使程序能运行起来。从现在开始我们要一点点的创建服务器,首先我们需要一个对象可以监听进来的连接。因为我们的程序是基于TCP/IP协议的,我们使用SocketAccepter这个类。
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
public class MinaTimeServer
{
public static void main( String[] args )
{
IoAcceptor acceptor = new NioSocketAcceptor();
}
}
实际上我们使用的是NioSocketAccepter(SocketAcceptor的一个实现),然后我们接着定义一个Handler类,然后把NioSocketAcceptor绑定到一个端口上。
import java.net.InetSocketAddress;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
public class MinaTimeServer
{
private static final int PORT = 9123;
public static void main( String[] args ) throws IOException
{
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.bind( new InetSocketAddress(PORT) );
}
}
你可以看到我们通过调用 bind( new InetSocketAddress(PORT) );方法来指定socket要绑定的端口号,这样远程客户端就可以通过这个端口连接到服务器。
接下来我们向配置中添加一个过滤器,这个过滤器会在诸如新会话创建,接收消息,发送消息,会话关闭等事件发生向日志里输出信息。 下一个过滤器是ProtocolCodecFilter,这个过滤器会把二进制数据转换成消息对象,或逆向转换。我们使用现成的TextLineCodecFactory来编码和解码消息,它可以处理基于文本的消息对象(你不必自己编写编码部分)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import org.apache.mina.core.service.IoAcceptor;
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;
public class MinaTimeServer
{
public static void main( String[] args )
{
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
acceptor.bind( new InetSocketAddress(PORT) );
}
}
接下来我们定义一个handler,这个handler用来响应客户端连接和时间请求。一个handler的实现类必须实现IoHandler接口。对于创建基于MINA的应用程序来说这部分基本上都是工作量最大的,因为它要为所有进来的客户端请求提供服务。在这个例子中我们继承IoHandlerAdapter,这个类实现了适配器设计模式,对所有的IoHandler接口的方法提供了默认的实现,这大大的减少编码的工作量,我们只需要根据实际情况override其中一部分方法就可以了。
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import org.apache.mina.core.service.IoAcceptor;
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;
public class MinaTimeServer
{
public static void main( String[] args ) throws IOException
{
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
acceptor.setHandler( new TimeServerHandler() );
acceptor.bind( new InetSocketAddress(PORT) );
}
}
上面我们通过acceptor.setHandler( new TimeServerHandler() );设置了IoHandler。
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.service.IoAcceptor;
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;
public class MinaTimeServer
{
public static void main( String[] args ) throws IOException
{
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
acceptor.setHandler( new TimeServerHandler() );
acceptor.getSessionConfig().setReadBufferSize( 2048 );
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
acceptor.bind( new InetSocketAddress(PORT) );
}
}
这里我们添加了两行:设置输入缓存区大小和会话的空闲属性。输入缓冲区大小可以告诉操作系统为接收的数据准备多大的空间。第二个设置里我们指定检查会话空闲的方法和间隔。setIdleTime的第一个参数是定义检查空闲的方法,可以是IdleStatus.BOTH_IDLE,IdleStatus.READER_IDLE,IdleStatus.WRITER_IDLE。第二个参数是间隔多长时间没有出现参数一指定的行为后,会话被视为空闲状态。
handler的代码如下:
import java.util.Date;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
public class TimeServerHandler extends IoHandlerAdapter
{
@Override
public void exceptionCaught( IoSession session, Throwable cause ) throws Exception
{
cause.printStackTrace();
}
@Override
public void messageReceived( IoSession session, Object message ) throws Exception
{
String str = message.toString();
if( str.trim().equalsIgnoreCase("quit") ) {
session.close();
return;
}
Date date = new Date();
session.write( date.toString() );
System.out.println("Message written...");
}
@Override
public void sessionIdle( IoSession session, IdleStatus status ) throws Exception
{
System.out.println( "IDLE " + session.getIdleCount( status ));
}
}
在这个类中我们使用了exceptionCaught, messageReceived 和 sessionIdle方法。实现handler的时候应该总是重写exceptionCaught方法。当在处理远程连接时发生异常而这个方法没有定义的话,异常会被忽略。
在exceptionCaught方法里我们只是简单的打印了异常的stack trace。 在多数情况下都是这样处理, 除非我们需要根据异常情况来执行一些恢复操作。
在messageReceived方法里我们接收到客户端发送过来的数据然后把当前时间写回客户端。如果客户端过来的消息是“quit”这个单词,我们把会话关闭。这个方法还会打印客户端的时间。根据使用的编码协议实现的不同,被传过来的第二个参数也会不一样。同样向session.write(Object)方法里传递的消息对象也要根据编码协议实现来确定。如果不指定任何编码协议实现,你得到的消息对象的类型是IoBuffer,同样你也需要把IoBuffer传递给write方法。
当会话处于空闲状态时每隔一段时间,sessionIdle方法会被调用一次。这个时间是通过acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );.来指定的。
剩下的事情就是指定服务器的监听的socket地址,然后使服务器运行起来。代码如下:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
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;
public class MinaTimeServer
{
private static final int PORT = 9123;
public static void main( String[] args ) throws IOException
{
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
acceptor.setHandler( new TimeServerHandler() );
acceptor.getSessionConfig().setReadBufferSize( 2048 );
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
acceptor.bind( new InetSocketAddress(PORT) );
}
}
试一下时间服务器
现在我们继续编译和执行这个程序。测试服务器最简单的办法就是启动服务器然后用telnet连接服务器。
客户端输出:
user@myhost:~> telnet 127.0.0.1 9123
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
hello
Wed Oct 17 23:23:36 EDT 2007
quit
Connection closed by foreign host.
user@myhost:~>
服务器输出:
MINA Time server started.
Message written...
TCP客户端的例子
我们已经了解过客户端的架构,下面我们来看一个客户端实现的例子。 我们用Sumup客户端作为参考例子。
我们去除了包引用等常规代码,以便专注于重要实现部分。代码如下:
public static void main(String[] args) throws Throwable {
NioSocketConnector connector = new NioSocketConnector();
connector.setConnectTimeoutMillis(CONNECT_TIMEOUT);
if (USE_CUSTOM_CODEC) {
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new SumUpProtocolCodecFactory(false)));
} else {
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
}
connector.getFilterChain().addLast("logger", new LoggingFilter());
connector.setHandler(new ClientSessionHandler(values));
IoSession session;
for (;;) {
try {
ConnectFuture future = connector.connect(new InetSocketAddress(HOSTNAME, PORT));
future.awaitUninterruptibly();
session = future.getSession();
break;
} catch (RuntimeIoException e) {
System.err.println("Failed to connect.");
e.printStackTrace();
Thread.sleep(5000);
}
}
// wait until the summation is done
session.getCloseFuture().awaitUninterruptibly();
connector.dispose();
}
要创建一个客户端,你需要做如下事情:
- 创建一个Connector(连接器)
- 创建一个FilterChain(过滤器链)
- 创建一个IOHandler并添加到Connector
- 绑定服务器
我们来看看每一步的细节:
创建一个Connector(连接器)
NioSocketConnector connector = new NioSocketConnector();
这里我们创建一个NioSocketConnector实例
创建一个FilterChain(过滤器链)
if (USE_CUSTOM_CODEC) {
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new SumUpProtocolCodecFactory(false)));
} else {
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
}
我们向FilterChain添加了一个过滤器ProtocolCodec
创建一个IOHandler并添加到Connector
connector.setHandler(new ClientSessionHandler(values));
这里我们创建了一个ClientSessionHandler并且设置到Connector里
绑定服务器
IoSession session;
for (;;) {
try {
ConnectFuture future = connector.connect(new InetSocketAddress(HOSTNAME, PORT));
future.awaitUninterruptibly();
session = future.getSession();
break;
} catch (RuntimeIoException e) {
System.err.println("Failed to connect.");
e.printStackTrace();
Thread.sleep(5000);
}
}
这里是最重要的部分,我们连接到远程服务器。因为连接是异步处理,我们使用ConnectFuture来确定连接已经创建完成。一旦连接建立,我们得到连接对应的IoSession对象。要向服务器发送消息,我们需要调用IoSession的write方法。所有从服务器过来的消息会经过FilterChain然后最终到达IoHandler
UDP服务器的例子
我们从 org.apache.mina.example.udp包里的例子代码开始,为了方便,我们主要看一下跟MINA相关的部分。
要创建一个服务器,我们需要做一下事情:
- 创建一个Datagram Socket监听客户端的请求(例如 MemoryMonitor.java)
- 创建一个IoHandler来处理MINA框架产生的时间(例如 MemoryMonitorHandler.java)
下面是第一步的代码片段
NioDatagramAcceptor acceptor = new NioDatagramAcceptor();
acceptor.setHandler(new MemoryMonitorHandler(this));
在这里我们创建了一个NioDatagramAcceptor实例来监听客户端的请求,然后设置IoHandler。下一步是向FilterChain里添加一个日志过滤器LoggingFilter。LoggingFilter是一个查看MINA行为的好方法。它会在各个阶段输出日志,可以让你了解MINA是如何工作的。
DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
chain.addLast("logger", new LoggingFilter());
下一步是关于UDP通信的代码,我们让acceptor重用网络地址。
DatagramSessionConfig dcfg = acceptor.getSessionConfig();
dcfg.setReuseAddress(true);
acceptor.bind(new InetSocketAddress(PORT));
当然最后的事情就是调用bind方法
IoHandler实现
在服务端有三个主要的事件需要处理:
- Session Created - 会话被创建
- Message Received - 接收到消息
- Session Closed - 会话被关闭
创建会话事件
@Override
public void sessionCreated(IoSession session) throws Exception {
SocketAddress remoteAddress = session.getRemoteAddress();
server.addClient(remoteAddress);
}
在创建会话事件处理里,我们调用了addClient方法来向GUI添加一个Tab页。
接收到消息
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
if (message instanceof IoBuffer) {
IoBuffer buffer = (IoBuffer) message;
SocketAddress remoteAddress = session.getRemoteAddress();
server.recvUpdate(remoteAddress, buffer.getLong());
}
}
当接收到消息后我们把消息里数据dump出来。在这个方法里应用程序需要发送响应,处理消息,把响应写到会话里。
会话关闭事件
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("Session closed...");
SocketAddress remoteAddress = session.getRemoteAddress();
server.removeClient(remoteAddress);
}
在会话关闭事件里,我们把会话对应的Tab从GUI上的删除。
UDP客户端的例子
要实现一个客户端的我们需要做以下事情:
- 创建Socket并连接到服务器
- 设置IoHandler
- 收集空闲内存
- 向服务器发送数据
我们先看看org.apache.mina.example.udp.client包的MemMonClient.java代码。有下面这几行代码:
connector = new NioDatagramConnector();
connector.setHandler( this );
ConnectFuture connFuture = connector.connect( new InetSocketAddress("localhost", MemoryMonitor.PORT ));
我们创建了一个NioDatagramConnector,然后设置handler并连接到服务器。一个容易出错的地方是,你必须设置InetSocketAddress的服务器地址,不然连接不到服务器。 然后就是等待服务器的响应,一旦我们连接成功,就开始发送数据到服务器,代码如下:
connFuture.addListener( new IoFutureListener(){
public void operationComplete(IoFuture future) {
ConnectFuture connFuture = (ConnectFuture)future;
if( connFuture.isConnected() ){
session = future.getSession();
try {
sendData();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
log.error("Not connected...exiting");
}
}
});
这里,我们向ConnectFuture添加了一个监听器,当客户端连接成功会得到一个方法回调。然后开始写数据,向服务器写数据的处理在sendData方法中实现,代码如下:
private void sendData() throws InterruptedException {
for (int i = 0; i < 30; i++) {
long free = Runtime.getRuntime().freeMemory();
IoBuffer buffer = IoBuffer.allocate(8);
buffer.putLong(free);
buffer.flip();
session.write(buffer);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
throw new InterruptedException(e.getMessage());
}
}
}
这个方法会每隔30秒向服务器发送客户端的空闲内存大小。在这里我们分配了一个大小可以放下一个long变量的IoBuffer,然后把空闲内存大小放进去。然后这个缓冲区被发送到服务器。
总结
在这一章中我们了解基于MINA的客户端和服务器的应用程序框架,我们也尝试的实现了基于TCP和UDP的客户端/服务器实现。在后面的章节中我们会讨论MINA的核心组件和一些高级主题。