Mina框架学习笔记(四)

上一节中给出了一个简单的基于 Apache MINA 的网络应用的实现,可以用来熟悉基本的架构。而在实际开发中,网络应用都是有一定复杂度的。下面会以一个比较复杂的联机游戏作为示例来详细介绍 Apache MINA 的概念、API 和典型用法。

该联机游戏支持两个人进行俄罗斯方块的对战。这个游戏借鉴了 QQ 的“火拼俄罗斯”。用户在启动客户端之后,需要输入一个昵称进行注册。用户可以在“游戏大厅”中查看当前已注册的所有其它用户。当前用户可以选择另外的一个用户发送游戏邀请。邀请被接受之后就可以开始进行对战。在游戏过程中,当前用户可以看到对方的游戏状态,即方块的情况。该游戏的运行效果如 图 3 所示。



联机游戏示例运行效果图 

下面开始以这个应用为例来具体介绍 Apache MINA 中的基本概念。先从 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 所示。


方法说明
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 接受器的时候,只需要调用 bind方法并指定要监听的套接字地址。当不再接受连接的时候,调用 unbind停止监听即可。关于 I/O 接受器的具体用法,可以参考 清单 2 中给出的计算器服务的实现。

I/O 连接器用来发起连接,与对等体(服务器)进行通讯,并发出相应的 I/O 事件交给 I/O 处理器来处理。使用 I/O 连接器的时候,只需要调用 connect方法连接指定的套接字地址。另外可以通过 setConnectTimeoutMillis设置连接超时时间(毫秒数)。

清单 3 中给出了使用 I/O 连接器的一个示例。


SocketConnector connector = new NioSocketConnector(); 
connector.setConnectTimeoutMillis(CONNECT_TIMEOUT); 
connector.getFilterChain().addLast("logger", new LoggingFilter()); 
connector.getFilterChain().addLast("protocol", 
    new ProtocolCodecFilter(new TetrisCodecFactory())); 
ConnectFuture connectFuture = connector.connect(new InetSocketAddress(host, port)); 
connectFuture.awaitUninterruptibly(); 

 

在 清单 3 中,首先创建一个 Java NIO 的套接字连接器 NioSocketConnector 的实例,接着设置超时时间。再添加了 I/O 过滤器之后,通过 connect 方法连接到指定的地址和端口即可。

在介绍完 I/O 服务之后,下面介绍 I/O 会话。

 



回页首

 

I/O 会话表示一个活动的网络连接,与所使用的传输方式无关。I/O 会话可以用来存储用户自定义的与应用相关的属性。这些属性通常用来保存应用的状态信息,还可以用来在 I/O 过滤器和 I/O 处理器之间交换数据。I/O 会话在作用上类似于 Servlet 规范中的 HTTP 会话。

Apache MINA 中 I/O 会话实现的接口是 org.apache.mina.core.session.IoSession。该接口中比较重要的方法如 表 2 所示。


方法说明
close(boolean immediately)关闭当前连接。如果参数 immediately为 true的话,连接会等到队列中所有的数据发送请求都完成之后才关闭;否则的话就立即关闭。
getAttribute(Object key)从 I/O 会话中获取键为 key的用户自定义的属性。
setAttribute(Object key, Object value)将键为 key,值为 value的用户自定义的属性存储到 I/O 会话中。
removeAttribute(Object key)从 I/O 会话中删除键为 key的用户自定义的属性。
write(Object message)将消息对象 message发送到当前连接的对等体。该方法是异步的,当消息被真正发送到对等体的时候,IoHandler.messageSent(IoSession,Object)会被调用。如果需要的话,也可以等消息真正发送出去之后再继续执行后续操作。

在介绍完 I/O 会话之后,下面介绍 I/O 过滤器。

 



回页首

 

从 I/O 服务发送过来的所有 I/O 事件和请求,在到达 I/O 处理器之前,会先由 I/O 过滤器链中的 I/O 过滤器进行处理。Apache MINA 中的过滤器与 Servlet 规范中的过滤器是类似的。过滤器可以在很多情况下使用,比如记录日志、性能分析、访问控制、负载均衡和消息转换等。过滤器非常适合满足网络应用中各种横切的非功能性需求。在一个基于 Apache MINA 的网络应用中,一般存在多个过滤器。这些过滤器互相串联,形成链条,称为过滤器链。每个过滤器依次对传入的 I/O 事件进行处理。当前过滤器完成处理之后,由过滤器链中的下一个过滤器继续处理。当前过滤器也可以不调用下一个过滤器,而提前结束,这样 I/O 事件就不会继续往后传递。比如负责用户认证的过滤器,如果遇到未认证的对等体发出的 I/O 事件,则会直接关闭连接。这可以保证这些事件不会通过此过滤器到达 I/O 处理器。

Apache MINA 中 I/O 过滤器都实现 org.apache.mina.core.filterchain.IoFilter接口。一般来说,不需要完整实现 IOFilter接口,只需要继承 Apache MINA 提供的适配器 org.apache.mina.core.filterchain.IoFilterAdapter,并覆写所需的事件过滤方法即可,其它方法的默认实现是不做任何处理,而直接把事件转发到下一个过滤器。

IoFilter接口提供了 15 个方法。这 15 个方法大致分成两类,一类是与过滤器的生命周期相关的,另外一类是用来过滤 I/O 事件的。第一类方法如 表 3 所示。


方法说明
init()当过滤器第一次被添加到过滤器链中的时候,此方法被调用。用来完成过滤器的初始化工作。
onPreAdd(IoFilterChain parent, String name, IoFilter.NextFilter nextFilter)当过滤器即将被添加到过滤器链中的时候,此方法被调用。
onPostAdd(IoFilterChain parent, String name, IoFilter.NextFilter nextFilter)当过滤器已经被添加到过滤器链中之后,此方法被调用。
onPreRemove(IoFilterChain parent, String name, IoFilter.NextFilter nextFilter)当过滤器即将被从过滤器链中删除的时候,此方法被调用。
onPostRemove(IoFilterChain parent, String name, IoFilter.NextFilter nextFilter)当过滤器已经被从过滤器链中删除的时候,此方法被调用。
destroy()当过滤器不再需要的时候,它将被销毁,此方法被调用。

在 表 3 中给出的方法中,参数 parent 表示包含此过滤器的过滤器链,参数 name 表示过滤器的名称,参数 nextFilter 表示过滤器链中的下一个过滤器。

第二类方法如 表 4 所示。


方法说明
filterClose(IoFilter.NextFilter nextFilter, IoSession session)过滤对 IoSession的 close方法的调用。
filterWrite(IoFilter.NextFilter nextFilter, IoSession session, WriteRequest writeRequest)过滤对 IoSession的 write方法的调用。
exceptionCaught(IoFilter.NextFilter nextFilter, IoSession session, Throwable cause)过滤对 IoHandler的 exceptionCaught方法的调用。
messageReceived(IoFilter.NextFilter nextFilter, IoSession session, Object message)过滤对 IoHandler的 messageReceived方法的调用。
messageSent(IoFilter.NextFilter nextFilter, IoSession session, WriteRequest writeRequest)过滤对 IoHandler的 messageSent方法的调用。
sessionClosed(IoFilter.NextFilter nextFilter, IoSession session)过滤对 IoHandler的 sessionClosed方法的调用。
sessionCreated(IoFilter.NextFilter nextFilter, IoSession session)过滤对 IoHandler的 sessionCreated方法的调用。
sessionIdle(IoFilter.NextFilter nextFilter, IoSession session, IdleStatus status)过滤对 IoHandler的 sessionIdle方法的调用。
sessionOpened(IoFilter.NextFilter nextFilter, IoSession session)过滤对 IoHandler的 sessionOpened方法的调用。

对于 表 4 中给出的与 I/O 事件相关的方法,它们都有一个参数是 nextFilter,表示过滤器链中的下一个过滤器。如果当前过滤器完成处理之后,可以通过调用 nextFilter中的方法,把 I/O 事件传递到下一个过滤器。如果当前过滤器不调用 nextFilter 中的方法的话,该 I/O 事件就不能继续往后传递。另外一个共同的参数是 session,用来表示当前的 I/O 会话,可以用来发送消息给对等体。下面通过具体的实例来说明过滤器的实现。

BlacklistFilter是 Apache MINA 自带的一个过滤器实现,其功能是阻止来自特定地址的连接,即所谓的“黑名单”功能。BlacklistFilter继承自 IoFilterAdapter,并覆写了 IoHandler相关的方法。清单 4 中给出了部分实现。


public void messageReceived(NextFilter nextFilter, IoSession session, Object message) { 
    if (!isBlocked(session)) { 
        nextFilter.messageReceived(session, message); 
    } else { 
        blockSession(session); 
    } 
} 

private void blockSession(IoSession session) { 
    session.close(true); 
}

 

在 清单 4 中 messageReceived 方法的实现中,首先通过 isBlocked 来判断当前连接是否应该被阻止,如果不是的话,则通过 nextFilter.messageReceived 把该 I/O 事件传递到下一个过滤器;否则的话,则通过 blockSession 来阻止当前连接。

ProtocolCodecFilter 用来在字节流和消息对象之间互相转换。当该过滤器接收到字节流的时候,需要首先判断消息的边界,然后把表示一条消息的字节提取出来,通过一定的逻辑转换成消息对象,再把消息对象往后传递,交给 I/O 处理器来执行业务逻辑。这个过程称为“解码”。与“解码”对应的是“编码”过程。在“编码”的时候,过滤器接收到的是消息对象,通过与“解码”相反的逻辑,把消息对象转换成字节,并反向传递,交给 I/O 服务来执行 I/O 操作。

在“编码”和“解码”中的一个重要问题是如何在字节流中判断消息的边界。通常来说,有三种办法解决这个问题:

  • 使用固定长度的消息。这种方式实现起来比较简单,只需要每次读取特定数量的字节即可。
  • 使用固定长度的消息头来指明消息主体的长度。比如每个消息开始的 4 个字节的值表示了后面紧跟的消息主体的长度。只需要首先读取该长度,再读取指定数量的字节即可。
  • 使用分隔符。消息之间通过特定模式的分隔符来分隔。每次只要遇到该模式的字节,就表示到了一个消息的末尾。

 

具体到示例应用来说,客户端和服务器之间的通信协议比较复杂,有不同种类的消息。每种消息的格式都不相同,同类消息的内容也不尽相同。因此,使用固定长度的消息头来指明消息主体的长度就成了最好的选择。

示例应用中的每种消息主体由两部分组成,第一部分是固定长度的消息类别名称,第二部分是每种消息的主体内容。图 4 中给出了示例应用中一条完整的消息的结构。



示例应用中消息的结构 

AbstractTetrisCommand用来描述联机游戏示例应用中的消息。它是一个抽象类,是所有具体消息的基类。其具体实现如 清单 5 所示。


public abstract class AbstractTetrisCommand implements TetrisCommand { 	
    public abstract String getName(); 

    public abstract byte[] bodyToBytes() throws Exception; 
	
    public abstract void bodyFromBytes(byte[] bytes) throws Exception; 

    public byte[] toBytes() throws Exception { 
        byte[] body = bodyToBytes(); 
        int commandNameLength = Constants.COMMAND_NAME_LENGTH; 
        int len = commandNameLength + body.length; 
        byte[] bytes = new byte[len]; 
        String name = StringUtils.rightPad(getName(), commandNameLength, 
            Constants.COMMAND_NAME_PAD_CHAR); 
        name = name.substring(0, commandNameLength); 
        System.arraycopy(name.getBytes(), 0, bytes, 0, commandNameLength); 
        System.arraycopy(body, 0, bytes, commandNameLength, body.length); 
        return bytes; 
    } 
}

 

如 清单 5 所示,AbstractTetrisCommand 中定义了 3 个抽象方法:getNamebodyToBytes 和 bodyFromBytes,分别用来获取消息的名称、把消息的主体转换成字节数组和从字节数组中构建消息。bodyToBytes对应于前面提到的“编码”过程,而 bodyFromBytes对应于“解码”过程。每种具体的消息都应该实现这 3 个方法。AbstractTetrisCommand 中的方法 toBytes 封装了把消息的主体转换成字节数组的逻辑,在字节数组中,首先是长度固定为 Constants.COMMAND_NAME_LENGTH的消息类别名称,紧接着是每种消息特定的主体内容,由 bodyToBytes 方法来生成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值