在介绍完示例应用中的消息格式之后,下面将讨论具体的“编码”和“解码”过程。“编码”过程由编码器来完成,编码器需要实现org.apache.mina.filter.codec.ProtocolEncoder
接口,一般来说继承自 org.apache.mina.filter.codec.ProtocolEncoderAdapter
并覆写所需的方法即可。清单 6 中给出了示例应用中消息编码器 CommandEncoder
的实现。
清单 6. 联机游戏示例应用中消息编码器 CommandEncoder
public class CommandEncoder extends ProtocolEncoderAdapter {
public void encode(IoSession session, Object message,
ProtocolEncoderOutput out) throws Exception {
AbstractTetrisCommand command = (AbstractTetrisCommand) message;
byte[] bytes = command.toBytes();
IoBuffer buf = IoBuffer.allocate(bytes.length, false);
buf.setAutoExpand(true);
buf.putInt(bytes.length);
buf.put(bytes);
buf.flip();
out.write(buf);
}
}
|
在 清单 6 中,encode
方法封装了编码的逻辑。由于 AbstractTetrisCommand
的 toBytes
已经完成了到字节数组的转换,encode
方法直接使用即可。首先写入消息主体字节数组的长度,再是字节数组本身,就完成了编码的过程。
与编码过程相比,解码过程要相对复杂一些。具体的实现如 清单 7 所示。
清单 7. 联机游戏示例应用中消息解码器 CommandDecoder
public class CommandDecoder extends CumulativeProtocolDecoder {
protected boolean doDecode(IoSession session, IoBuffer in,
ProtocolDecoderOutput out) throws Exception {
if (in.prefixedDataAvailable(4, Constants.MAX_COMMAND_LENGTH)) {
int length = in.getInt();
byte[] bytes = new byte[length];
in.get(bytes);
int commandNameLength = Constants.COMMAND_NAME_LENGTH;
byte[] cmdNameBytes = new byte[commandNameLength];
System.arraycopy(bytes, 0, cmdNameBytes, 0, commandNameLength);
String cmdName = StringUtils.trim(new String(cmdNameBytes));
AbstractTetrisCommand command = TetrisCommandFactory
.newCommand(cmdName);
if (command != null) {
byte[] cmdBodyBytes = new byte[length - commandNameLength];
System.arraycopy(bytes, commandNameLength, cmdBodyBytes, 0,
length - commandNameLength);
command.bodyFromBytes(cmdBodyBytes);
out.write(command);
}
return true;
} else {
return false;
}
}
}
|
在 清单 7 中可以看到,解码器 CommandDecoder
继承自 CumulativeProtocolDecoder
。这是 Apache MINA 提供的一个帮助类,它会自动缓存所有已经接收到的数据,直到编码器认为可以开始进行编码。这样在实现自己的编码器的时候,就只需要考虑如何判断消息的边界即可。如果一条消息的后续数据还没有接收到,CumulativeProtocolDecoder
会自动进行缓存。在之前提到过,解码过程的一个重要问题是判断消息的边界。对于固定长度的消息来说,只需要使用 Apache MINA 的IoBuffer
的 remaining
方法来判断当前缓存中的字节数目,如果大于消息长度的话,就进行解码;对于使用固定长度消息头来指明消息主体的长度的情况,IoBuffer
提供了prefixedDataAvailable
方法来满足这一需求。prefixedDataAvailable
会检查当前缓存中是否有固定长度的消息头,并且由此消息头指定长度的消息主体是否已经全部在缓存中。如果这两个条件都满足的话,说明一条完整的消息已经接收到,可以进行解码了。解码的过程本身并不复杂,首先读取消息的类别名称,然后通过TetrisCommandFactory.newCommand
方法来生成一个该类消息的实例,接着通过该实例的 bodyFromBytes
方法就可以从字节数组中恢复消息的内容,得到一个完整的消息对象。每次成功解码一个消息对象,需要调用 ProtocolDecoderOutput
的 write
把此消息对象往后传递。消息对象会通过过滤器链,最终达到 I/O 处理器,在IoHandler.messageReceived
中接收到此消息对象。如果当前缓存的数据不足以用来解码一条消息的话,doDecode
只需要返回 false
即可。接收到新的数据之后,doDecode
会被再次调用。
过滤器链
过滤器只有在添加到过滤器链中的时候才起作用。过滤器链是过滤器的容器。过滤器链与 I/O 会话是一一对应的关系。org.apache.mina.core.filterchain.IoFilterChain
是 Apache MINA 中过滤器链的接口,其中提供了一系列方法对其中包含的过滤器进行操作,包括查询、添加、删除和替换等。如 表 5 所示。
表 5. IoFilterChain 接口的方法
方法
说明
addFirst(String name, IoFilter filter) |
将指定名称的过滤器添加到过滤器链的开头。 |
addLast(String name, IoFilter filter) |
将指定名称的过滤器添加到过滤器链的末尾。 |
contains(String name) |
判断过滤器链中是否包含指定名称的过滤器。 |
get(String name) |
从过滤器链中获取指定名称的过滤器。 |
remove(String name) |
从过滤器链中删除指定名称的过滤器。 |
replace(String name, IoFilter newFilter) |
用过滤器 newFilter 替换掉过滤器链中名为 name 的过滤器。 |
getSession() |
获取与过滤器链一一对应的 I/O 会话。 |
在介绍完 I/O 过滤器和过滤器链之后,下面介绍 I/O 处理器。
I/O 处理器
I/O 事件通过过滤器链之后会到达 I/O 处理器。I/O 处理器中与 I/O 事件对应的方法会被调用。Apache MINA 中 org.apache.mina.core.service.IoHandler
是 I/O 处理器要实现的接口,一般情况下,只需要继承自 org.apache.mina.core.service.IoHandlerAdapter
并覆写所需方法即可。IoHandler
接口的方法如 表 6 所示。
表 6. IoHandler 接口的方法
方法
说明
sessionCreated(IoSession session) |
当有新的连接建立的时候,该方法被调用。 |
sessionOpened(IoSession session) |
当有新的连接打开的时候,该方法被调用。该方法在 sessionCreated 之后被调用。 |
sessionClosed(IoSession session) |
当连接被关闭的时候,此方法被调用。 |
sessionIdle(IoSession session, IdleStatus status) |
当连接变成闲置状态的时候,此方法被调用。 |
exceptionCaught(IoSession session, Throwable cause) |
当 I/O 处理器的实现或是 Apache MINA 中有异常抛出的时候,此方法被调用。 |
messageReceived(IoSession session, Object message) |
当接收到新的消息的时候,此方法被调用。 |
messageSent(IoSession session, Object message) |
当消息被成功发送出去的时候,此方法被调用。 |
对于 表 6 中的方法,有几个需要重点的说明一下。首先是 sessionCreated
和 sessionOpened
的区别。sessionCreated
方法是由 I/O 处理线程来调用的,而 sessionOpened
是由其它线程来调用的。因此从性能方面考虑,不要在 sessionCreated
方法中执行过多的操作。对于 sessionIdle
,默认情况下,闲置时间设置是禁用的,也就是说sessionIdle
并不会被调用。可以通过 IoSessionConfig.setIdleTime(IdleStatus, int)
来进行设置。
Apache MINA 中的基本概念已经介绍完了,下面介绍状态机的使用。
使用状态机
在 I/O 处理器中实现业务逻辑的时候,对于简单的情况,一般只需要在 messageReceived
方法中对传入的消息进行处理。如果需要写回数据到对等体的话,用IoSession.write
方法即可。在另外的一些情况下,客户端和服务器端的通信协议比较复杂,客户端其实是有状态变迁的。这个时候可以用 Apache MINA 提供的状态机实现,可以使得 I/O 处理器的实现更加简单。
状态机中两个重要的元素是状态以及状态之间的迁移。示例应用中客户端的状态以及迁移如 图 5 所示。
图 5. 联机游戏示例应用中客户端的状态以及迁移
客户端初始化的时候,其状态为“未连接”,表示客户端还没有在服务器上面注册,此时还不能进行游戏;接着用户需要输入一个昵称来注册到服务器上面,完成之后状态迁移到“闲置”。此时客户端会接收到当前在线的所有其它用户的列表。当前用户可以邀请其它用户和他一块游戏,也可以接收来自其它用户的邀请。邀请发送出去之后,客户端的状态迁移到“邀请已发送”。如果接受了其它用户的邀请,客户端的状态迁移到“邀请已接收”。如果某个用户的邀请被另外一个用户接受的话,两个客户端的状态都会迁移到“游戏中”。
要实现这样较为复杂的状态机的话,只需要在 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是一个高性能、异步事件驱动的网络应用程序框架,主要用在开发网络通信应用...通过深入理解IoService接口和IoHandler机制,以及掌握如何创建和管理连接,可以更好地利用Mina框架来满足各种网络应用的需求。
Apache Mina是一个高性能、事件驱动的网络应用框架,主要用于简化开发服务器端的复杂性,尤其在处理TCP/IP、UDP和SSL/...通过不断实践和深入学习,开发者可以更好地掌握Mina框架,构建出高效、稳定和可维护的网络应用。
### Mina2学习笔记知识点概览 #### 一、Mina入门详解 ##### 第一步:下载使用的Jar包 - **mina-core-2.0.0-M1.jar**:这是Mina核心库,提供了NIO框架的基本功能。 - **slf4j-api-1.5.2.jar**:用于日志记录的高级...
IoSession是Mina框架中的核心概念,代表了客户端与服务器之间的一个会话。它包含了诸如读写操作、会话状态、连接参数等一系列重要信息。开发者可以通过IoSession进行数据传输、设置会话属性、管理会话生命周期等操作...
### Mina2.0学习笔记核心知识点概览 #### 一、Mina入门与环境搭建 ...以上是对《Mina2.0学习笔记(修订版)》的核心知识点概括,希望能帮助读者快速了解Mina框架的关键技术和应用场景,为后续深入学习打下坚实基础。
在这个"Apache MINA2学习笔记DEMO"中,我们很可能会看到如何使用MINA来创建一个自定义协议的示例。自定义协议通常是为了满足特定应用的需求,例如高效的数据传输、安全性或者特定的编码格式。MINA允许开发者定义自己...
### Mina2.0学习笔记知识点汇总 #### 一、Mina简介 - **Mina定义**:Mina是Apache组织推出的一个网络应用框架,它能够帮助开发者构建高性能且易于扩展的网络应用程序。通过提供一个抽象的、事件驱动的异步API,Mina...
最近使用Mina开发一个Java的NIO服务端程序,因此也特意学习了Apache的这个Mina框架。 引言 1 一. Mina入门 2 第一步.下载使用的Jar包 2 第二步.工程创建配置 2 第三步.服务端程序 3 第四步.客户端程序 6 第五步.长...
Apache Mina是一个基于Java的网络通信框架,专为高性能、高可用性和可扩展性而设计。...在深入学习Mina的过程中,理解IoService及其子类的工作原理,以及如何结合IoHandler实现业务逻辑,对于掌握Mina框架至关重要。
### Mina初步学习笔记知识点概览 #### 一、Mina简介及下载配置流程 **Mina**,全称**Multipurpose Infrastructure Networked Applications**,是Apache基金会开发的一个高性能网络应用框架,旨在帮助开发者构建高...
在MINA的学习笔记中,记录的所有API通常会包括以下几个核心部分: 1. **IoSession**: 这是MINA的核心接口,代表了客户端和服务器之间的连接。IoSession提供了读写数据、管理连接状态、获取会话属性等功能。例如,`...
### Mina2.0 学习笔记(重点) #### 一、Mina入门 ##### 第一步:下载使用的Jar包 1. **Mina核心库**:登录Mina官网下载`mina2.0.1.zip`,解压后得到`mina-core-2.0.0-M1.jar`。 2. **日志框架SLF4J**:访问SLF4J...
Apache Mina是一个强大的开源网络应用框架,主要设计用于构建高性能、高可扩展性的网络应用程序。Mina通过提供一个抽象的、事件驱动的异步API,简化了基于Java NIO(Non-blocking Input/Output)的复杂编程,使得...
**Mina 学习笔记(入门)** Apache Mina 是一个高度可扩展的网络通信框架,主要用于构建高性能、高效率的服务器端应用。它提供了一种简单的方式来处理网络协议,如TCP/IP和UDP/IP,以及SSL/TLS加密的连接。在本学习...
在本学习笔记中,我们将专注于MINA的子项目——FtpServer,它是实现FTP服务器功能的一个模块。 FTP(File Transfer Protocol)是一种广泛使用的互联网协议,用于在不同主机之间传输文件。Apache MINA FtpServer提供...
《mina学习笔记》 Apache MINA(Multipurpose Infrastructure for Network Applications)是一个开源框架,主要设计用于简化网络应用程序的开发,尤其是TCP和UDP协议的应用。MINA 提供了一种与网络协议无关的API,...