《Apache MINA 2.0 用户指南》第二章:基础知识
在第一章中,我们对 Apache MINA 有了一个基本认识。本章中,我们将继续认识一下客户端/服务器端结构以及规划一个基于 MINA 的服务器或者客户端的详情。
我们也将披露一些很简单的,基于 TCP 和 UDP 的服务器和客户端的例子。
基于 MINA 的应用架构
问的最多的问题:"一个基于 MINA 的应用看起来像什么"?本小节我们将来了解一下基于 MINA 的应用架构。我们收集了一些基于 MINA 的演示信息。
架构鸟瞰图
这里,我们可以看到, MINA 是你的应用程序 (可能是一个客户端应用或者一个服务器端应用) 和基础网络层之间的粘合剂,可以基于 TCP、UDP、in-VM 通信甚至一个客户端的 RS-232C 串行协议。
你要做的仅仅是在 MINA 之上设计你自己的应用实现,而不需要去处理网络层的那些复杂业务。
现在我们再深入细节探讨一下。下图演示了 MINA 内部的更多细节,这正是每个 MINA 组件做的事情:
(上图来自 Emmanuel Lécharny 简报 现实中的 MINA (ApacheCon EU 2009))
概况来讲,基于 MINA 的应用划分为三个层次:
1. 创建一个 I/O service - 从已存在的可用 service (*Acceptor) 中挑选一个或者创建你自己的
2. 创建一个 Filter Chain - 从现有 Filter 中挑选,或者创建一个用于转换请求/响应的自定义 Filter
3. 创建一个 I/O Handler - 处理不同消息时编写具体业务逻辑
具体创建基本就是如此。
接下来我们会对服务器端架构以及客户端架构进行更加深入阅读。
当然, MINA 提供的东东不仅于此,你可能会注意其他的一些方面的内容,比如消息加密/解密,网络配置如何扩大规模,等等... 我们在以后的几章中会对这些方面进一步讨论。
服务器端架构
前面我们披露了基于 MINA 的应用架构。现在我们来关注一下服务器端架构。从根本上说,服务器端监听一个端口以获得连入的请求,将其进行处理然后发送回复。服务器端还会为每个客户端 (无论是基于 TCP 还是基于 UDP 协议的) 创建并维护一个 session,这在第四章 《Apache MINA 2.0 用户指南》第四章:会话 中将进行进一步解释。
只要一个客户端连接到了 MINA 服务器端,我们就要创建一个新的 session 以存放持久化数据。即使协议还没连接上,也要创建这么一个 session。下图演示了 MINA 处理连入连接的过程:
(官网图已挂)
连入消息的处理
现在我们来解释 MINA 对连入消息的处理。
假定 session 已被创建,新连入的消息将导致一个 selector 被唤醒。
客户端架构
前面我们对基于 MINA 的服务端架构有了一个大体认识,现在我们看一下客户端的情况。客户端需要连接到一个服务端,发送消息并处理响应。
TCP 服务器示例
接下来的教程介绍构建基于 MINA 的应用的过程。这个教程介绍的是构建一个时间服务器。本教程需要以下先决条件:
Log4J 1.3 用户:slf4j-api.jar、slf4j-log4j13.jar 和 Log4J 1.3.x
java.util.logging 用户:slf4j-api.jar 和 slf4j-jdk14.jar
重要:请确认你用的是和你的日志框架匹配的 slf4j-*.jar
例如,slf4j-log4j12.jar 和 log4j-1.3.x.jar 一起使用的话,将会发生故障
我们已经在 Windows? 2000 professional 和 linux 之上对程序进行了测试。如果你在运行本程序时遇到任何问题,不要犹豫请 联系我们,我们以通知 MINA 的开发人员。另外,本教程尽量保留了独立的开发环境 (IDE、编辑器...等等)。本教程示例适用于你所喜爱的任何开发环境。篇幅所限,本文省略掉了关于程序的编译命令以及执行步骤。如果你在编译或执行 Java 程序时需要帮助,请参考 Java 教程。
编写 MINA 时间服务器
我们以创建一个叫做 MinaTimeServer.java 的文件开始。初始化代码如下:
这段程序对所有人来说都很简单明了。我们简单定义了一个用于启动程序的 main 方法。现在,我们开始添加组成我们服务器的代码。首先,我们需要一个用于监听连入的连接的对象。因为本程序基于 TCP/IP,我们在程序中添加了 SocketAcceptor。
NioSocketAcceptor 类就绪了,我们继续定义处理类并绑定 NioSocketAcceptor 到一个端口:
如你所见,有一个关于 acceptor.setLocalAddress( new InetSocketAddress(PORT) ); 的调用。这个方法定义了这一服务器要监听到的主机和端口。最后一个方法是 IoAcceptor.bind() 调用。这个方法将会绑定到指定端口并开始处理远程客户端请求。
接下来我们在配置中添加一个过滤器。这个过滤器将会日志记录所有信息,比如 session 的新建、接收到的消息、发送的消息、session 的关闭。接下来的过滤器是一个 ProtocolCodecFilter。这个过滤器将会把二进制或者协议特定的数据翻译为消息对象,反之亦然。我们使用一个现有的 TextLine 工厂因为它将为你处理基于文本的消息 (你无须去编写 codec 部分)。
接下来,我们将定义用于侍服客户端连接和当前时间的请求的处理器。处理器类是一个必须实行 IoHandler 接口的类。对于几乎所有的使用 MINA 的程序,这里都会变成程序的重负载的地方,因为它将侍服所有来自客户端的请求。本文我们将扩展 IoHandlerAdapter 类。这个类遵循了 适配器设计模式,简化了需要为满足在一个类中传递实现了 IoHandler 接口的需求而要编写的代码量。
现在我们对 NioSocketAcceptor 中的配置进行添加。这将允许我们为用于接收客户端连接的 socket 进行socket 特有的设置。
MinaTimeServer 类中新加了两行。这些方法设置了 IoHandler,为 session 设置了输入缓冲区大小以及 idle 属性。指定缓冲区大小以通知底层操作系统为传入的数据分配多少空间。第二行指定了什么时候检查空闲 session。在对 setIdleTime 的调用中,第一个参数定义了再断定 session 是否闲置时要检查的行为,第二个参数定义了在 session 被视为空闲之前以毫秒为单位的时间长度内必须发生。
处理器代码如下所示:
这个类中所用的方法是为 exceptionCaught、messageReceived 和 sessionIdle。exceptionCaught 应该总是在处理器中进行定义,以处理正常的远程连接过程时抛出的异常。如果这一方法没有定义,可能无法正常报告异常。
exceptionCaught 方法将会对错误和 session 关闭的 stack trace 进行简单打印。对于更多的程序,这将是常规,除非处理器能够从异常情况下进行恢复。
messageReceived 方法会从客户端接收数据并将当前时间回写给客户端。如果接收自客户端的消息是单词 "quit",那么当前 session 将被关闭。这一方法也会向客户端打印输出当前时间。取决于你所使用的协议编解码器,传递到这一方法的对象 (第二个参数) 会有所不同,就和你传给 session.write(Object) 方法的对象一样。如果你不定义一个协议编码器,你很可能会接收到一个 IoBuffer 对象,而且被要求写出一个 IoBuffer 对象。
一旦 session 保持空闲状态到达 acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 ); 所定义的时间长度,sessionIdle 方法会被调用。
剩下的工作就是定义服务器端将要监听的套接字地址,并进行启动服务的调用。代码如下所示:
测试时间服务器
现在我们开始对程序进行编译。你编译好程序以后你就可以运行它了,你可以测试将会发生什么。测试程序最简单的方法就是启动这个程序,然后对程序进行 telnet:
接下来是什么?
请访问我们的文档页面以查找更多资源。当然你也可以继续去阅读其他教程。
TCP 客户端示例
在上文中我们已经了解了客户端架构。现在我们将披露一个客户端实现的示例。
我们将使用 Sumup Client 作为一个参考实现。
我们将移除掉样板代码并专注于重要结构上。以下是为客户端代码:
要构建一个客户端,我们需要做以下事情:
这里我们创建了一个 NIO 套接字 connector。
创建一个 Filter Chain
我们为 Connector 的 Filter Chain 添加了一些过滤器。这里我们添加的是一个 ProtocolCodec。
创建 IOHandler
这里我们创建了一个 ClientSessionHandler 的实例并将其设置为 Connector 的处理器。
绑定到服务器
这是最重要的东东。我们将连接到远程服务器。因为是异步连接连接,我们使用了 ConnectFuture 来了解何时连接结束。一旦连接结束,我们将得到相关联的 IoSession。要向服务器端发送任何消息,我们都要写入 session。所有来自服务器端的响应或者消息都将穿越 Filter chain 并最终由 IoHandler 处理。
UDP 服务器端示例
现在我们看一下 org.apache.mina.example.udp 包里的代码。简单起见,我们将只专注于 MINA 相关构建方面的东西。
要构建服务器我们需要做以下事情:
1. 创建一个 Datagram Socket 以监听连入的客户端请求 (参考 MemoryMonitor.java)
2. 创建一个 IoHandler 以处理 MINA 框架生成的事件 (参考 MemoryMonitorHandler.java)
这里是第 1 点提到的一些代码片段:
这里我们创建了一个 NioDatagramAcceptor 以监听连入的客户端请求,并设置了 IoHandler。"PORT" 是一整型变量。下一步将要为这一 DatagramAcceptor 使用的过滤器链添加一个日志过滤器。LoggingFilter 实在 MINA 中表现不错的一个选择。它在不同阶段产生日志事务,以为我们观察 MINA 的工作情况。
接下来我们来看一些更具体的 UDP 传输的代码。我们设置 acceptor 以复用地址:
当然,要做的最后一件事就是调用 bind()。
IoHandler 实现
对于我们服务器实现有三个主要事件:
Session 创建事件
在这个 session 创建事件中,我们调用了 addClient() 方法,它为界面添加了一个选项卡。
Message 收到事件
在这个消息接收到事件中,我们对接收到的消息中的数据进行了处理。需要发送返回的应用,可以在这个方法中处理消息并回写响应。
Session 关闭事件
在 Session 关闭事件中,我们将客户端选项卡从界面中移除。
UDP 客户端示例
前面我们讲了 UDP 服务器端示例,现在我们来看一下与之对应的客户端代码。
客户端的实现需要做的事情:
我们创建了一个 NioDatagramConnector,设置了处理器然后连接到服务器。我曾经落入的一个陷阱是,你必须在 InetSocketAddress 对象中设置主机,否则它将什么也不干。这个例子是在一台 Windows XP 主机上编写并测试,因此在其他环境中可能会有所不同。解析来我们将等待客户端连接到的主机的确认。一旦得知我们已经建立连接,我们就可以开始向服务器端写数据了:
这里我们为 ConnectFuture 对象添加了一个监听者,当我们接收到客户端已建立连接的回调时,我们就可以写数据了。向服务器端写数据将会由一个叫做 sendData 的方法处理。这个方法如下所示:
这个方法将在 30 秒之内的每秒钟向服务器端发送一次空闲内存的数量。在这里你可以看到我们分配了一个足够大的 IoBuffer 来保存一个 long 类型变量,然后将空闲内存的数量放进缓存。缓冲随即写给服务器端。
本章总结
在本章中,我们了解了基于 MINA 的客户端以及服务器端的应用架构。我们还涉及到 TCP 客户端/服务器端、UDP 客户端和服务器端的演示例子。
在接下来的几章中我们将讨论 MINA 的核心结构以及一些高级主题。
原文链接: http://mina.apache.org/mina-project/userguide/ch2-basics/ch2-basics.html。
我们也将披露一些很简单的,基于 TCP 和 UDP 的服务器和客户端的例子。
基于 MINA 的应用架构
问的最多的问题:"一个基于 MINA 的应用看起来像什么"?本小节我们将来了解一下基于 MINA 的应用架构。我们收集了一些基于 MINA 的演示信息。
架构鸟瞰图
这里,我们可以看到, MINA 是你的应用程序 (可能是一个客户端应用或者一个服务器端应用) 和基础网络层之间的粘合剂,可以基于 TCP、UDP、in-VM 通信甚至一个客户端的 RS-232C 串行协议。
你要做的仅仅是在 MINA 之上设计你自己的应用实现,而不需要去处理网络层的那些复杂业务。
现在我们再深入细节探讨一下。下图演示了 MINA 内部的更多细节,这正是每个 MINA 组件做的事情:
(上图来自 Emmanuel Lécharny 简报 现实中的 MINA (ApacheCon EU 2009))
概况来讲,基于 MINA 的应用划分为三个层次:
- I/O Service (I/O 服务) - 具体 I/O 操作
- I/O Filter Chain (I/O 过滤器链) - 将字节过滤/转换为想要的数据结构。反之亦然
- I/O Handler (I/O 处理器) - 这里实现实际的业务逻辑
1. 创建一个 I/O service - 从已存在的可用 service (*Acceptor) 中挑选一个或者创建你自己的
2. 创建一个 Filter Chain - 从现有 Filter 中挑选,或者创建一个用于转换请求/响应的自定义 Filter
3. 创建一个 I/O Handler - 处理不同消息时编写具体业务逻辑
具体创建基本就是如此。
接下来我们会对服务器端架构以及客户端架构进行更加深入阅读。
当然, MINA 提供的东东不仅于此,你可能会注意其他的一些方面的内容,比如消息加密/解密,网络配置如何扩大规模,等等... 我们在以后的几章中会对这些方面进一步讨论。
服务器端架构
前面我们披露了基于 MINA 的应用架构。现在我们来关注一下服务器端架构。从根本上说,服务器端监听一个端口以获得连入的请求,将其进行处理然后发送回复。服务器端还会为每个客户端 (无论是基于 TCP 还是基于 UDP 协议的) 创建并维护一个 session,这在第四章 《Apache MINA 2.0 用户指南》第四章:会话 中将进行进一步解释。
- I/O Acceptor 监听网络以获取连入的连接或者包
- 对于一个新的连接,一个新的 session 会被创建,之后所有来自该 IP 地址/端口号组合的请求会在同一 session 中处理
- 在一个 session 中接收到的所有包,将穿越上图中所示的 Filter Chain (过滤器链)。过滤器可以被用于修正包的内容 (比如转化为对象,添加或者删除信息等等)。对于从原始字节到高层对象的相互转换,PacketEncoder/Decoder 相当有用。
- 包或转化来的对象最终交给 IOHandler。IOHandler 可以用于实现各种具体业务需求。
只要一个客户端连接到了 MINA 服务器端,我们就要创建一个新的 session 以存放持久化数据。即使协议还没连接上,也要创建这么一个 session。下图演示了 MINA 处理连入连接的过程:
(官网图已挂)
连入消息的处理
现在我们来解释 MINA 对连入消息的处理。
假定 session 已被创建,新连入的消息将导致一个 selector 被唤醒。
客户端架构
前面我们对基于 MINA 的服务端架构有了一个大体认识,现在我们看一下客户端的情况。客户端需要连接到一个服务端,发送消息并处理响应。
- 客户端首先创建一个 IOConnector (用以连接套接字的 MINA 结构),开启一个服务器的绑定
- 在连接创建时,一个 session 会被创建并关联到该连接
- 应用或者客户端写入 session,导致数据在穿越 Filter Chain (过滤器链) 后被发送给服务器端
- 所有接收自服务器端的响应或者消息穿越 Filter Chain (过滤器链) 后由 IOHandler 接收并处理
TCP 服务器示例
接下来的教程介绍构建基于 MINA 的应用的过程。这个教程介绍的是构建一个时间服务器。本教程需要以下先决条件:
- MINA 2.x Core
- JDK 1.5 或更高
- SLF4J 1.3.0 或更高
Log4J 1.3 用户:slf4j-api.jar、slf4j-log4j13.jar 和 Log4J 1.3.x
java.util.logging 用户:slf4j-api.jar 和 slf4j-jdk14.jar
重要:请确认你用的是和你的日志框架匹配的 slf4j-*.jar
例如,slf4j-log4j12.jar 和 log4j-1.3.x.jar 一起使用的话,将会发生故障
我们已经在 Windows? 2000 professional 和 linux 之上对程序进行了测试。如果你在运行本程序时遇到任何问题,不要犹豫请 联系我们,我们以通知 MINA 的开发人员。另外,本教程尽量保留了独立的开发环境 (IDE、编辑器...等等)。本教程示例适用于你所喜爱的任何开发环境。篇幅所限,本文省略掉了关于程序的编译命令以及执行步骤。如果你在编译或执行 Java 程序时需要帮助,请参考 Java 教程。
编写 MINA 时间服务器
我们以创建一个叫做 MinaTimeServer.java 的文件开始。初始化代码如下:
public class MinaTimeServer {
public static void main(String[] args) {
// code will go here next
}
}
这段程序对所有人来说都很简单明了。我们简单定义了一个用于启动程序的 main 方法。现在,我们开始添加组成我们服务器的代码。首先,我们需要一个用于监听连入的连接的对象。因为本程序基于 TCP/IP,我们在程序中添加了 SocketAcceptor。
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
public class MinaTimeServer
{
public static void main( String[] args )
{
IoAcceptor acceptor = new NioSocketAcceptor();
}
}
NioSocketAcceptor 类就绪了,我们继续定义处理类并绑定 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) );
}
}
如你所见,有一个关于 acceptor.setLocalAddress( new InetSocketAddress(PORT) ); 的调用。这个方法定义了这一服务器要监听到的主机和端口。最后一个方法是 IoAcceptor.bind() 调用。这个方法将会绑定到指定端口并开始处理远程客户端请求。
接下来我们在配置中添加一个过滤器。这个过滤器将会日志记录所有信息,比如 session 的新建、接收到的消息、发送的消息、session 的关闭。接下来的过滤器是一个 ProtocolCodecFilter。这个过滤器将会把二进制或者协议特定的数据翻译为消息对象,反之亦然。我们使用一个现有的 TextLine 工厂因为它将为你处理基于文本的消息 (你无须去编写 codec 部分)。
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) );
}
}
接下来,我们将定义用于侍服客户端连接和当前时间的请求的处理器。处理器类是一个必须实行 IoHandler 接口的类。对于几乎所有的使用 MINA 的程序,这里都会变成程序的重负载的地方,因为它将侍服所有来自客户端的请求。本文我们将扩展 IoHandlerAdapter 类。这个类遵循了 适配器设计模式,简化了需要为满足在一个类中传递实现了 IoHandler 接口的需求而要编写的代码量。
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) );
}
}
现在我们对 NioSocketAcceptor 中的配置进行添加。这将允许我们为用于接收客户端连接的 socket 进行socket 特有的设置。
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) );
}
}
MinaTimeServer 类中新加了两行。这些方法设置了 IoHandler,为 session 设置了输入缓冲区大小以及 idle 属性。指定缓冲区大小以通知底层操作系统为传入的数据分配多少空间。第二行指定了什么时候检查空闲 session。在对 setIdleTime 的调用中,第一个参数定义了再断定 session 是否闲置时要检查的行为,第二个参数定义了在 session 被视为空闲之前以毫秒为单位的时间长度内必须发生。
处理器代码如下所示:
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。exceptionCaught 应该总是在处理器中进行定义,以处理正常的远程连接过程时抛出的异常。如果这一方法没有定义,可能无法正常报告异常。
exceptionCaught 方法将会对错误和 session 关闭的 stack trace 进行简单打印。对于更多的程序,这将是常规,除非处理器能够从异常情况下进行恢复。
messageReceived 方法会从客户端接收数据并将当前时间回写给客户端。如果接收自客户端的消息是单词 "quit",那么当前 session 将被关闭。这一方法也会向客户端打印输出当前时间。取决于你所使用的协议编解码器,传递到这一方法的对象 (第二个参数) 会有所不同,就和你传给 session.write(Object) 方法的对象一样。如果你不定义一个协议编码器,你很可能会接收到一个 IoBuffer 对象,而且被要求写出一个 IoBuffer 对象。
一旦 session 保持空闲状态到达 acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 ); 所定义的时间长度,sessionIdle 方法会被调用。
剩下的工作就是定义服务器端将要监听的套接字地址,并进行启动服务的调用。代码如下所示:
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 Client 作为一个参考实现。
我们将移除掉样板代码并专注于重要结构上。以下是为客户端代码:
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
- 创建一个 Filter Chain
- 创建一个 IOHandler 并添加到 Connector
- 绑定到服务器
NioSocketConnector connector = new NioSocketConnector();
这里我们创建了一个 NIO 套接字 connector。
创建一个 Filter Chain
if (USE_CUSTOM_CODEC) {
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new SumUpProtocolCodecFactory(false)));
} else {
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
}
我们为 Connector 的 Filter Chain 添加了一些过滤器。这里我们添加的是一个 ProtocolCodec。
创建 IOHandler
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。要向服务器端发送任何消息,我们都要写入 session。所有来自服务器端的响应或者消息都将穿越 Filter chain 并最终由 IoHandler 处理。
UDP 服务器端示例
现在我们看一下 org.apache.mina.example.udp 包里的代码。简单起见,我们将只专注于 MINA 相关构建方面的东西。
要构建服务器我们需要做以下事情:
1. 创建一个 Datagram Socket 以监听连入的客户端请求 (参考 MemoryMonitor.java)
2. 创建一个 IoHandler 以处理 MINA 框架生成的事件 (参考 MemoryMonitorHandler.java)
这里是第 1 点提到的一些代码片段:
NioDatagramAcceptor acceptor = new NioDatagramAcceptor();
acceptor.setHandler(new MemoryMonitorHandler(this));
这里我们创建了一个 NioDatagramAcceptor 以监听连入的客户端请求,并设置了 IoHandler。"PORT" 是一整型变量。下一步将要为这一 DatagramAcceptor 使用的过滤器链添加一个日志过滤器。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 创建
- Message 接收
- Session 关闭
Session 创建事件
@Override
public void sessionCreated(IoSession session) throws Exception {
SocketAddress remoteAddress = session.getRemoteAddress();
server.addClient(remoteAddress);
}
在这个 session 创建事件中,我们调用了 addClient() 方法,它为界面添加了一个选项卡。
Message 收到事件
@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());
}
}
在这个消息接收到事件中,我们对接收到的消息中的数据进行了处理。需要发送返回的应用,可以在这个方法中处理消息并回写响应。
Session 关闭事件
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("Session closed...");
SocketAddress remoteAddress = session.getRemoteAddress();
server.removeClient(remoteAddress);
}
在 Session 关闭事件中,我们将客户端选项卡从界面中移除。
UDP 客户端示例
前面我们讲了 UDP 服务器端示例,现在我们来看一下与之对应的客户端代码。
客户端的实现需要做的事情:
- 创建套接字并连接到服务器端
- 设置 IoHandler
- 收集空闲内存
- 发送数据到服务器端
connector = new NioDatagramConnector();
connector.setHandler( this );
ConnectFuture connFuture = connector.connect( new InetSocketAddress("localhost", MemoryMonitor.PORT ));
我们创建了一个 NioDatagramConnector,设置了处理器然后连接到服务器。我曾经落入的一个陷阱是,你必须在 InetSocketAddress 对象中设置主机,否则它将什么也不干。这个例子是在一台 Windows XP 主机上编写并测试,因此在其他环境中可能会有所不同。解析来我们将等待客户端连接到的主机的确认。一旦得知我们已经建立连接,我们就可以开始向服务器端写数据了:
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 秒之内的每秒钟向服务器端发送一次空闲内存的数量。在这里你可以看到我们分配了一个足够大的 IoBuffer 来保存一个 long 类型变量,然后将空闲内存的数量放进缓存。缓冲随即写给服务器端。
本章总结
在本章中,我们了解了基于 MINA 的客户端以及服务器端的应用架构。我们还涉及到 TCP 客户端/服务器端、UDP 客户端和服务器端的演示例子。
在接下来的几章中我们将讨论 MINA 的核心结构以及一些高级主题。
原文链接: http://mina.apache.org/mina-project/userguide/ch2-basics/ch2-basics.html。