使用 Apache MINA 2 开发网络应用
简介: Apache MINA 2 是一个开发高性能和高可伸缩性网络应用程序的网络应用框架。它提供了一个抽象的事件驱动的异步 API,可以使用 TCP/IP、UDP/IP、串口和虚拟机内部的管道等传输方式。Apache MINA 2 可以作为开发网络应用程序的一个良好基础。本文将介绍 Apache MINA 2 的基本概念和 API,包括 I/O 服务、I/O 会话、I/O 过滤器和 I/O 处理器。另外还将介绍如何使用状态机。本文包含简单的计算器服务和复杂的联机游戏两个示例应用。
Apache MINA 2 是一个开发高性能和高可伸缩性网络应用程序的网络应用框架。它提供了一个抽象的事件驱动的异步 API,可以使用 TCP/IP、UDP/IP、串口和虚拟机内部的管道等传输方式。Apache MINA 2 可以作为开发网络应用程序的一个良好基础。下面将首先简单介绍一下 Apache MINA 2。
Apache MINA 2 介绍
Apache MINA 是 Apache 基金会的一个开源项目,目前最新的版本是 2.0.0-RC1。本文中使用的版本是 2.0.0-M6。从 参考资料 中可以找到相关的下载信息。下面首先介绍基于 Apache MINA 的网络应用的一般架构。
基于 Apache MINA 的网络应用的架构
基于 Apache MINA 开发的网络应用,有着相似的架构。图 1 中给出了架构的示意图。
图 1. 基于 Apache MINA 的网络应用的架构
如 图 1 所示,基于 Apache MINA 的网络应用有三个层次,分别是 I/O 服务、I/O 过滤器和 I/O 处理器:
I/O 服务:I/O 服务用来执行实际的 I/O 操作。Apache MINA 已经提供了一系列支持不同协议的 I/O 服务,如 TCP/IP、UDP/IP、串口和虚拟机内部的管道等。开发人员也可以实现自己的 I/O 服务。
I/O 过滤器:I/O 服务能够传输的是字节流,而上层应用需要的是特定的对象与数据结构。I/O 过滤器用来完成这两者之间的转换。I/O 过滤器的另外一个重要作用是对输入输出的数据进行处理,满足横切的需求。多个 I/O 过滤器串联起来,形成 I/O 过滤器链。
I/O 处理器:I/O 处理器用来执行具体的业务逻辑。对接收到的消息执行特定的处理。
创建一个完整的基于 Apache MINA 的网络应用,需要分别构建这三个层次。Apache MINA 已经为 I/O 服务和 I/O 过滤器提供了不少的实现,因此这两个层次在大多数情况下可以使用已有的实现。I/O 处理器由于是与具体的业务相关的,一般来说都是需要自己来实现的。
事件驱动的 API
Apache MINA 提供的是事件驱动的 API。它把与网络相关的各种活动抽象成事件。网络应用只需要对其感兴趣的事件进行处理即可。事件驱动的 API 使得基于 Apache MINA 开发网络应用变得比较简单。应用不需要考虑与底层传输相关的具体细节,而只需要处理抽象的 I/O 事件。比如在实现一个服务端应用的时候,如果有新的连接进来,I/O 服务会产生 sessionOpened这样一个事件。如果该应用需要在有连接打开的时候,执行某些特定的操作,只需要在 I/O 处理器中此事件处理方法 sessionOpened中添加相应的代码即可。
在介绍 Apache MINA 中的基本概念的细节之前,首先通过一个简单的应用来熟悉上面提到的三个层次的具体职责。
从简单应用开始
在使用 Apache MINA 开发复杂的应用之前,首先将介绍一个简单的应用。通过此应用可以熟悉上面提到的三个层次,即 I/O 服务、I/O 过滤器和 I/O 处理器。该应用是一个简单的计算器服务,客户端发送要计算的表达式给服务器,服务器返回计算结果。比如客户端发送 2+2,服务器返回 4.0作为结果。
在实现此计算器的时候,首先需要考虑的是 I/O 服务。该计算器使用 TCP/IP 协议,需要在指定端口监听,接受客户端的连接。Apache MINA 提供了基于 Java NIO 的套接字实现,可以直接使用。其次要考虑的是 I/O 过滤器。I/O 过滤器过滤所有的 I/O 事件和请求,可以用来处理横切的需求,如记录日志、压缩等。最后就是 I/O 处理器。I/O 处理器用来处理业务逻辑。具体到该应用来说,就是在接收到消息之后,把该消息作为一个表达式来执行,并把结果发送回去。I/O 处理器需要实现 org.apache.mina.core.service.IoHandler接口或者继承自 org.apache.mina.core.service.IoHandlerAdapter。该应用的 I/O 处理器的实现如 清单 1 所示。
清单 1. 计算器服务的 I/O 处理器 CalculatorHandler
清单 2 中,首先创建一个 org.apache.mina.transport.socket.nio.NioSocketAcceptor 的实例,由它提供 I/O 服务;接着获得该 I/O 服务的过滤器链,并添加两个新的过滤器,一个用来记录相关日志,另外一个用来在字节流和文本之间进行转换;最后配置 I/O 处理器。完成这些之后,通过 bind 方法来在特定的端口进行监听,接收连接。服务器启动之后,可以通过操作系统自带的 Telnet 工具来进行测试,如 图 2 所示。在输入表达式之后,计算结果会出现在下面一行。
图 2. 使用 Telnet 工具测试计算器服务
在介绍了简单的计算器服务这个应用之后,下面说明本文中会使用的复杂的联机游戏应用。
联机游戏示例说明
上一节中给出了一个简单的基于 Apache MINA 的网络应用的实现,可以用来熟悉基本的架构。而在实际开发中,网络应用都是有一定复杂度的。下面会以一个比较复杂的联机游戏作为示例来详细介绍 Apache MINA 的概念、API 和典型用法。
该联机游戏支持两个人进行俄罗斯方块的对战。这个游戏借鉴了 QQ 的“火拼俄罗斯”。用户在启动客户端之后,需要输入一个昵称进行注册。用户可以在“游戏大厅”中查看当前已注册的所有其它用户。当前用户可以选择另外的一个用户发送游戏邀请。邀请被接受之后就可以开始进行对战。在游戏过程中,当前用户可以看到对方的游戏状态,即方块的情况。该游戏的运行效果如 图 3 所示。
图 3. 联机游戏示例运行效果图
下面开始以这个应用为例来具体介绍 Apache MINA 中的基本概念。先从 I/O 服务开始。
I/O 服务
I/O 服务用来执行真正的 I/O 操作,以及管理 I/O 会话。根据所使用的数据传输方式的不同,有不同的 I/O 服务的实现。由于 I/O 服务执行的是输入和输出两种操作,实际上有两种具体的子类型。一种称为“I/O 接受器(I/O acceptor)”,用来接受连接,一般用在服务器的实现中;另外一种称为“I/O 连接器(I/O connector)”,用来发起连接,一般用在客户端的实现中。对应在 Apache MINA 中的实现,org.apache.mina.core.service.IoService是 I/O 服务的接口,而继承自它的接口 org.apache.mina.core.service.IoAcceptor 和 org.apache.mina.core.service.IoConnector 则分别表示 I/O 接受器和 I/O 连接器。IoService 接口提供的重要方法如 表 1 所示。
表 1. IoService 中的重要方法
方法说明
setHandler(IoHandler handler)设置 I/O 处理器。该 I/O 处理器会负责处理该 I/O 服务所管理的所有 I/O 会话产生的 I/O 事件。
getFilterChain()获取 I/O 过滤器链,可以对 I/O 过滤器进行管理,包括添加和删除 I/O 过滤器。
getManagedSessions()获取该 I/O 服务所管理的 I/O 会话。
下面具体介绍 I/O 接受器和 I/O 连接器。
I/O 接受器
I/O 接受器用来接受连接,与对等体(客户端)进行通讯,并发出相应的 I/O 事件交给 I/O 处理器来处理。使用 I/O 接受器的时候,只需要调用 bind方法并指定要监听的套接字地址。当不再接受连接的时候,调用 unbind停止监听即可。关于 I/O 接受器的具体用法,可以参考 清单 2 中给出的计算器服务的实现。
I/O 连接器
I/O 连接器用来发起连接,与对等体(服务器)进行通讯,并发出相应的 I/O 事件交给 I/O 处理器来处理。使用 I/O 连接器的时候,只需要调用 connect方法连接指定的套接字地址。另外可以通过 setConnectTimeoutMillis设置连接超时时间(毫秒数)。
清单 3 中给出了使用 I/O 连接器的一个示例。
清单 3. I/O 连接器示例
AbstractTetrisCommand用来描述联机游戏示例应用中的消息。它是一个抽象类,是所有具体消息的基类。其具体实现如 清单 5 所示。
清单 5. 联机游戏示例应用中的消息 AbstractTetrisCommand
客户端初始化的时候,其状态为“未连接”,表示客户端还没有在服务器上面注册,此时还不能进行游戏;接着用户需要输入一个昵称来注册到服务器上面,完成之后状态迁移到“闲置”。此时客户端会接收到当前在线的所有其它用户的列表。当前用户可以邀请其它用户和他一块游戏,也可以接收来自其它用户的邀请。邀请发送出去之后,客户端的状态迁移到“邀请已发送”。如果接受了其它用户的邀请,客户端的状态迁移到“邀请已接收”。如果某个用户的邀请被另外一个用户接受的话,两个客户端的状态都会迁移到“游戏中”。
要实现这样较为复杂的状态机的话,只需要在 I/O 处理器中以声明式的方式定义状态和迁移条件就可以了。首先需要声明状态机中状态,如 清单 8 所示。
清单 8. 联机游戏示例应用中的状态声明
@State public static final String ROOT = "Root";
@State(ROOT) public static final String NOT_CONNECTED = "NotConnected";
@State(ROOT) public static final String IDLE = "Idle";
@State(ROOT) public static final String INVITATION_SENT = "InvitationSent";
@State(ROOT) public static final String INVITATION_ACCEPTED = "InvitationAccepted";
@State(ROOT) public static final String PLAYING = "Playing";
如 清单 8 所示,上面定义了一共六个状态。通过标注 @State就声明了一个状态。需要注意的是状态之间是可以继承的。如果状态机接收到一个事件的时候,在当前状态中找不到对应的迁移,就会在其父状态上继续查找。状态的继承在某些情况下是很有用的,比如希望为所有的状态都增加同样的迁移逻辑,就可以直接把迁移条件添加在父状态上面。一个典型的场景就是错误处理,一般来说,所有的状态都需要错误处理,而错误处理的逻辑一般都是相同的。把发生错误时候的迁移放在父状态中,可以简洁的描述这一场景。
定义了状态之后,下面应该声明状态之间的迁移。如 清单 9 所示。
清单 9. 联机游戏示例应用中的状态迁移声明
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = NOT_CONNECTED, next = IDLE)
public void login(TetrisServerContext context, IoSession session, LoginCommand cmd) {
String nickName = cmd.getNickName();
context.nickName = nickName;
session.setAttribute("nickname", nickName);
session.setAttribute("status", UserStatus.IDLE);
sessions.add(session);
users.add(nickName);
RefreshPlayersListCommand command = createRefreshPlayersListCommand();
broadcast(command);
}
@IoHandlerTransition(on = EXCEPTION_CAUGHT, in = ROOT, weight = 10)
public void exceptionCaught(IoSession session, Exception e) {
LOGGER.warn("Unexpected error.", e);
session.close(true);
}
@IoHandlerTransition(in = ROOT, weight = 100)
public void unhandledEvent() {
LOGGER.warn("Unhandled event.");
}
清单 9 中,使用标注 @IoHandlerTransition声明了一个状态迁移。每个状态迁移可以有四个属性:on、in、next和 weight,其中属性 in是必须的,其余是可选的。属性 on表示触发此状态迁移的事件名称,如果省略该属性的话,则默认为匹配所有事件的通配符。该属性的值可以是表中给出的 I/O 处理器中能处理的七种事件类型。属性 in表示状态迁移的起始状态。属性 next表示状态迁移的结束状态,如果省略该属性的话,则默认为表示当前状态 的 _self_。属性 weight用来指明状态迁移的权重。一个状态的所有迁移是按照其权重升序排列的。对于当前状态,如果有多个可能的迁移,排序靠前的迁移将会发生。代码中的第一个标注声明了如果当前状态是“未连接”,并且接收到了 MESSAGE_RECEIVED事件,而且消息的内容是一个 LoginCommand对象的话,login方法会被调用,调用完成之后,当前状态迁移到“闲置”。第二个标注声明了对于任何的状态,如果接收到了 EXCEPTION_CAUGHT事件,exceptionCaught方法会被调用。最后一个标注声明了一个状态迁移,其起始状态是 ROOT,表示该迁移对所有的事件都起作用。不过它的 weight是 100,优先级比较低。该状态迁移的作用是处理其它没有对应状态迁移的事件。
使用了 Apache MINA 提供的状态机之后,创建 I/O 处理器的方式也发生了变化。I/O 处理器的实例由状态机来创建,如 清单 10 所示。
清单 10. 在状态机中创建 I/O 处理器
private static IoHandler createIoHandler() {
StateMachine sm = StateMachineFactory.getInstance(
IoHandlerTransition.class).create(ServerHandler.NOT_CONNECTED,
new ServerHandler());
return new StateMachineProxyBuilder().setStateContextLookup(
new IoSessionStateContextLookup(new StateContextFactory() {
public StateContext create() {
return new ServerHandler.TetrisServerContext();
}
})).create(IoHandler.class, sm);
}
在 清单 10 中,TetrisServerContext是提供给状态机的上下文对象,用来在状态之间共享数据。当然用 IoSession也是可以实现的,不过上下文对象的好处是类型安全,不需要做额外的类型转换。
在介绍完状态机后,下面介绍一些高级话题,包括异步操作以及 Apache MINA 与 JMX 和 Spring 的集成。高级话题
在前面章节中介绍了 Apache MINA 的基本概念和具体用法,下面讨论一些高级话题。
异步操作
Apache MINA 中的很多操作都是异步的,比如连接的建立、连接的关闭、还有数据的发送等。在编写网络应用的时候,需要考虑这一点。比如 IoConnector的 connect方法,其返回值是 org.apache.mina.core.future.ConnectFuture类的对象。通过此对象,可以查询连接操作的状态。清单 3 中已经使用了 ConnectFuture。另外一个常用的是发送数据时使用的 org.apache.mina.core.future.WriteFuture,如 清单 11 所示。
清单 11. WriteFuture 的使用
IoSession session = ...; // 获取 I/O 会话对象
WriteFuture future = session.write("Hello World"); // 发送数据
future.awaitUninterruptibly(); // 等待发送数据操作完成
if(future.isWritten())
{
// 数据已经被成功发送
}
else
{
// 数据发送失败
}
由于这样的需求很常见,I/O 处理器中提供了 messageSent方法,当数据发送成功的时候,该方法会被调用。
JMX 集成
Apache MINA 可以集成 JMX 来对网络应用进行管理和监测。下面通过对前面给出的计算器服务进行简单修改,来说明如何集成 JMX。所需的修改如 清单 12 所示。
清单 12. Apache MINA 与 JMX 的集成如 清单 12 所示,首先获取平台提供的受控 bean 的服务器,接着创建受控 bean(MBean)来包装想要管理和监测的对象,这里使用的是 I/O 连接器对象。最后把创建出来的受控 bean 注册到服务器即可。
在启动计算器服务应用的时候,添加下面的启动参数:-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8084 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false,就启用了 JMX。接着通过 JVM 提供的“Java 监视和管理控制台”(运行 jconsole)就可以连接到此应用进行管理和监测了。监测的结果如 图 6 所示。
图 6. 通过“Java 监视和管理控制台”管理和监测基于 Apache MINA 的应用Spring 集成
Apache MINA 可以和流行的开源框架 Spring 进行集成,由 Spring 来管理 Apache MINA 中的对象。与 Spring 集成的方式也比较简单,只需要编写相应的 Spring 配置文件即可。清单 13 中给出了与 Spring 集成之后的计算器服务的配置文件。
清单 13. Apache MINA 与 Spring 集成的配置文件清单 13 中创建 I/O 处理器和 I/O 过滤器的方式很直接。由于不能直接从 I/O 接受器获取过滤器链,这里创建了一个 org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder类的 bean,用来构建过滤器链。由 Apache MINA 提供的网络地址编辑器 org.apache.mina.integration.beans.InetSocketAddressEditor允许以“主机名 : 端口”的形式指定网络地址。在声明 I/O 接受器的时候,通过 init-method指明了当 I/O 接受器创建成功之后,调用其 bind方法来接受连接;通过 destroy-method声明了当其被销毁的时候,调用其 unbind来停止监听。总结
Apache MINA 是一个很好的网络应用框架,它通过事件驱动的 API 为开发网络应用提供了良好的架构基础,同时也提供了丰富的 I/O 服务和 I/O 过滤器的实现,使得开发网络应用变得简单。本文详细介绍了 Apache MINA 中的基本概念,包括 I/O 服务、I/O 会话、I/O 过滤器和 I/O 处理器等,同时介绍了如何利用状态机来实现逻辑复杂的 I/O 处理器。除此之外,还讨论了 Apache MINA 如何与 JMX 和 Spring 进行集成。本文提供了一个简单的计算器服务和复杂的俄罗斯方块联机游戏作为示例,可以帮助读者更好的掌握基于 Apache MINA 的网络应用开发。


Spring 集成