Netty In Action-第七章 编解码器Codec

本文介绍了Netty的编解码器框架及使用。编解码器由解码器和编码器组成,分别处理入站和出站数据。Netty提供了丰富的解码器和编码器抽象基类,如ByteToMessageDecoder、ReplayingDecoder等。还介绍了编解码器的组合使用,以及其他编码方式,如CombinedChannelDuplexHandler。

本章介绍

  • Codec,编解码器
  • Decoder,解码器
  • Encoder,编码器

Netty提供了编解码器框架,使得编写自定义的编解码器很容易,并且也很容易重用和封装。本章讨论Netty的编解码器框架以及使用。

1.编解码Codec

编写一个网络应用程序需要实现某种编解码器,编解码器的作用就是将原始字节数据域自定义的消息对象进行互转。网络中都是以字节码的形式来传输数据的,服务器编码数据后发送到客户端,客户端需要对数据进行解码,因为编解码器由两部分组成:

  • Decoder,解码器
  • Encoder,编码器

解码器负责将消息从字节或其他序列形式转成指定的消息对象,编码器则相反;解码器负责处理“入站”数据,编码器负责处理出站数据。编码器和解码器的结构很简单,消息被编码后解码会自动通过ReferenceCountUtil.release(message)释放,如果不想释放消息可以使用ReferenceCountUtil.retain(message),这将会使引用数量增加而没有消息发布,大多数时候不需要这么做。

2.解码器

Netty提供了丰富的解码器抽象基类,我们可以很容易的实现这些基类来自定义解码器。下面是解码器的一个类型:

  • 解码字节到消息
  • 解码消息到消息
  • 解码消息到字节

本章将概述不同的抽象基类,来帮助了解解码器的实现。深入了解Netty的解码器之前先了解解码器的作用是什么?解码器负责解码“入站”数据从一种格式到另一种格式,解码器处理入站数据是抽象ChannelInboundHandler的实现。实践中使用解码器很简单,就是将入站数据转换后传递到ChannelPipeline中的下一个ChannelInboundHandler进行处理;这样的处理是很灵活的,我们可以将解码器放在ChannelPipeline中,重用逻辑。

2.1 ByteToMessageDecoder

通常你需要将消息从字节解码成消息或从字节解码成其他的序列化字节。这是一个常见的任务,Netty提供了抽象基类,我们可以使用他们来实现。Netty中提供的ByteToMessageDecoder可以将字节消息解码成POJO对象,下面列出了ByteToMessageDecoder两个主要方法:

  • decode(ChannelHandlerContext, ByteBuf,List<Object>),这个方法是唯一的一个需要自己实现的抽象方法,作用是将ByteBuf数据解码成其他形式的数据。
  • decodeLast(ChannelHandlerContext, ByteBuf,List<Object>),实际上调用的是decode(...)。

例如服务器从某个客户端接收到一个整数值的字节码,服务器将数据读入ByteBuf并经过ChannelPipeline中的每个ChannelInboundHandler进行处理,看下图:

上图显示了从“入站”ByteBuf读取bytes后由ToIntegerDecoder进行解码,然后将解码后的消息传递到ChannelPipeline中的下一个ChannelInboundHandler。看下面的ToIntegerDecoder的实现代码:

package netty.in.action.chapter7;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

public class ToIntegerDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() > 4) {
            out.add(in.readInt());
        }
    }
}

从上面的代码可能会发现,我们需要检查ByteBuf读之前是否有足够的字节,若没有这个检查岂不是更好?是的,Netty提供了这样的处理允许byte-to-message解码,在下一节讲解。除了ByteToMessageDecoder之外,Netty还提供了许多其他的解码接口。

2.2 ReplayingDecoder

ReplayingDecoder是byte-to-message解码的一种特殊的抽象基类,类读取缓冲区的数据之前需要检查缓冲区是否有足够的字节,使用ReplayingDecoder就无需自己检查;若ByteBuf中有足够的字节,则会正常读取;若没有足够的字节则会停止解码。也正是因为这样的包装使得ReplayingDecoder带有一定的局限性。

  • 不是所有的操作都被ByteBuf支持,如果调用一个不支持的操作会抛出DecoderException。
  • ByteBuf.readableBytes()大部分时间不会返回期望值

如果你能忍受上面列出的限制,相比ByteToMessageDecoder,你可能更喜欢ReplayingDecoder。在满足需求的情况下推荐使用ByteToMessageDecoder,因为它的处理比较简单,没有ReplayingDecoder的实现那么负责。ReplayingDecoder继承与ByteToMessageDecoder,所以他们提供的接口是相同的。下面代码是ReplayingDecoder的实现:

package netty.in.action.chapter7;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.util.List;

public class ToIntegerReplayingDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        out.add(in.readInt());
    }
}

当从接受的数据ByteBuf读取Integer,若没有足够的字节可读,decode(...)会停止解码,若有足够的字节可读,则会读取数据添加到List列表中。使用ReplayingDecoder或ByteToMessageDecoder是个人喜好的问题,Netty提供了这两种的实现,选择哪一个都可以。

2.3 MessageToMessageDecoder

将消息对象转成消息对象可是使用MessageToMessageDecoder,它是一个抽象类,需要我们自己实现其decode(...)。message-to-message通上面将的byte-to-message的处理机制一样,看下图:

看下面的实现代码:

package netty.in.action.chapter7;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;

import java.util.List;

public class IntegerToStringDecoder extends MessageToMessageDecoder<Integer> {
    @Override
    protected void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
        out.add(String.valueOf(msg));
    }
}

2.4 解码器总结

解码器是用来处理入站数据,Netty提供了很多解码器的实现,可以根据需求详细了解。我们发送数据需要将数据编码,Netty中也提供了编码器的支持。

3.编码器

Netty提供了一些基类,我们可以很简单的实现编码器。同样的,编码器与下面两种类型:

  • 消息对象编码成消息对象
  • 消息对象编码成字节码

相对解码器,编码器少了一个byte-tobyte的类型,因为出站数据这样做没有意义。编码器的作用就是将处理好的数据转成字节码以便在网络中传输。对照上面列出的两种编码器类型,Netty也分别提供了两个抽象类:MessageToByteEncoder和MessageToMessageEncoder。下面是类关系图:

3.1 MessageToByteEncoder

MessageToByteEncoder是抽象类,我们自定义一个继承MessageToMessageEncoder的编码器只需要实现其提供的encode(...)方法。其工作流如下图:

 实现代码如下:

package netty.in.action.chapter7;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class IntegerToByteEncoder extends MessageToByteEncoder<Integer> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
        out.writeInt(msg);
    }
}

3.2 MessageToMessageEncoder

需要将消息编码成其他的消息时可以使用Netty提供的MessageToMessageEncoder抽象类来实现。例如将Integer编码成String,其工作流程如下:

实现代码如下:

package netty.in.action.chapter7;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;

import java.util.List;

public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
        out.add(String.valueOf(msg));
    }
}

4.编解码器

实际编码中,一般会将编码和解码操作封装在一个类中,解码处理“入站”数据,编码处理“出站”数据。知道了编码和解码,对于下面的情况不会感觉惊讶:

  • byte-to-message编码和解码
  • message-to-message编码和解码

如果确定需要在ChannelPipeline中使用编码器和解码器,需要更好的使用一个抽象的编解码器。统一使用编解码器的时候,不可能只删除解码器或编码器而离开ChannelPipeline导致某种不一致的状态。使用编解码器将强制性的要么都在ChannelPipeline,要么都不在ChannelPipeline。

考虑到这一点,我们在下面季节将更深入的分析Netty提供的编解码抽象类。

4.1 byte-to-byte编解码器

Netty4较之前的版本,其结构有很大的变化,在Netty4中实现byte-to-byte提供了2个类:ByteArrayEncoder和ByteArrayDecoder。这连个类用来处理字节到字节的编码和解码。下面是这两个类的源码,一看就知道是如何处理的:

public class ByteArrayDecoder extends MessageToMessageDecoder<ByteBuf> {  
    @Override  
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {  
         // copy the ByteBuf content to a byte array  
        byte[] array = new byte[msg.readableBytes()];  
        msg.getBytes(0, array);  
  
        out.add(array);  
    }  
}  
@Sharable  
public class ByteArrayEncoder extends MessageToMessageEncoder<byte[]> {  
    @Override  
    protected void encode(ChannelHandlerContext ctx, byte[] msg, List<Object> out) throws Exception {  
        out.add(Unpooled.wrappedBuffer(msg));  
    }  
} 

4.2 ByteToMessageCodec

ByteToMessageCodec用来处理byte-to-message和message-to-byte。如果想要解码字节消息成POJO或编码POJO消息成字节,对于这种情况,ByteToMessageCodec<I>是一个不错的选择。ByteToMessageCodec是一种组合,器等同于ByteToMessageDecoder和ByteToMessageEncoder的组合。ByteToMessageCodec是个抽象类,其中2个方法需要我们自己实现:

  • encode(ChannelHandlerContext, I, ByteBuf) 编码
  • decode(ChannelHandlerContext, ByteBuf, List<Object>)

4.3 MessageToMessageCodec

MessageToMessageCodec用于message-to-message的编码和解码,可以看成是MessageToMessageDecoder和MessageToMessageEncoder的组合体。MessageToMessageCodec是抽象类,其中有2个方法需要实现:

  • encode(ChannelHandlerContext, OUTBOUN_IN, ByteBuf) 编码
  • decode(ChannelHandlerContext, INBOUND_IN, List<Object>)

但是这种解码器能有用吗?

有许多用例,最常见的就是需要将消息从一个API转到灵一个API。这种情况下需要自定义API和或旧的API使用另一个消息类型。下面的代码显示了在Websocket框架APIS之间转换消息:

package netty.in.action.chapter7;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.websocketx.*;

import java.util.List;

@ChannelHandler.Sharable
public class WebsocketConvertHandler extends MessageToMessageCodec<WebSocketFrame, WebsocketConvertHandler.MyWebSocketFrame> {

    public static WebsocketConvertHandler INSTANCE = new WebsocketConvertHandler();

    @Override
    protected void encode(ChannelHandlerContext ctx, MyWebSocketFrame msg, List<Object> out) throws Exception {
        switch (msg.type) {
            case BINARY:
                out.add(new BinaryWebSocketFrame(msg.getData()));
                break;
            case CLOSE:
                out.add(new CloseWebSocketFrame(true,0,msg.getData()));
                break;
            case PING:
                out.add(new PingWebSocketFrame(msg.getData()));
                break;
            case PONG:
                out.add(new PongWebSocketFrame(msg.getData()));
                break;
            case TEXT:
                out.add(new TextWebSocketFrame(msg.getData()));
                break;
            case CONTINUATION:
                out.add(new ContinuationWebSocketFrame(msg.getData()));
                break;
            default:
                throw new IllegalStateException("Unsupported websocket msg"+ msg);
        }
    }

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, WebSocketFrame msg, List<Object> out) throws Exception {
        if (msg instanceof BinaryWebSocketFrame) {
            out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.BINARY, msg.content().copy()));
            return;
        }
        if (msg instanceof CloseWebSocketFrame) {
            out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.CLOSE, msg.content().copy()));
            return;
        }
        if (msg instanceof PingWebSocketFrame) {
            out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.PING, msg.content().copy()));
            return;
        }
        if (msg instanceof PongWebSocketFrame) {
            out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.PONG, msg.content().copy()));
            return;
        }
        if (msg instanceof TextWebSocketFrame) {
            out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.TEXT, msg.content().copy()));
            return;
        }
        if (msg instanceof ContinuationWebSocketFrame) {
            out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.CONTINUATION, msg.content().copy()));
            return;
        }
        throw new IllegalStateException("Unsupported websocket msg"+ msg);
    }

    public static final class MyWebSocketFrame {

        public  enum FrameType{
            BINARY,CLOSE,PING,PONG,TEXT,CONTINUATION
        }
        private final FrameType type;
        private final ByteBuf data;

        public MyWebSocketFrame(FrameType type, ByteBuf data) {
            this.type = type;
            this.data = data;
        }

        public FrameType getType() {
            return type;
        }

        public ByteBuf getData() {
            return data;
        }
    }
}

5.其他编码方式

使用编解码器来充当编码器和解码器的组合失去了单独使用编码器和解码器的灵活性,编解码器要么是都有要么都没有。你可能想知道是否有解决这种僵化问题方式,还可以让编码器和解码器在ChannelPipeline中作为一个逻辑单元。幸运的是,Netty提供了一种解决方案,使用CombinedChannelDuplexHandler。虽然这个类不是编解码器API的一部分,但是它经常被用来建立一个编码器。

5.1 CombinedChannelDuplexHandler

如何使用CombinedChannelDuplexHandler来结合解码器和编码器呢?下面我们从两个简单的例子了解。

package netty.in.action.chapter7.combin;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

/**
 *  解码器,将byte转成char
 */
public class ByteToCharDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        while (in.readableBytes() >= 2) {
            out.add(Character.valueOf(in.readChar()));
        }
    }
}
package netty.in.action.chapter7.combin;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**
 * 编码器,将char转成byte
 */
public class CharToByteEncoder extends MessageToByteEncoder<Character> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Character msg, ByteBuf out) throws Exception {
        out.writeChar(msg);
    }
}
package netty.in.action.chapter7.combin;

import io.netty.channel.CombinedChannelDuplexHandler;

/**
 * 继承CombinedChannelDuplexHandler,用于绑定解码器和编码器
 */
public class CharCodec extends CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {
    public CharCodec() {
        super(new ByteToCharDecoder(), new CharToByteEncoder());
    }
}

从上面的代码可以看出,使用CombinedChannelDuplexHandler绑定解码器和编码器很容易实现,比使用*Codec更灵活。

Netty还提供了其他的协议支持,放在io.netty.handler.codec包下,如:

  • Google的protobuf,在io.netty.handler.codec.protobuf包下
  • Google的SPDY协议
  • RTSP(实时流传输协议),在io.netty.handler.codec.rtsp包下
  • SCTP(流控制传输协议),在io.netty.handler.codec.sctp包下
  • ......
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值