Apache MINA 1.x 中的ByteBuffer对象使用的问题

最近在为公司做一个消息中心的项目,项目中使用了Apache的MINA 1.7的版本,为消息中心做通讯接口。

[size=13px; line-height: 20px; ][size=x-small;]  Apache MINA(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的 MINA 版本支持基于 Java NIO 技术的 TCP/UDP 应用程序开发、串口通讯程序,MINA 所支持的功能也在进一步的扩展中。[/size][/size]

    使用MINA组件确实可以提高通讯的并发性能以及可靠性,在1.7版本中,MINA封装了JDK中的ByteBuffer,虽然调用上方便了不少,但是如果不正当的使用它,容易引起性能上的问题,甚至会出现Out of Memory的问题。这次我就遇到了Out of Memory的问题,后来经过我的仔细分析和调试,终于发现了这条臭虫。

    首先来看一下如何搭建MINA的TCP的服务端:

   
        // output log to console
	org.apache.log4j.BasicConfigurator.configure();
	acceptor = new SocketAcceptor();

        // Create a service configuration
        SocketAcceptorConfig cfg = new SocketAcceptorConfig();
        cfg.setReuseAddress(true);
        // add a protocol codec, the protocol is wrapper from 0x00 to 0xff, and between 0x00 and 0xff is a message.
        cfg.getFilterChain().addLast(
                "protocolFilter",
                new ProtocolCodecFilter(
                        new WrapperTagProtocolCodecFactory()));
        // add logger filter
        cfg.getFilterChain().addLast("logger", new LoggingFilter());
        
        acceptor
        .bind(new InetSocketAddress(PORT), new IoHandlerAdapter(){
			@Override
			public void exceptionCaught(IoSession session, Throwable cause)
					throws Exception {
				// do something...
			}

			@Override
			public void messageReceived(IoSession session, Object message)
					throws Exception {
				session.write("response something").join();
			}
        	
        }, cfg);
    


    这段代码只是作为测试用例而写的,从代码中我们可以看到,代码中创建了SocketAcceptor对象,并且使用了自定义的ProtocolCodecFilter,这个通讯协议比较简单,协议格式为"<0x00><Message><0xff>",传输的消息被0x00和0xff两个字节包裹着。那么读取这条时,当读到0x00时开始,读取到0xff结束。当未读取到0x00时而接收到了一些数据,这些读取到的数据将被忽略。WrapperTagProtocolCodecFactory是一个工厂类,他将负责创建解码对象和编码对象,解码对象负责解释读取中的协议数据,并将协议数据转换为消息。而编码对象是将消息转换为协议数据。
    Socket通讯是一个不可靠的消息传输通道,网络上传输的数据,并不是一次性就从客户端送到服务端,或者是服务端送到客户端,而是通过数据链路层,将数据送过来,这样会产生数据将有可能通过多次送到目的地。而MINA中的组件使用了ByteBuffer对象作为数据接收缓存,所以需要制定合理的通讯协议,来确保接收端收到的数据是完整的,可靠的。
    当然如上的协议并不能完全的避免数据传输所产生的问题,因为还有可能产生数据丢包等其他的问题,但这个协议适合数据流小的消息。
    当Socket通道的消息经过了协议的过滤器之后,还将经过LoggingFilter的过滤,这里实际上就是将收到的消息记录到Logger中。之后,将送给IoHandler处理,上面的代码通过匿名类继承了IoHandlerAdapter,为了方便测试,这里只重载了messageReceived方法。当MINA收到消息之后,立即发送回应的数据到客户端。
    接下来,我们看看WrapperTagProtocolCodecFactory创建的两个对象:
   
public class WrapperTagProtocolCodecFactory extends
		DemuxingProtocolCodecFactory {

	public WrapperTagProtocolCodecFactory(){
		super.register(WrapperTagDecoder.class);
		super.register(WrapperTagEncoder.class);
	}

	public WrapperTagProtocolCodecFactory(String charset){
		super.register(new WrapperTagDecoder(charset));
		super.register(new WrapperTagEncoder(charset));
	}
}
    

    此工厂创建了WrapperTagDecoder(解码类)和WrapperTagEncoder(编码类)两个对象。
    WrapperTagDecoder
   
public class WrapperTagDecoder extends MessageDecoderAdapter {

	private String charset = "gb2312";
	
	private Logger logger = Logger.getLogger(WrapperTagDecoder.class);
	
	public WrapperTagDecoder(String charset){
		this.charset = charset;
	}
	
	public WrapperTagDecoder(){
		
	}

	public MessageDecoderResult decodable(IoSession session, ByteBuffer in) {
		return MessageDecoderResult.OK;
	}

	public MessageDecoderResult decode(IoSession session, ByteBuffer in,
			ProtocolDecoderOutput out) throws Exception {
		InputStream stream = in.asInputStream();
		int head = stream.read();
		if(head == -1){
			throw new IOException("read -1 byte");
		}
		logger.info("Read head 0x00.");
		int b = -1;
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		while((b = stream.read()) != 0xff){
			os.write(b);
		}
		logger.info("Read rear 0xff.");
		String message = new String(os.toByteArray(), charset);
		out.write(message);
		logger.debug("Read message:" + message + ", charset:" + charset);
		return MessageDecoderResult.OK;
	}

}
    

    此类继承了MessageDecoderAdapter类,待会我们来看这个类的问题。
    WrapperTagEncoder
   
public class WrapperTagEncoder implements MessageEncoder {
	private static final Set<Class<?>> TYPES;
	
	private String charset = "gb2312";
	
	static {
        Set<Class<?>> types = new HashSet<Class<?>>();
        types.add(String.class);
        TYPES = Collections.unmodifiableSet(types);
    }
	
	public WrapperTagEncoder(String charset){
		this.charset = charset;
	}
	
	public WrapperTagEncoder(){
		
	}

	public Set<Class<?>> getMessageTypes() {
		return TYPES;
	}

	public void encode(IoSession session, Object message,
			ProtocolEncoderOutput out) throws Exception {
		ByteBuffer buf = ByteBuffer.allocate(256);
        // Enable auto-expand for easier encoding
        buf.setAutoExpand(true);
        
        buf.put((byte)0x00);
        buf.put(message.toString().getBytes(charset));
        buf.put((byte)0xff);
        
        buf.flip();
        out.write(buf);
	}
    

    从实现上来看,WrapperTagEncoder比WrapperTagDecoder要来简单一些。WrapperTagEncoder只需要负责写就行了,而不需要控制多段数据的问题。那么上述的WrapperTagDecoder会出现什么问题呢?
    MINA的Socket是基于Java的NIO而实现的非阻塞式的通讯,从WrapperTagDecoder代码我们看到ByteBuffer中提供了一个方法asInputStream,也就是将ByteBuffer转换为输入流,这个倒没什么问题,问题是在于读取的时候,使用了阻塞式的调用方法,stream.read()。如果接收的数据大于了ByteBuffer预分配的容量时,会报出:
    org.apache.camel.CamelException: org.apache.mina.filter.codec.ProtocolDecoderException: java.lang.OutOfMemoryError: Java heap space (Hexdump: .....一堆ByteBuffer已读取到的数据,16进制显示)
    据我分析,ByteBuffer会预先分配一定容量的堆空间,MINA的ByteBuffer应该是直接初始化了JVM底层中的堆空间,ByteBuffer默认的堆空间应该是1024byte,从ByteBuffer的asInputStream的InputStream的实现代码中可以看到,read方法实际上调用的是get()方法,如下:
   
    @Override
    public int read() {
         if (ByteBuffer.this.hasRemaining()) {
             return ByteBuffer.this.get() & 0xff;
         } else {
             return -1;
         }
    }

    @Override
    public int read(byte[] b, int off, int len) {
        int remaining = ByteBuffer.this.remaining();
        if (remaining > 0) {
            int readBytes = Math.min(remaining, len);
            ByteBuffer.this.get(b, off, readBytes);
            return readBytes;
            } else {
                    return -1;
            }
        }
    

    实际上MINA的底层实现中,他会先将已读取到缓存中的ByteBuffer先扔出来给Filter,如果此时ByteBuffer已满了,那么就需要MINA会扩展ByteBuffer的容量,在扩展的过程中,就不应该再去调用任何有关get或者read的方法,由于实现中,不断的在使用read循环读取ByteBuffer中的数据,这样效率降低了,而且容易导致阻塞,本身Filter的生命周期的时间是有限的,这样会扰乱MINA本身的处理机制。如果继续想使用read方法获取数据,应该使用read(byte[] b),以块的形式读取,如果返回了-1,那么直接就返回出去,并将已读到消息,缓存起来,直到读取到0xff时,才清除掉缓存,并送到ProtocolDecoderOutput对象中,接着他就会交给IoHandler进行处理。下面是改良后的WrapperTagDecoder。
   
public class WrapperTagDecoder extends MessageDecoderAdapter {

	private String charset = "gb2312";
	
	private Logger logger = Logger.getLogger(WrapperTagDecoder.class);
	
	private boolean readRear = false;
	
	private boolean readHead = false;
	
	private int receivedBufferSize = 1024;
	
	private ByteArrayOutputStream os = new ByteArrayOutputStream();
	
	public WrapperTagDecoder(String charset){
		this.charset = charset;
	}
	
	public int getReceivedBufferSize() {
		return receivedBufferSize;
	}

	public void setReceivedBufferSize(int receivedBufferSize) {
		this.receivedBufferSize = receivedBufferSize;
	}

	public WrapperTagDecoder(){
		
	}

	public MessageDecoderResult decodable(IoSession session, ByteBuffer in) {
		return MessageDecoderResult.OK;
	}

	public MessageDecoderResult decode(IoSession session, ByteBuffer in,
			ProtocolDecoderOutput out) throws Exception {
		InputStream stream = in.asInputStream();
		if(!readHead){
			int head = stream.read();
			if(head == -1){
				throw new IOException("read -1 byte");
			}
			readHead = true;
			logger.info("Read head 0x00.");
		}
		byte[] buffer = new byte[receivedBufferSize];
		int length = stream.read(buffer);
		if(length == -1)
			return MessageDecoderResult.OK;
		if(buffer[length - 1] == -1){
			readRear = true;
			logger.info("Read rear 0xff.");
			os.write(buffer, 0, length - 1);
		}else{
			os.write(buffer, 0, length);
		}
		if(readRear){
			String message = new String(os.toByteArray(), charset);
			out.write(message);
			logger.debug("Read message:" + message + ", charset:" + charset);
			
			readRear = false;
			readHead = false;
			os.reset();
		}
		return MessageDecoderResult.OK;
	}

}
    

    从使用上来看,看上去似乎还在使用阻塞式的方式去取,但是当你看到InputStream的实现时,你会发现,MINA将他封装了一层,内部还是调用这get的方法。InputStream其实是给方便习惯使用了阻塞式编程的程序员而设计的,我也是从这边慢慢的习惯过来,如果要深入了解非阻塞的编程尤其是MINA的组件,可以多看看网上的一些例子,最直接的方式就是去查看MINA其中的一个解析类,org.apache.mina.filter.codec.textline.TextLineCodecFactory。这里面将ByteBuffer运用的炉火纯青,尤其是对position和limit方法运用的是那么的到位,以及内部缓存,看的我是一直叫好。
    不过这里还是提醒大家一句,如果是高性能的并发应用,MINA的ByteBuffer从性能上来看并不好到哪里去,还不如Java中的ByteBuffer对象,尤其是对postion的调用,性能上比Java本身的慢了不少。但是MINA 2.x已抛弃了ByteBuffer的设计,使用了更为有效的缓存方式,我建议大家还是使用2.x的版本,由于公司的项目使用了Apache Camel的组件,目前camel-mina的版本封装的是1.7的版本,期待Apache大佬们能够更新到2.x的版本。也希望遇到此问题的朋友们能够得到一点帮助。
   
   

你可能感兴趣的:(ByteBuffer,Mina)