【Pigeon源码阅读】RPC底层通信实现原理(八)

本文深入剖析Pigeon的RPC通信机制,讲解TCP协议格式,包括粘包半包问题的解决方案、定长消息头格式、Netty3 Handler的相关实现。重点介绍了服务端和客户端的实现细节,如FrameDecoder、Crc32Handler、CompressHandler、ProviderDecoder、NettyServerHandler等组件的作用和工作原理。
摘要由CSDN通过智能技术生成


pigeon底层通过Netty-3.9.2.Final实现服务端和客户端的连接通信,对应实现类为NettyServer和NettyClient。在内部,处理RPC通信的核心逻辑又分别定义在NettyServerPipelineFactory和NettyClientPipelineFactory,这两个类都实现了ChannelPipelineFactory,重写了里面的getPipeline方法,用于处理发送请求和处理请求的相关流程逻辑。

pigeon TCP协议格式

pigeon目前区分两种TCP协议方式,一种是非统一(默认)协议,为普通序列化方式如Hessian,json等方式调用,另一种是统一协议,如Thrift调用和泛化调用,其中泛化调用可以在不直到api设计的基础上,直接通过指定方法名字符串来调用相应的服务方法。基于不同协议,tcp消息包格式也是不同的,下面分开解析

粘包半包问题

在TCP传输中,一个完整的消息包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,因而数据接收方无法区分消息包的头尾,在接收方来看,可能一次接收的数据内容只是一次完整请求的部分数据,或包含多次请求的数据等情况。基于此,常见有三种解决方式:

  1. 消息包定长,固定每个消息包长度,不够的话用空格补,缺点是长度不能灵活控制,字节不够的情况下补空格会造成浪费,字节过多还要处理分包传输的逻辑
  2. 使用定长消息头和变长消息体,其中消息体的长度必须在消息头指出,在接收方每次接收数据时,先确保获取到定长的消息头,再从消息头中解析出消息体的长度,以此来界定获取以此完整请求的消息包数据。
  3. 在消息包尾部指定分隔符,缺点是需要遍历数据,判断是否为分隔符,影响效率。

在pigeon中,是基于第二种,使用定长消息头和变长消息体的方式实现的。

定长消息头格式

在消息头部分,统一协议和默认协议的区别较大,这里分开讲述:

默认协议消息格式

默认协议消息格式具体包括2部分:消息头、消息体,其中消息体包含变长请求体和定长请求尾两部分。
默认协议消息头固定为7个字节:

  1. 第1-2个字节固定为十六进制的0x39、0x3A,即十进制的57、58,可以用来区分是默认协议还是统一协议。
  2. 第3个字节为序列化方式,如Hessian是2,java是3,json是7等。
  3. 第4-7个字节:消息体长度,int,占4个字节,值为请求体长度(请求或响应对象序列化后的字节长度)+请求尾长度11。

统一协议消息格式

类似默认协议消息,统一协议消息格式也包括2部分:消息头、消息体,和默认协议不同的是,统一协议中,消息体部分为完整请求体尾部不再包含请求尾,但会在请求体头部包含一个两字节长度的请求头。这里需要区分消息头和请求头的区别。
统一协议的消息头固定为8个字节:

  1. 第1-2个字节固定为十六进制的0xAB、0xBA,即十进制的171、186,或8位有符号整型的-85、-70,可以用来区分是默认协议还是统一协议。
  2. 第3个字节为协议版本,也可以称作command字节,会用来定义编解码的相关自定义行为,如压缩方式、数据校验方式等,具体command第8位表示是否需要进行校验数据完整性,第6、7位定义了是否进行压缩及具体的压缩方式。
  3. 第4个字节为序列化方式,一般为1。
  4. 第5~8个字节为消息体长度。

消息体

消息体部分,不区分是统一协议还是默认协议,最终解析出请求和响应对象类型分别为com.dianping.dpsf.protocol.DefaultRequest或com.dianping.dpsf.protocol.DefaultResponse,而除此之外,两种协议有细微区别:

  1. 统一协议没有请求尾,在消息体头部会有两个定长字节,这两个字节在序列化内部赋值,是Thrift内部计算的头部长度。
  2. 默认协议没有请求头,在消息体尾部会有11位定长字节,前8个字节为消息sequence,long型,值请从0开始递增,每个消息的sequence都不同;后3个字节固定为:29,30,31

DefaultRequest或com的具体定义如下:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "seq", scope = DefaultRequest.class)
public class DefaultRequest implements InvocationRequest {
   

    /**
     * 不能随意修改!
     */
    private static final long serialVersionUID = 652592942114047764L;

    // 必填,序列化类型,默认hessian为2
    private byte serialize;

    // 必填,消息sequence,long型,值请从0开始递增,每个消息的sequence都不同
    @JsonProperty("seq")
    private long seq;

    //必填,如果调用需要返回结果,固定为1,不需要回复为2,手动回复为3
    private int callType = Constants.CALLTYPE_REPLY;

    // 必填,超时时间,单位毫秒
    private int timeout = 0;

    // 请求创建时间, 不参与序列化传输
    @JsonIgnore
    private transient long createMillisTime;

    //必填,服务名称url,服务唯一的标识
    @JsonProperty("url")
    private String serviceName;

    //必填,服务方法名称
    private String methodName;

    //必填,服务方法的参数值
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
    private Object[] parameters;

    //必填,消息类型,服务调用固定为2,心跳为1,服务框架异常为3,服务调用业务异常为4
    private int messageType = Constants.MESSAGE_TYPE_SERVICE;

    // 旧版上下文信息传递对象
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
    private Object context;

    // 服务版本
    private String version;

    // 必填,调用者所属应用名称,在META-INF/app.properties里的app.name值
    private String app = ConfigManagerLoader.getConfigManager().getAppName();
    
    // 请求体大小
    @JsonIgnore
    private transient int size;
}

DefaultResponse的具体定义如下:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "seq", scope = DefaultResponse.class)
public class DefaultResponse implements InvocationResponse {
   

    /**
     * 不能随意修改!
     */
    private static final long serialVersionUID = 4200559704846455821L;

    private transient byte serialize;

    // 返回的消息sequence,对应发送的消息sequence,long型
    @JsonProperty("seq")
    private long seq;

    //消息类型,服务调用为2,服务调用业务异常为4,服务框架异常为3,心跳为1
    private int messageType;

    // 请求返回异常的相关堆栈信息
    @JsonProperty("exception")
    private String cause;

    // 返回服务调用结果
    @JsonProperty("response")
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
    private Object returnVal;

    // 旧版上下文传递
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
    private Object context;

    // 请求体大小
    @JsonIgnore
    private transient int size;

    // 请求创建时间, 不参与序列化传输
    @JsonIgnore
    private transient long createMillisTime;

    // 响应自定义上下文信息
    private Map<String, Serializable> responseValues = null;

}


Netty3 Handler相关实现

上下游传递原理

在开始分析pigeonRPC通信实现之前,先简略总结下netty3利用ChannelHandler完成通信处理的相关逻辑。
对于netty接收到一个请求后,会调用一系列的拦截器handler来处理我们的请求,可以将请求方看成在下游,服务方的核心处理逻辑在上游,服务方接收到请求后,会将请求不断往上游传递,交由一个个ChannelUpstreamHandler#handleUpstream方法处理,在到达最上游并由核心逻辑处理完后,又交由一个个ChannelDownStreamHandler#handleDownstream处理,到最下游通过网络通信将结果回传给客户端。
基于此,我们可以通过实现ChannelUpstreamHandler和ChannelDownStreamHandler,在请求的上下游传递中,拓展我们的逻辑。

相关实现

ChannelHandlerContext

请求在上下游处理过程中,处理的上下文数据是通过ChannelHandlerContext实现的。比如我们在特定的handler中通过ChannelHandlerContext的sendUpstream和sendDownstream方法将请求传递到下一个handler中处理。对于传递处理的上下文数据,可以通过getAttachment和setAttachment进行读写。

OneToOneDecoder & OneToOneEncoder

请求数据传输在进入服务端之后和返回客户端之前,分别需要进行解码和编码操作,这由OneToOneDecoder和OneToOneEncoder两个抽象类分别实现,两者分别实现了ChannelUpstreamHandler和ChannelDownstreamHandler接口,一般通过继承这两个抽象类,并实现内部的encode或decode方向模版方法来实现具体的编解码操作。

SimpleChannelHandler

SimpleChannelHandler同时实现了ChannelUpstreamHandler和ChannelDownstreamHandler接口,是一个双向handler,内部简单地实现了handleUpstream和handleDownstream,会根据传入ChannelEvent地类型,进行必要地向下转型,得到更加有意义的子类型,而后调用相关的处理方法。默认实现本Handler一般是作为最上层地handler,如果在本Handler之后还需要向更上游传递,需要确保在handleUpstream地最后,手动调用了super.handleUpstream方法。

ChannelEvent

netty有多种事件可以在Channel中传递,交由用户定义的handler处理,这些事件都以ChennelEvent的形式定义,常用有以下事件:

  1. MessageEvent:正常消息请求事件
  2. ChannelStateEvent:channel状态变更事件,包括以下几种:
    1. OPEN: channel开启
    2. BOUND: channel绑定到特定的本地地址
    3. CONNECTED: channel连接到特定的远程地址
    4. INTEREST_OPS: channel对特定感兴趣的操作会进行暂停

pigeon RPC通信的核心实现原理

下面分成服务端和客户端两部分,从这两个类展开分析pigeon

服务端实现

下面先看NettyServerPipelineFactory的实现:

public class NettyServerPipelineFactory implements ChannelPipelineFactory {
   
    // 服务端连接实例引用
    private NettyServer server;

    // 通过编解码工厂,获取单例编解码配置
    private static CodecConfig codecConfig = CodecConfigFactory.createClientConfig();

    public NettyServerPipelineFactory(NettyServer server) {
   
        this.server = server;
    }
    
    // 初始化pipeline
    public ChannelPipeline getPipeline() {
   
        ChannelPipeline pipeline = pipeline();
        pipeline.addLast("framePrepender", new FramePrepender());
        pipeline.addLast("frameDecoder", new FrameDecoder());
        pipeline.addLast("crc32Handler", new Crc32Handler(codecConfig));
        pipeline.addLast("compressHandler", new CompressHandler(codecConfig));
        pipeline.addLast("providerDecoder", new ProviderDecoder());
        pipeline.addLast("providerEncoder", new ProviderEncoder());
        pipeline.addLast("serverHandler", new NettyServerHandler(server));
        return pipeline;
    }

}

对于上面所有的Handler,可以分为3类:

  1. UpStreamHandler
    1. FrameDecoder
    2. ProviderDecoder
    3. NettyServerHandler
  2. DownStreamHandler
    1. FramePrepender
    2. ProviderEncoder
  3. 双向Handler
    1. Crc32Handler
    2. compressHandler

结合代码分析,最终的调用时序如下所示:
在这里插入图片描述
下面根据每个handler的处理顺序,依次分析每个handler的处理逻辑

FrameDecoder

顾名思义,FrameDecoder用来解析出通信管道中一次请求的数据,解决tcp通信中粘包和半包的问题。
pigeon的FrameDecoder继承自netty的org.jboss.netty.handler.codec.frame.FrameDecoder,而org.jboss.netty.handler.codec.frame.FrameDecoder又继承自SimpleChannelUpstreamHandler,在netty实现的FrameDecoder,核心实现方法是messageReceived,大致实现原理是不断读取接收到的字节流,并累加到cumulation变量,通过调用callDecode来尝试对当前累加的字节Buffer cumulation进行解码,直到解析出一个完整请求的feame对象,最后会调用Channels#fireMessageReceived触发Handler的pipeline调用来完成一次完整请求。
在解码过程callDecode中调用了一个抽象模版方法decode来完成具体的解码逻辑,decode方法尝试解析cumulation变量,如果不能按照自定义解析规则解析出一个完整请求的数据包,就返回null,否则返回一个完整的数据包,这里读取成功的同时,需要更新cumulation的字节起始点到当前完整数据包字节的尾部。
分析完Netty的解码流程,具体看看pigeon如何基于自己设计的协议格式来进行数据包解析。

在pigeon中,decode方法在自定义的FrameDecoder中实现,代码尝试先解析出请求头,再通过头部的请求体长度,解析出一次完整请求的包体,在代码中请求尾的长度包含在请求体的长度内部,具体实现如下所示:

public class FrameDecoder extends org.jboss.netty.handler.codec.frame.FrameDecoder {
   


    @Override
    protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer)
            throws Exception {
   

        Object message = null;

        // 如果当前累积的小于两个字节,直接返回null
        if (buffer.readableBytes() <= 2) {
   
            return message;
        }

        byte[] headMsgs = new byte[2];

        // 复制buff两个字节到headMsgs
        buffer.getBytes(buffer.readerIndex(), headMsgs);

        if ((0x39 == headMsgs[0] && 0x3A == headMsgs[1])) {
   
            // 0x39=57,0x3A=58
            //old protocol
            message = doDecode(buffer);
        } else if ((byte) 0xAB == headMsgs[0] && (byte) 0xBA == headMsgs[1]) {
   
            //0xAB=171,0xBA=186
            //new protocol
            message = _doDecode(buffer);

        } else {
   
            throw new IllegalArgumentException("Decode invalid message head:" +
                    headMsgs[0] + " " + headMsgs[1] + ", " + "message:" + buffer);
        }

        return message;

    }

    protected Object doDecode(ChannelBuffer buffer) throws Exception {
   

        CodecEvent codecEvent = null;
        // FRONT_LENGTH = 7,即如果buffer小于消息头7位长度,直接返回null
        if (buffer.readableBytes() <= CodecConstants.FRONT_LENGTH) {
   
            return codecEvent;
        }
        // 从消息的第3位开始,读取4位字节位一个无符号整数,实际位请求体大小
        int totalLength = (int) buffer.getUnsignedInt(
                buffer.readerIndex() +
                        CodecConstants.HEAD_LENGTH);

        // 最后包体大小是请求体大小+请求头大小
        int frameLength = totalLength + CodecConstants.FRONT_LENGTH;

        // 当前累积的buffer是否已经包含一个完整的数据包
        if (buffer.readableBytes() >= frameLength) {
   

            // 获取具体数据包字节内容
            ChannelBuffer frame = buffer.slice(buffer.readerIndex(), frameLength);
            // 更新累积缓存的读起点,方便读取处理下一个数据包
            buffer.readerIndex(buffer.readerIndex() + frameLength);

            // 用CodecEvent包装frame,并标记位非统一协议
            codecEvent = new CodecEvent(frame, false);
            // 设置接收时间
            codecEvent.setReceiveTime(System.currentTimeMillis());
        }

        return codecEvent;
    }

    protected Object _doDecode(ChannelBuffer buffer)
            throws Exception {
   
        CodecEvent codecEvent = null;

        // _FRONT_LENGTH = 10,即如果buffer小于消息头10位长度,直接返回null
        if (buffer.readableBytes() <= CodecConstants._FRONT_LENGTH) {
   
            return codecEvent;
        }

        // 从消息的第4位开始,读取4位字节位一个无符号整数,实际位请求体大小
        int totalLength = (int) (buffer.getUnsignedInt(
                buffer.readerIndex() +
                        CodecConstants._HEAD_LENGTH));

        // 最后包体大小是请求体大小+请求头大小
        int frameLength = totalLength + CodecConstants._FRONT_LENGTH_;

        // 当前累积的buffer是否已经包含一个完整的数据包
        if (buffer.readableBytes() >= frameLength) {
   

            // 获取具体数据包字节内容
            ChannelBuffer frame = buffer.slice(buffer.readerIndex(), frameLength);
            // 更新累积缓存的读起点,方便读取处理下一个数据包
            buffer.readerIndex(buffer.readerIndex() + frameLength);

            // 用CodecEvent包装frame,并标记位统一协议
            codecEvent = new CodecEvent(frame, true);
            // 设置接收时间
            codecEvent.setReceiveTime(System.currentTimeMillis());
        }

        return codecEvent;
    }

}

Crc32Handler

Crc32Handler主要用于校验统一协议请求的数据完整性,在解析出完整消息包长度数据之后,在解码为DefaultRequest之前,会先获取实际的数据包数据,计算crc32校验和,再和消息尾部传入的校验和进行比对,如果一致,说明校验通过,否则校验失败。而在请求结束发送相应时,又会对数据计算校验和,放在消息包尾中,以便客户端获取校验。
看看代码的具体实现:

public class Crc32Handler extends SimpleChannelHandler {
   

    private static final Logger logger = LoggerLoader.getLogger(Crc32Handler.class);

    private static ThreadLocal<Adler32> adler32s = new ThreadLocal<Adler32>();

    private CodecConfig codecConfig;

    public Crc32Handler(CodecConfig codecConfig) {
   
        this.codecConfig = codecConfig;
    }

    // 上游接收数据处理
    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
   
        if (e.getMessage() == null || !(e.getMessage() instanceof CodecEvent))
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值