第6章:Dubbo远程调用

第6章:Dubbo远程调用

1.Dubbo调用介绍

RPC调用:客户端会将服务调用接口、方法名、方法参数类型和方法参数值等序列化为二进制流,经过网络传输到服务端,服务端经过反序列化为请求对象Request,进行反射调用,最后将结果进行返回。

在这里插入图片描述

客户端请求时会从注册中心拉取和订阅对应的服务列表,Cluster会把拉取的服务列表聚合为一个Invoker,每个接口名都有一个Directory(RegistryDIrectory实例,一对一)

所有的路由和负载均衡都是在客户端实现的(这个也是之前李宗根讲的)。先路由,后负载均衡。

然后交给底层I/O线程池进行读写、序列化和反序列化,这里一定不能阻塞(NIO实现,Netty)

2.Dubbo协议详解

在这里插入图片描述

16字节长的报文头部,主要携带内容参数如下:
在这里插入图片描述

Dubbo字段解析2
在这里插入图片描述

2.1 Dubbo解决粘包拆包问题

在网络通信中(基于TCP)需要解决网络粘包/拆包的问题,常用解决方法有用回车、换行、固定长度、用特殊分隔符等。Dubbo采用的是使用特殊分隔符号0xdabb魔法数

Dubbo粘包拆包的解决主要是在解码decode过程中

ExchangeCodec.java

// 入口方法
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);
}

解析请求头

protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
    
    
  // check magic number.
  if (readable > 0 && header[0] != MAGIC_HIGH
      || readable > 1 && header[1] != MAGIC_LOW) {
    
    
    int length = header.length;
    if (header.length < readable) {
    
    
      header = Bytes.copyOf(header, readable);
      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) {
    
    
        buffer.readerIndex(buffer.readerIndex() - header.length + i);
        header = Bytes.copyOf(header, i);
        break;
      }
    }
    return super.decode(channel, buffer, readable, header);
  }
  // check length.
  if (readable < HEADER_LENGTH) {
    
    
    return DecodeResult.NEED_MORE_INPUT;
  }

  // get data length.
  int len = Bytes.bytes2int(header, 12);
  checkPayload(channel, len);

  int tt = len + HEADER_LENGTH;
  if (readable < tt) {
    
    
    return DecodeResult.NEED_MORE_INPUT;
  }

  // limit input stream.
  ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);

  try {
    
    
    return decodeBody(channel, is, header);
  } finally {
    
    
    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);
      }
    }
  }
}

这个方法主要是检查请求头的相关信息

1.检查魔法数,魔法数高位和低位各占1字节
2.检查当前请求头是否完整,如果不完整直接返回
3.获取此次请求体的长度。然后判断 请求头+消息体长度 是否 大于此次消息包的长度,如果大于的话,说明此次的消息不是完整的一个消息,也意味着进行拆包了,直接返回,等待其它信息
4.解析消息体

2.2 客户端并发调用Dubbo服务端正确响应对应线程

在实际使用场景中,客户端会使用多线程并发调用服务,Dubbo如何做到正确响应对应线程?

关键点在于请求头中的全局请求id标识,示意图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SMvevEnc-1638092471257)(/Dubbo请求响应.png)]

当客户端多个线程并发请求时,框架内部会调用DefaultFuture对象的get方法进行等待。请求发送时,会反序列化创建Request对象,这个时候被分配一个唯一id(requestId),将requestId-DefaultFuture的关联关系存储到HashMap中,就是上图中的Futures集合。当客户端收到响应时,会根据Response对象中的id,从Futures集合中查找对应的DefaultFuture对象,最终会唤醒对应的线程并通知结果。客户端也会启动一个定时扫描线程去探测超时没有返回的请求。

3.编解码器原理

3.1 Dubbo协议编码器

将Java对象编码成字节流返回成字节流返回给客户端,主要做的是:1⃣️构造16字节长的请求头header。2⃣️对消息体进行序列化处理

主要是通过魔法数0xdabb来判断一次请求的开始,同时Dubbo也会去检查一次流的长度是否超过8MB。(与Excel上传原理相同)

3.2 Dubbo协议解码器

解码工作与编码工作的流程相似,主要做的是:1⃣️解码报文头部(16字节)。2⃣️解码报文体,以及将报文体转换成RpcInvocation

解码报文体是强协议相关,与采用的序列化协议相关(通过读取header的序列化ID来感知采用的序列化协议)

4.DubboHandler

4.1 请求响应Handler

在Dubbo框架内部,所有方法调用会被抽象为Request/Response,每次调用(一次会话)都会创建一个Request对象,如果是方法调用会返回一个Response对象。HeaderExchangeHandler用来处理这种场景,主要负责以下4种事情

  • 更新发送和读取请求时间戳(主要用于Dubbo心跳处理判断)
  • 判断请求格式和编解码是否有错,并返回客户端失败的具体原因(verify)
  • 处理Request请求和Response正常响应
  • 支持Telnet调用

当发送请求时,会在DefaultFuture中保存请求对象并阻塞请求线程(await),完成请求后,设置响应Response并唤醒阻塞线程,将Response中的结果通知调用方。

参考Dubbo框架中的DefaultFuture.java

public static void received(Channel channel, Response response) {
    
    
  try {
    
    
    DefaultFuture future = FUTURES.remove(response.getId());
    if (future != null) {
    
    
      future.doReceived(response);
    } else {
    
    
      logger.warn("The timeout response finally returned at "
                  + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()))
                  + ", response " + response
                  + (channel == null ? "" : ", channel: " + channel.getLocalAddress()
                     + " -> " + channel.getRemoteAddress()));
    }
  } finally {
    
    
    CHANNELS.remove(response.getId());
  }
}

private void doReceived(Response res) {
    
    
  lock.lock();
  try {
    
    
    response = res;
    if (done != null) {
    
    
      done.signal();
    }
  } finally {
    
    
    lock.unlock();
  }
  if (callback != null) {
    
    
    invokeCallback(callback);
  }
}

4.2 心跳Handler

Dubbo默认客户端和服务端都会发送心跳报文,用来保持TCP长链接状态(避免新建TCP连接的开销)。在客户端和服务端,Dubbo内部开启一个线程循环扫描并检测连接是否超时,在服务端超时则会主动关闭客户端连接,在客户端超时则会主动重新创建连接。默认心跳检测时间时60秒。

超过超时时间没有发送报文,就会发送心跳报文检测TCP连接状态。

参考博文:

https://blog.csdn.net/u013076044/article/details/89279699

猜你喜欢

转载自blog.csdn.net/weixin_42478399/article/details/121594746