深入理解Dubbo原理系列(四)- Dubbo核心调用和通信

  • 一. Dubbo调用
  • 二. Dubbo协议详解
  • 2.1 Dubbo协议架构
  • 2.2 编解码器
  • 2.2.1 Dubbo请求Request编解码器
  • Request编码器
  • Request解码器
  • 2.2.2 Dubbo响应Response编解码器
  • Response编码器
  • Response解码器
  • 三. Telnet调用原理
  • 四. Dubbo核心Handler和线程模型
  • 4.1 核心Handler介绍
  • 4.2 Dubbo请求响应Handler
  • 总结


一. Dubbo调用

Dubbo调用流程图如下:

java dubbo间的调用 dubbo的整个调用过程_RPC


按照图中的标号顺序:

  1. 将多个服务提供者做一个聚合,

在Dubbo框架内部中,Directory接口的其中一个实现RegistryDirectory 类,它和接口名是一对一的关系(每一个接口都有一个RegistryDirectory实例),主要负责拉取 和订阅服务提供者、动态配置和路由项。

  1. 客户端服务调用首先触发路由操作。
  2. 然后将路由结果得到的服务列表作为负载均衡的参数。
  3. 经过负载均衡后会选出一台机器进行RPC调用。
  4. 客户端经过路由和负载均衡后,则将请求交给底层的IO线程池来处理,这里包含两种:Netty、Dubbo业务线程池。
  5. 在编解码层读取流中的字符串,最终交给Telnet对应的Handler 去解析方法调用。
  6. 进行端口复用,如果是Telnet调用(序列化反序列化使用的是fastjson),则先找到对应的Invoker进行方法调用。

而本篇文章主要将中心放到Dubbo协议、编解码实现和线程模型上。

二. Dubbo协议详解

2.1 Dubbo协议架构

首先,一次RPC调用,一般会分为:

  1. 协议头(16字节):主要携带了魔法数 (Oxdabb),以及当前请求报文类型(Request、Response)、心跳和事件等信息。
  2. 协议体

如下图所示(中间的内容表示对应比特位范围内存储的数据):

java dubbo间的调用 dubbo的整个调用过程_序列化_02


根据偏移比特位来进行划分:

偏移比特位

字段描述

具体标识

0-7

魔数高位

存储的是魔法数高位(OxdaOO)

8-15

魔数低位

存储的是魔法数高位(Oxbb)

16

数据包类型

0:Response;1:Request

17

调用方式

仅在第16位被设为1的情况下有效。 0:单向调用,1:双向调用。

18

时间标识

0:当前数据包是请求或响应包。1:当前数据包是心跳包。

19-23

序列化器编号

2:Hessian2Serialization 。 3:JavaSerialization。4: CompactedJavaSerialization。 6:FastJsonSerialization。7:NativeJavaSerialization。8:KryoSerialization。9:FstSerialization 。

24-31

状态

看状态响应码表

32-95

请求编号

这8个字节存储RPC请求的唯一ID,用来将请求和响应做关联

96-127

消息体长度

占用的4个字节存储消息体长度。在一次RPC请求过程中,消息体中依次会存储7部分内容

状态响应码表(存储于协议报文头部的status中):

状态值

状态符号

意义

20

OK

正确返回

30

CLIENT TIMEOUT

客户端超时

31

SERVERTIMEOUT

服务端超时

40

BADREQUEST

请求报文格式错误

50

BADRESPONSE

响应报文格式错误

60

SERVICE NOT FOUND

未找到匹配的服务

70

SERVICEERROR

服务调用错误

80

SERVER ERROR

服务端内部错误

90

CLIENTERROR

客户端错误

100

SERVER THREADPOOL EXHAUSTED ERROR

服务端线程池满拒绝执行

到这里为止,讲的都是Dubbo协议头,那么再来看下Dubbo协议体存储什么东西


从比特位128之后,就是协议体的内容了,Dubbo协议中,其协议体的数据存储非常讲究,意思是存储的内容是严格按照一定顺序来的,如下:

  1. Dubbo version
  2. Service name
  3. Service version
  4. Method name
  5. Method parameter types
  6. Method arguments
  7. Attachments

2.2 编解码器

我们知道,数据要想在网络上进行传输,肯定需要某种协议进行约束和规范,比如TCP协议这种。但是数据在网络传输的时候,难免会遇到粘包、拆包的情况。对于主流协议的解决方案大致有4种:

  • 将消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格。
  • 在包尾增加回车换行符进行分割,例如FTP协议。
  • 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。
  • 构造一个更复杂的应用层协议。

对于Dubbo而言,则是用特殊符号exdabb魔法数来分割处理粘包问题的具体细节后文会讲,结合代码来说)。而这一类实现肯定离不开编解码器的功劳。对于Dubbo中,所有的编解码层实现都应该继承Exchangecodec类,Exchange又扮演着把消息体解析为request和response的角色。因此来看下他的代码:

2.2.1 Dubbo请求Request编解码器

Request编码器

首先来看下入口方法,很明显会根据请求的类型来进行编码:

public class ExchangeCodec extends TelnetCodec {
	@Override
    public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
        if (msg instanceof Request) {
            encodeRequest(channel, buffer, (Request) msg);
        } else if (msg instanceof Response) {
            encodeResponse(channel, buffer, (Response) msg);
        } else {
            super.encode(channel, buffer, msg);
        }
    }
}

1.请求对象编码encodeRequest(),这里也可以看出来,

protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
    // 1.获取指定或者默认的序列化协议(Hessian2)
    Serialization serialization = getSerialization(channel);
    // 2.构造16字节头 HEADER_LENGTH=16
    byte[] header = new byte[HEADER_LENGTH];
    // 3.占用2个字节来存储魔法数
    Bytes.short2bytes(MAGIC, header);
    // 4.在第3个字节(16位和19〜23位)分别存储请求标志和序列化协议序号
    header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
    // 5.设置请求/响应标记
    if (req.isTwoWay()) {
        header[2] |= FLAG_TWOWAY;
    }
    if (req.isEvent()) {
        header[2] |= FLAG_EVENT;
    }
    // 6.设置请求唯一标识
    Bytes.long2bytes(req.getId(), header, 4);
    // 7.用来跳过buffer头部的16个字节,用来序列化消息体的
    int savedWriteIndex = buffer.writerIndex();
    buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
    ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
    ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
    if (req.isEvent()) {
        encodeEventData(channel, out, req.getData());
    } else {
        // 8.序列化请求的调用,data一般是RpcInvocation
        encodeRequestData(channel, out, req.getData(), req.getVersion());
    }
    out.flushBuffer();
    if (out instanceof Cleanable) {
        ((Cleanable) out).cleanup();
    }
    bos.flush();
    bos.close();
    int len = bos.writtenBytes();
    // 9.查是否超过默认8MB大小
    checkPayload(channel, len);
    // 10.将消息长度写入头部第12个字节的偏移量
    Bytes.int2bytes(len, header, 12);
    // 11.定位指针到协议头部开始的地方
    buffer.writerIndex(savedWriteIndex);
    // 12.写入完整的报文头到buffer
    buffer.writeBytes(header); 
    // 13.写完后,定位指针到消息体结束的地方
    buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}

2.编码请求对象体对应上述代码中注释8),encodeRequestData()方法,这部分主要就是对接口、方法、 方法参数类型、方法参数等进行编码。 ,对应DubboCodecencodeRequestData()方法:

@Override
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
    RpcInvocation inv = (RpcInvocation) data;
	// 1.写入框架的版本
    out.writeUTF(version);
    // https://github.com/apache/dubbo/issues/6138
    String serviceName = inv.getAttachment(INTERFACE_KEY);
    if (serviceName == null) {
        serviceName = inv.getAttachment(PATH_KEY);
    }
    // 2.写入调用的接口
    out.writeUTF(serviceName);
    // 3.写入接口指定的版本,默认是0.0.0
    out.writeUTF(inv.getAttachment(VERSION_KEY));
	// 4.写入方法名称
    out.writeUTF(inv.getMethodName());
    // 5.写入方法参数类型
    out.writeUTF(inv.getParameterTypesDesc());
    // 6.依次写入方法的参数值
    Object[] args = inv.getArguments();
    if (args != null) {
        for (int i = 0; i < args.length; i++) {
            out.writeObject(encodeInvocationArgument(channel, inv, i));
        }
    }
    // 7.写入隐式参数
    out.writeAttachments(inv.getObjectAttachments());
}

我们从方法中可以看出来,这和Dubbo消息体规定的写入顺序是一致的。接下来再来看下Request的解码器:

Request解码器

Request的解码器入口如下:

@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
    int readable = buffer.readableBytes();
    byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
    buffer.readBytes(header);
    return decode(channel, buffer, readable, header);
}

decode(channel, buffer, readable, header)

@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
    // 1.检查魔法数,处理流起始处并不是Dubbo魔法数0xdabb的场景
    if (readable > 0 && header[0] != MAGIC_HIGH
            || readable > 1 && header[1] != MAGIC_LOW) {
        int length = header.length;
        // 2.若流中还有数据可以读取
        if (header.length < readable) {
            // 3.那么为header重新分配空间,用来存储流中所有可读字节
            header = Bytes.copyOf(header, readable);
            // 4.将剩余的字节读取到header中
            buffer.readBytes(header, length, readable - length);
        }
        for (int i = 1; i < header.length - 1; i++) {
            if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
                // 5.将buffer读指针指向Dubbo报文头开始处:0xdabb
                buffer.readerIndex(buffer.readerIndex() - header.length + i);
                // 6.流起始处至下一个Dubbo报文之间的数据放到header中
                header = Bytes.copyOf(header, i);
                break;
            }
        }
        // 7.主要用于解析header数据,比如用于Telnet
        return super.decode(channel, buffer, readable, header);
    }
    // 8.如果读取数据长度小于16个字节,则进行等待,需要更多数据
    if (readable < HEADER_LENGTH) {
        return DecodeResult.NEED_MORE_INPUT;
    }

    // 10.取头部存储的报文长度,并校验长度是否超过限制
    int len = Bytes.bytes2int(header, 12);
    checkPayload(channel, len);
    int tt = len + HEADER_LENGTH;
    // 11.校验是否可以读取完整Dubbo报文,否则期待更多数据
    if (readable < tt) {
        return DecodeResult.NEED_MORE_INPUT;
    }
    // limit input stream.
    ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);

    try {
        // 12.解码消息体,is参数是完整的RPC调用报文
        return decodeBody(channel, is, header);
    } finally {
        // 13.如果解码过程有问题,则跳过这次RPC调用报文
        if (is.available() > 0) {
            try {
                if (logger.isWarnEnabled()) {
                    logger.warn("Skip input stream " + is.available());
                }
                StreamUtils.skipUnusedStream(is);
            } catch (IOException e) {
                logger.warn(e.getMessage(), e);
            }
        }
    }
}

解码消息体:

@Override
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
    byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
    long id = Bytes.bytes2long(header, 4);
    if ((flag & FLAG_REQUEST) == 0) {
        // 解码响应体分支
    } else {
        // 解码请求体分支
        // 1.请求标志位被设置,创建Request对象
        Request req = new Request(id);
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay((flag & FLAG_TWOWAY) != 0);
        if ((flag & FLAG_EVENT) != 0) {
            req.setEvent(true);
        }
        try {
            Object data;
            if (req.isEvent()) {
                ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
                data = decodeEventData(channel, in);
            } else {
                DecodeableRpcInvocation inv;
                if (channel.getUrl().getParameter(DECODE_IN_IO_THREAD_KEY, DEFAULT_DECODE_IN_IO_THREAD)) {
                	// 2.在I/O线程中直接解码
                    inv = new DecodeableRpcInvocation(channel, req, is, proto);
                    inv.decode();
                } else {
                	// 3.交给Dubbo业务线程池解码
                    inv = new DecodeableRpcInvocation(channel, req,
                            new UnsafeByteArrayInputStream(readMessageData(is)), proto);
                }
                data = inv;
            }
            // 4.将Rpclnvocation作为Request的数据域
            req.setData(data);
        } catch (Throwable t) {
            if (log.isWarnEnabled()) {
                log.warn("Decode request failed: " + t.getMessage(), t);
            }
            // 5.解码失败,先做标记并存储异常
            req.setBroken(true);
            req.setData(t);
        }

        return req;
    }
}

针对注释2和3,我们可以看到Dubbo解码过程中,把消息体转换成Rpclnvocation对象,具体解码会触发 DecodeableRpcInvocationdecode 方法,可以看出解码的流程严格按照编码的参数顺序来进行。

@Override
public Object decode(Channel channel, InputStream input) throws IOException {
    ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
            .deserialize(channel.getUrl(), input);
	// 1.读取框架版本
    String dubboVersion = in.readUTF();
    request.setVersion(dubboVersion);
    setAttachment(DUBBO_VERSION_KEY, dubboVersion);
    String path = in.readUTF();
    // 2.读取调用的接口
    setAttachment(PATH_KEY, path);
    // 3.读取接口指定的版本,默认是0.0.0
    setAttachment(VERSION_KEY, in.readUTF());
	// 4.读取方法名称
    setMethodName(in.readUTF());
    String desc = in.readUTF();
    // 5.读取方法参数类型
    setParameterTypesDesc(desc);
    try {
        // 。。。代码省略 
        // 依次读取方法的参数值
        args[i] = in.readObject(pts[i]);
        // 。。。代码省略
        for (int i = 0; i < args.length; i++) {
        	// 处理异步参数的回调,如果有则在服务端创建一个reference代理实例
            args[i] = decodeInvocationArgument(channel, this, pts, i, args[i]);
        }
		
    } catch (ClassNotFoundException e) {
        throw new IOException(StringUtils.toString("Read invocation data failed.", e));
    } finally {
        if (in instanceof Cleanable) {
            ((Cleanable) in).cleanup();
        }
    }
    return this;
}

针对Request的编解码,我们可以总结出以下结论:

结论1:对于Dubbo处理粘包拆包问题的解决:

  1. 在解码过程中,首先先判断此次传输的信息包的大小。
  2. 根据传输包的大小,确定本次传输的信息是否包含整个请求头,取与请求头固定长度比较最小值,然后读取相关信息到header中。如果此次信息包大于等于16字节,说明请求头是完整的。(查看解码器入口代码)
  3. 对于请求头的检查,会先检查魔法数,若请求头不完整则直接返回(可以说只有请求头是完整的,才会解析请求体)。
  4. 若发现拆包,直接返回,并进入等待状态。

结论2:为什么Dubbo协议要比普通的Http协议快点:

  1. 请求头较小,16字节,不会存储什么额外的信息。
  2. Http的编解码工作还会由Http服务器在做一层编解码,最后再由我们自己的应用来做一次编解码,比如转换成JSON。但是对于Dubbo一般不需要二次编码,而是直接编码二进制,然后传输(可参考Request编码部分)。

2.2.2 Dubbo响应Response编解码器

Response编码器

Response解码入口同样是:

public class ExchangeCodec extends TelnetCodec {
	@Override
    public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
        if (msg instanceof Request) {
            encodeRequest(channel, buffer, (Request) msg);
        } else if (msg instanceof Response) {
            encodeResponse(channel, buffer, (Response) msg);
        } else {
            super.encode(channel, buffer, msg);
        }
    }
}

来看下encodeResponse()方法:

protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
    int savedWriteIndex = buffer.writerIndex();
    try {
        // 1.获取指定或默认的序列化协议(Hessian2)
        Serialization serialization = getSerialization(channel);
        // 2.构造 16 字节头
        byte[] header = new byte[HEADER_LENGTH];
        // 3.占用2个字节来存储魔法数
        Bytes.short2bytes(MAGIC, header);
        // 4.在第3个字节(16位和19〜23位)存储响应标志
        header[2] = serialization.getContentTypeId();
        if (res.isHeartbeat()) {
            header[2] |= FLAG_EVENT;
        }
        // 5.在第4个字节存储响应状态
        byte status = res.getStatus();
        header[3] = status;
        // 6.设置请求唯一标识
        Bytes.long2bytes(res.getId(), header, 4);
        // 7.空出16字节头部用于存储响应体报文
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
        ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
        ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
        // encode response data or error message.
        if (status == Response.OK) {
            if (res.isHeartbeat()) {
                encodeEventData(channel, out, res.getResult());
            } else {
                // 8.编码响应体
                encodeResponseData(channel, out, res.getResult(), res.getVersion());
            // ...省略
            }
        }    
        // 9.查是否超过默认的8MB大小
        int len = bos.writtenBytes();
        checkPayload(channel, len);
        // 10.向消息长度写入头部第12个字节偏移量
        Bytes.int2bytes(len, header, 12);
        // 定位指针到报文头部开始位置
        buffer.writerIndex(savedWriteIndex);
        // 写入完整报文头部到 buffer
        buffer.writeBytes(header);
        // 写完后,定位指针到消息体结束的地方
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
    } catch (Throwable t) {
        // 若失败,则复位buffer、并将编码响应的异常发送给consumer,否则只能等待到超时
        // 告知客户端数据包的长度发生限制或者编码失败的具体原因。        
    }
}

响应体的编码:DubboCodec.encodeResponseData()

@Override
protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
    Result result = (Result) data;
    // 1.判断客户端请求的版本是否支持服务端参数返回
    boolean attach = Version.isSupportResponseAttachment(version);
    Throwable th = result.getException();
    if (th == null) {
        // 2.提取正常返回结果
        Object ret = result.getValue();
        if (ret == null) {
            // 3.在编码结果前,先写一个字节标志
            out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS : RESPONSE_NULL_VALUE);
        } else {
            out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS : RESPONSE_VALUE);
            // 4.分别写一个字节标记和调用结果
            out.writeObject(ret);
        }
    } else {
        // 5.标记调用抛异常,并序列化异常
        out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS : RESPONSE_WITH_EXCEPTION);
        out.writeThrowable(th);
    }

    if (attach) {
        // 6.记录服务端Dubbo的版本,并返回服务端隐式参数
        result.getObjectAttachments().put(DUBBO_VERSION_KEY, Version.getProtocolVersion());
        out.writeAttachments(result.getObjectAttachments());
    }
}
Response解码器

其实这里的部分和Request是一样的,接着上文中的代码,从注释12起:decodeBody()方法,只是根据RequestResponse类型不同,代码执行的if条件分支不一样。本质上其实差不多(就不讲了)

@Override
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
    byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
    // get request id.
    long id = Bytes.bytes2long(header, 4);
    if ((flag & FLAG_REQUEST) == 0) {
        // 解码响应体
    } else {
        // 解码请求体
    }
}

不同的是,响应体解码后,需要进行返回,最后将结果进行返回的时候,会触发DecodeableRpcResultdecode方法,主要针对不同的响应状态来对返回值的进行数据封装

@Override
public Object decode(Channel channel, InputStream input) throws IOException {
    if (log.isDebugEnabled()) {
        Thread thread = Thread.currentThread();
        log.debug("Decoding in thread -- [" + thread.getName() + "#" + thread.getId() + "]");
    }

    ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
            .deserialize(channel.getUrl(), input);

    byte flag = in.readByte();
    switch (flag) {
    	// 返回结果标记为Null值
        case DubboCodec.RESPONSE_NULL_VALUE:
            break;
        case DubboCodec.RESPONSE_VALUE:
        	// 读取方法调用返回值类型
            handleValue(in);
            break;
        case DubboCodec.RESPONSE_WITH_EXCEPTION:
        	// 会保存读取的返回值异常结果
            handleException(in);
            break;
        case DubboCodec.RESPONSE_NULL_VALUE_WITH_ATTACHMENTS:
       		// 读取返回值是Null,并且包含隐式参数
            handleAttachment(in);
            break;
        case DubboCodec.RESPONSE_VALUE_WITH_ATTACHMENTS:
        	// 读取返回时不是Null,并且包含隐式参数
            handleValue(in);
            handleAttachment(in);
            break;
        case DubboCodec.RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS:
            handleException(in);
            handleAttachment(in);
            break;
        default:
            throw new IOException("Unknown result flag, expect '0' '1' '2' '3' '4' '5', but received: " + flag);
    }
    if (in instanceof Cleanable) {
        ((Cleanable) in).cleanup();
    }
    return this;
}

最后的最后,响应码有这么几种:

状态值

状态符号

意义

5

RESPONSE NULL VALUE WITH ATTACHMENTS

响应空值包含隐藏参数

4

RESPONSE VALUE WITH ATTACHMENTS

响应结果包含隐藏参数

3

RESPONSE WITH EXCEPTION WITH ATTACHMENTS

异常返回包含隐藏参数

2

RESPONSENULLVALUE

响应空值

1

RESPONSE VALUE

响应结果

0

RESPONSE WITH EXCEPTION

异常返回

三. Telnet调用原理

上文主要是讲了Dubbo的一个编码解码的实现,其实,编解码器的处理有三种场景:

  • 请求。
  • 响应。
  • Telnet调用。

对于Telnet调用,主要是将Telnet当做明文的字符串来处理,按照Dubbo的调用规范,解析成调用命令格式,然后查找对应的Invoker,发起方法调用即可。

1.在Dubbo中,Telnet指令解析被设置成了一个扩展点TelnetHandler每个Telnet指令都会实现这个扩展点。

@SPI
public interface TelnetHandler {
	// message包含处理命令之外的所有字符串参数。
    String telnet(Channel channel, String message) throws RemotingException;
}

2.完成Telnet指令转发的核心实现类是TelnetHandlerAdapter

public class TelnetHandlerAdapter extends ChannelHandlerAdapter implements TelnetHandler {
	@Override
    public String telnet(Channel channel, String message) throws RemotingException {
        // ...
        if (message.length() > 0) {
            int i = message.indexOf(' ');
            if (i > 0) {
            	// 1.提取执行的命令
                command = message.substring(0, i).trim();
               	// 2.提取命令后的所有字符串信息
                message = message.substring(i + 1).trim();
            } else {
                command = message;
                message = "";
            }
        } else {
            command = "";
        }
        if (command.length() > 0) {
        	// 3.检查系统是否有命令对应的扩展点
            if (extensionLoader.hasExtension(command)) {
                if (commandEnabled(channel.getUrl(), command)) {
                    try {
                    	// 4.如果有,那么交给具体的扩展点去执行
                        String result = extensionLoader.getExtension(command).telnet(channel, message);
                        if (result == null) {
                            return null;
                        }
                        buf.append(result);
                    } catch (Throwable t) {
                        buf.append(t.getMessage());
                    }
                } 
                // ...
        }
        if (buf.length() > 0) {
        	// 5.在Telnet消息结尾追加回车和换行
            buf.append("\r\n");
        }
        // ...
        return buf.toString();
    }
}

其实现总结就是:

  1. 将用户输入的指令识别成command,然后将剩余的内容解析成message。
  2. message则交给命令的实现者去处理。

3.完成Telnet的指令转发后,则实现命令的调用了,具体实现在于InvokeTelnetHandler.telnet()方法:

public class InvokeTelnetHandler implements TelnetHandler {
	public String telnet(Channel channel, String message) {
        // ...
        // 1.提取调用方法,由接口名、方法名组成
        String method = message.substring(0, i).trim();
        // 2.提取调用方法的参数值
        String args = message.substring(i + 1, message.length() - 1).trim();
        i = method.lastIndexOf(".");
        if (i >= 0) {
        	// 3.提取方法前面的接口和法法名称
            service = method.substring(0, i).trim();
            method = method.substring(i + 1).trim();
        }

        List<Object> list;
        try {
        	// 4.将参数JSON串转换成JSON对象
            list = JSON.parseArray("[" + args + "]", Object.class);
        } catch (Throwable t) {
            return "Invalid json argument, cause: " + t.getMessage();
        }
        StringBuilder buf = new StringBuilder();
        // ...
        } else {
            for (ProviderModel provider : ApplicationModel.allProviderModels()) {
                // 5.以接口名、方法、参数值、类型等作为检索方法的条件进行方法的筛选
        }
        // ...
        if (selectedProvider != null) {
            if (invokeMethod != null) {
                try {
                	// 6.将JSON参数值转换成JAVA对象
                    Object[] array = realize(list.toArray(), invokeMethod.getParameterTypes(),
                            invokeMethod.getGenericParameterTypes());
                    long start = System.currentTimeMillis();
                    AppResponse result = new AppResponse();
                    try {
                    	// 7.根据查找到的Invoker、构造Rpclnvocation进行方法调用
                        Object o = invokeMethod.invoke(selectedProvider.getServiceInstance(), array);
                        // ...
               
        return buf.toString();
    }
}
  1. 当用户输入invoke指令的时候,会被转发到InvokeTelnetHandler类中进行Telnet调用。
  2. 提取方法调用信息、参数值、接口、方法等信息后,进行筛选,去查找对应的方法和Invoker对象
  3. 在真正进行方法调用前,需要将JSON对象转换成Java对象,然后触发方法调用并返回结果值。

四. Dubbo核心Handler和线程模型

4.1 核心Handler介绍

我以前的文章也有提及,从代码实现也可以看到,Dubbo的实现和Netty息息相关(默认情况下),而Netty就有Channel管道这种概念,因此Dubbo内部自然有个组件叫做:ChannlHandler

Dubbo框架内部使用大量Handler组成类似链表,依次处理具体逻辑,比如编解码、心跳时间戳和方法调用Handler因为Netty每次创建Handler都会经过ChannelPipeline, 大量的事件经过很多Pipeline会有较多的开销,因此Dubbo会将多个Handler聚合为一个Handler

来看下ChannlHandler的5种状态:

  • connected:Channel已经被创建
  • disconnected:Channel已经被断开
  • sent:消息被发送
  • received:消息被接收
  • caught:捕获到异常

而Dubbo会针对每个特性都会实现对应的ChannelHandler,这些Handler最终会和底层通信框架做关联,比如Netty。因此接下来看下RPC调用服务方处理Handler的逻辑:在DubboProtocol通过内部类继承ExchangeHandlerAdapte完成服务提供方Invoker实例的查找并进行服务的真实调用。

public class DubboProtocol extends AbstractProtocol {
	private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
		@Override
        public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
            // ...
            Invocation inv = (Invocation) message;
            // 1.查找invocation关联的Invoker,即查找当前已经暴露的服务。
            // 在服务暴露时服务端已经按照特定规则(端口、接口名、接口版本和接口分组)把实例Invoker存储到HashMap中了
            // 客户端调用过来时必须携带相同信息构造的key,找到对应Exporter然后调用
            Invoker<?> invoker = getInvoker(channel, inv);
            // ...
            RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
            // 2.调用业务方具体方法
            Result result = invoker.invoke(inv);
            return result.thenApply(Function.identity());
        }
	}
}

紧接着,跟进getInvoker(channel, inv)方法,即服务端Invoker的查找过程:

Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {
    boolean isCallBackServiceInvoke = false;
    boolean isStubServiceInvoke = false;
    // 1.获取服务暴露协议的端口
    int port = channel.getLocalAddress().getPort();
    // 2.获取调用传递的接口
    String path = (String) inv.getObjectAttachments().get(PATH_KEY);
	// ...省略,主要是异步参数回调的一些处理逻辑
	// 3.根据端口、接口名、接口分组和版本构造唯一的key
    String serviceKey = serviceKey(
            port,
            path,
            (String) inv.getObjectAttachments().get(VERSION_KEY),
            (String) inv.getObjectAttachments().get(GROUP_KEY)
    );
    // 4.从存储着暴露服务的集合map中,根据上文得到的唯一key,获取实例Invoker(封装于Exporter对象中)
    DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
	// 5.返回所需要的Invoker实例即可
    return exporter.getInvoker();
}

而Dubbo为了编织这些Handler,适应不同的场景,提供了一套可以定制的线程模型,一般有两种:

  • I/O线程池:负责读写报文,如Netty。
  • Dubbo业务线程池:负责处理一些耗时的事件,比如读写数据库的操作。

这是我从Dubbo官网上截图下来的,他代表Dubbo实现线程派发的一个流程:

java dubbo间的调用 dubbo的整个调用过程_java dubbo间的调用_03


在进行一个补充:

  • Dispatcher:线程池派发器。是Dubbo中的扩展点负责创建具有线程派发能力的 ChannelHandler

而这一类的ChannelHandler目前有6种策略调用:

分发策略

分发实现类

作用

all

AllDispatcher

将所有I/O事件交给Dubbo线程池处理,Dubbo默认启用

connection

ConnectionOrderedDispatcher

单独线程池处理连接断开事件,和Dubbo线程池分开

direct

DirectDispatcher

所有方法调用和事件处理在I/O线程中

execution

ExecutionDispatcher

只在线程池处理接收请求,其他事件在I/O线程池中

message

MessageOnlyChannelHandler

只在线程池处理请求和响应事件,其他事件在I/O线程池中

mockdispatcher

MockDispatcher

默认返回Null

4.2 Dubbo请求响应Handler

Dubbo毕竟是一个RPC框架,那么主要的肯定还是讲它的有关请求响应的Handler了。在Dubbo框架内部,所有方法调用会被抽象成Request/Response,每次调用都会创建一个请求Request,如果是方法调用则会返回一个Response对象。Dubbo中使用HeaderExchangeHandler类来进行处理:

  • 更新发送和读取请求时间戳。
  • 判断请求格式或编解码是否有错,并响应客户端失则的具体原因。
  • 处理Request请求和Response正常响应。
  • 支持上文所提到的Telnet调用。

请求响应Handler实现: 入口于HeaderExchangeHandler类下的received方法:

@Override
public void received(Channel channel, Object message) throws RemotingException {
    final ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
    // 接收请求
    if (message instanceof Request) {
        Request request = (Request) message;
        if (request.isEvent()) {
        	// 处理处理readonly事件,在channel中打标
            handlerEvent(channel, request);
        } else {
            if (request.isTwoWay()) {
            	// 处理方法的调用,并将结果返回给客户端
                handleRequest(exchangeChannel, request);
            } else {
                handler.received(exchangeChannel, request.getData());
            }
        }
    } else if (message instanceof Response) {
    	// 接收响应
        handleResponse(channel, (Response) message);
    } else if (message instanceof String) {
    	// 若客户端不支持Telnet调用
        if (isClientSide(channel)) {
            Exception e = new Exception("Dubbo client can not supported string message: " + message + " in channel: " + channel + ", url: " + channel.getUrl());
            logger.error(e.getMessage(), e);
        } else {
        	// 触发Telnet调用,并将结果返回
            String echo = handler.telnet(channel, (String) message);
            if (echo != null && echo.length() > 0) {
                channel.send(echo);
            }
        }
    } else {
        handler.received(exchangeChannel, message);
    }
}

额一看到上述的代码中,会针对message的类型(Response或者Request)类进行对应的处理:

  • 处理请求报文:handleRequest()
  • 处理响应报文:handleResponse()

handleRequest()HeaderExchangeHandler类中):

void handleRequest(final ExchangeChannel channel, Request req) throws RemotingException {
    Response res = new Response(req.getId(), req.getVersion());
    if (req.isBroken()) {
        Object data = req.getData();

        String msg;
        if (data == null) {
            msg = null;
        } else if (data instanceof Throwable) {
        	// 1.处理请求格式不正确(编解码),并把异常传换成字符串返回
            msg = StringUtils.toString((Throwable) data);
        } else {
            msg = data.toString();
        }
        res.setErrorMessage("Fail to decode request due to: " + msg);
        res.setStatus(Response.BAD_REQUEST);

        channel.send(res);
        return;
    }
    // find handler by message class.
    Object msg = req.getData();
    try {
    	// 2. 调用DubboProtocol的reply方法,进行方法调用()
        CompletionStage<Object> future = handler.reply(channel, msg);
        // ....
    } catch (Throwable e) {
    	// 方法调用失败的处理逻辑,返回状态和错误信息
        res.setStatus(Response.SERVICE_ERROR);
        res.setErrorMessage(StringUtils.toString(e));
        channel.send(res);
    }
}

handleResponse()的实现就比较简单:

static void handleResponse(Channel channel, Response response) throws RemotingException {
    if (response != null && !response.isHeartbeat()) {
    	// 唤醒阻塞的线程并通知结果
        DefaultFuture.received(channel, response);
    }
}

这里做个解释:

  1. 因为Dubbo在发送请求的时候,会在DefaultFuture类中保存请求对象并阻塞请求线程。
  2. 那么进行handleResponse()响应返回的时候,会唤醒阻塞线程并将Response中的结果通知调用方。

总结

Dubbo客户端调用远程服务流程?

1.将多个服务提供者进行聚合。
2.客户端中,经历路由、负载均衡。筛选出某一个机器进行RPC调用,将调用的请求交给底层Netty来处理。
3.在编解码层会进行请求的处理,最终交给Telnet对应的Handler进行处理。
4.服务端接收到请求,进行解码,进行处理并返回数据,客户端获得响应数据。

Dubbo协议讲了什么?

1.Dubbo协议分为协议头和协议体(保存具体的数据,并且数据按照一定顺序来存储
2.编解码器根据Dubbo协议的规定,按照上述的顺序规范进行数据的编码、解码操作。
3.Dubbo利用魔法数exdabb来处理粘包和拆包问题若数据不够,则进入等待状态,只有完整的数据,其请求体才能够被解析。
3.Dubbo协议的优点:请求头较小,Dubbo一般不需要二次编码,传输效率高。

Dubbo中的Telnet调用是什么?

1.将Telnet当做明文的字符串来处理,按照Dubbo的调用规范,解析成调用命令格式,然后查找对应的Invoker,发起方法调用。
2.在Dubbo中,Telnet指令解析被设置成了一个扩展点:TelnetHandler。

Dubbo核心Handler说了什么?

1.ChannlHandler是Dubbo的一个核心组件,Dubbo会针对每个特性都会实现对应的ChannelHandler,这些Handler最终会和底层通信框架做关联。
2.而Dubbo为了编织这些Handler,适应不同的场景,提供了一套可以定制的线程模型,一般有两种:
----I/O线程池:负责读写报文。
----Dubbo业务线程池:负责业务的处理。

总的来说,将上面的话题串起来就是:

  1. 客户端发起一个RPC调用的请求,那么前提是对于客户端而言,有相关的服务可以使用,那么这个过程需要经历路由操作、负载均衡,最终选取某一个服务发起调用的请求。
  2. 在客户端中,会将调用请求Request通过编码器进行序列化操作。并将请求抽象成Request,这类请求交给HeaderExchangeHandler类来处理。
  3. 对于客户端而言的HeaderExchangeHandler先判断请求的格式、编码是否有错,若没有错误则处理Request请求,并负责响应最终的结果且返回。
  4. 处理Request请求,则统一的调用DubboProtocol下的reply()方法(此时请求线程会被堵塞,请求对象也会被保存于DefaultFuture中),主要做的事情是:

4.1 查找当前已经暴露的服务。在服务暴露时,服务端已经按照特定规则(端口、接口名、接口版本和接口分组)把实例Invoker存储到HashMap中了,因此客户端调用时必须携带相同信息构造的key,找到对应Invoker。
4.2 获得Invoker后,则调用其invoke方法。

  1. 相关的请求到达服务端后,服务端同样需要将请求进行解码。(此时客户端和服务端之间已经建立起了Netty连接,即存在一个IO线程池负责读写报文),服务端需要通过分发策略进行对应请求的处理,最后将结果进行返回。
  2. 对于服务端而言的HeaderExchangeHandlerhandleResponse()方法进行响应的返回,会唤醒阻塞线程并将Response中的结果通知调用方

上面的流程是我的一个个人理解,若有不对,还望指正。