Dubbo底层原理

一、RPC原理

在上述图中,通过1-10的步骤图解的形式,说明了RPC每一步的调用过程。具体描述为:

  • 1、客户端想要发起一个远程过程调用

  • 2、客户端Stub程序【客户端代理对象】接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。

  • 3、调用Socket通信协议,通过网络将数据包信息发送给服务端。

  • 4、服务端Stub程序【服务端代理对象】接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。

  • 5、服务端Stub程序【服务端代理对象】准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。

  • 6、服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序【服务端代理对象】

  • 7、服务端Stub程序【服务端代理对象】将程序调用结果按照约定的协议进行序列化,并通过网络发送回客户端Stub程序【客户端代理对象】

  • 8、客户端Stub程序【客户端代理对象】接收到服务端Stub【服务端代理对象】发送的返回数据,对数据进行反序列化操作,并将调用返回的数据传递给客户端请求发起者。

  • 9、客户端请求发起者得到调用结果,整个RPC调用过程结束。


1、RPC涉及到的相关技术

通过上文一系列的文字描述和讲解,我们已经了解了RPC的由来和RPC整个调用过程。我们可以看到RPC是一系列操作的集合,其中涉及到很多对数据的操作,以及网络通信。因此,对RPC中涉及到的技术做一个总结和分析:

  • a、动态代理技术: 上文中我们提到的Client Stub和Sever Stub程序,在具体的编码和开发实践过程中,都是使用动态代理技术自动生成的一段程序。

  • b、序列化和反序列化: 在RPC调用的过程中,我们可以看到数据需要在一台机器上传输到另外一台机器上。在互联网上,所有的数据都是以字节的形式进行传输的。而我们在编程的过程中,往往都是使用数据对象,因此想要在网络上将数据对象和相关变量进行传输,就需要对数据对象做序列化和反序列化的操作


序列化: 把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。

反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。


二、网络编程-NIO编程


1、I/O模型说明

I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。 Java 共支持 3 种网络编程模型/IO 模式:BIO(同步并阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)。其中阻塞与非阻塞主要指的是访问IO资源的线程是否会阻塞(或处于等待),非阻塞不会等待资源是否准备就绪,阻塞会一直阻塞等待;同步和异步主要是指的数据的请求方式,同步和异步是指访问数据的一种机制。


2、BIO(Blocking I/O,同步并阻塞)

BIO属于同步阻塞型IO,在服务器端的实现模式为,一个连接对应一个线程。当客户端有连接请求的时候服务端需要启动一个新的线程与之进行对应处理。

这个模式的缺点很明显,当我们的连接请求发送到服务器端的时候,服务器就会新启动一个线程来进行处理,当新建的线程不做任何处理只是挂着(阻塞)的时候,就会给服务器造成不必要的线程开销。

举个例子:我们用BIO开发了一个即时聊天系统,每一个客户端给我们的服务器端发送消息之前都要和我们的服务器端进行连接。但是发送完消息之后我们的客户端却不下线(不主动关闭),服务器端的线程就要一直阻塞的等待客户端给他发消息,(将线程挂起来)。这就会给服务器造成不必要的线程开销。这还不是最可怕的,当我们的客户端越来越多的情况下,每一个客户端的接入服务器都会建立一个新的线程与之对应。当客户端的连接数增多,产生高并发的时候,整个BIO网络就会占用大量的jvm线程,造成服务器性能降低,最后可能导致服务器宕机。


a、 BIO编程简单流程

① 服务器端启动一个ServerSocket

② 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户 建立一个线程与之通讯

③ 客户端发出请求后, 先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝

④ 如果有响应,客户端线程会等待请求结束后,在继续执行


b、 BIO是阻塞IO,那么阻塞到底发生在哪里?

  • ServserSocket.accept()是阻塞的

  • 所有输入流和输出流都是阻塞的,接收方等待发送方发送消息的时候是阻塞等待的,如果发送方一直不发送消息,那么接收方就要一直阻塞等待干不了其他事


c、 缺点

  • 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write

  • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大

  • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费


案例

    public class BIOServer {
        public static void main(String[] args) throws Exception {
            //线程池机制
            //思路
            //1. 创建一个线程池
            //2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)

            ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

            //创建ServerSocket
            ServerSocket serverSocket = new ServerSocket(6666);

            System.out.println("服务器启动了");

            while (true) {

                System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
                //监听,等待客户端连接
                System.out.println("等待连接....");
                final Socket socket = serverSocket.accept();
                System.out.println("连接到一个客户端");

                //就创建一个线程,与之通讯(单独写一个方法)
                newCachedThreadPool.execute(new Runnable() {
                    public void run() { //我们重写
                        //可以和客户端通讯
                        handler(socket);
                    }
                });

            }


        }

        //编写一个handler方法,和客户端通讯
        public static void handler(Socket socket) {

            try {
                System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
                byte[] bytes = new byte[1024];
                //通过socket 获取输入流
                InputStream inputStream = socket.getInputStream();

                //循环的读取客户端发送的数据
                while (true) {

                    System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());

                    System.out.println("read....");
                   int read =  inputStream.read(bytes);
                   if(read != -1) {
                       System.out.println(new String(bytes, 0, read
                       )); //输出客户端发送的数据
                   } else {
                       break;
                   }
                }


            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                System.out.println("关闭和client的连接");
                try {
                    socket.close();
                }catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }
    }


3、NIO详细介绍


NIO 全称non-blocking IO ,非阻塞是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的。它的主要特点

  • NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

  • NIO是 面向缓冲区编程的。数据读取到一个缓冲区中,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络

  • Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入, 这个线程同时可以去做别的事情。通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。


NIO 三大核心原理示意图


上面这一张图描述了 NIO 的 Selector 、 Channel 和 Buffer 的关系:

  • 每个 channel 都会对应一个 Buffer

  • Selector 对应一个线程, 一个线程对应多个 channel(连接)

  • 每个 channel 都注册到 Selector选择器上

  • Selector不断轮询查看Channel上的事件, 事件是通道Channel非常重要的概念;事件状态:Connect(连接就绪)、Accept(接受就绪)、Read(读就绪)、Write(写就绪)

  • Selector 会根据不同的事件,完成不同的处理操作

  • Buffer 就是一个内存块 , 底层是有一个数组

  • 数据的读取写入是通过 Buffer, 这个和 BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO 的 Buffer 是可以读也可以写 , channel 是双向的


将Channel1、Channel2、Channel3、ChannelN等连接注册到selector多路复用器中,从而实现单个线程去监视多个连接;一旦某个连接处于就绪状态,那么也就是触发了读/写事件的时候,selectot监听到该连接的状态,就会开启一个线程去处理对应的应用程序


4、缓冲区(Buffer)


4.1 基本介绍

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个数组,该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。

在 NIO 中,Buffer是一个顶层父类,它是一个抽象类, 类的层级关系图,常用的缓冲区分别对应: byte、short、 int、 long、float、double、char 7种。


4.2、Buffer常用API介绍

示例代码:

  import java.nio.ByteBuffer;
  /**
  * 创建缓冲区
  */
  public class CreateBufferDemo {
    public static void main(String[] args) {
      //1.创建一个指定长度的缓冲区, 以ByteBuffer为例
      ByteBuffer byteBuffer = ByteBuffer.allocate(5);
      for (int i = 0; i < 5; i++) {
        System.out.println(byteBuffer.get());
      }
      //由于上面代码已经取了5个数据,下标会移动到5,再在此调用会报错
      //System.out.println(byteBuffer.get());
      //2.创建一个有内容的缓冲区
      ByteBuffer wrap = ByteBuffer.wrap("ddd".getBytes());
      for (int i = 0; i < 5; i++) {
        System.out.println(wrap.get());
     }
   }
  }


4.2.2、缓冲区对象添加数据

图解:

示例代码:

  import java.nio.ByteBuffer;
  /**
  * 添加缓冲区
  */
  public class PutBufferDemo {
    public static void main(String[] args) {

      //1.创建一个指定长度的缓冲区, 以ByteBuffer为例
      ByteBuffer byteBuffer = ByteBuffer.allocate(10);
      System.out.println(byteBuffer.position());//0 获取当前索引所在位置
      System.out.println(byteBuffer.limit());//10 最多能操作到哪个索引
      System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
      System.out.println(byteBuffer.remaining());//10 还有多少个能操作

      //修改当前索引位置
      //byteBuffer.position(1);
      //修改最多能操作到哪个索引位置
      //byteBuffer.limit(9);
      //System.out.println(byteBuffer.position());//1 获取当前索引所在位置
      //System.out.println(byteBuffer.limit());//9 最多能操作到哪个索引
      //System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
      //System.out.println(byteBuffer.remaining());//8 还有多少个能操作
      //添加一个字节
      byteBuffer.put((byte) 97);
      System.out.println(byteBuffer.position());//1 获取当前索引所在位置
      System.out.println(byteBuffer.limit());//10 最多能操作到哪个索引
      System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
      System.out.println(byteBuffer.remaining());//9 还有多少个能操作

      //添加一个字节数组
      byteBuffer.put("abc".getBytes());
      System.out.println(byteBuffer.position());//4 获取当前索引所在位置
      System.out.println(byteBuffer.limit());//10 最多能操作到哪个索引
      System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
      System.out.println(byteBuffer.remaining());//6 还有多少个能操作

      //当添加超过缓冲区的长度时会报错
      byteBuffer.put("012345".getBytes());
      System.out.println(byteBuffer.position());//10 获取当前索引所在位置
      System.out.println(byteBuffer.limit());//10 最多能操作到哪个索引
      System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
      System.out.println(byteBuffer.remaining());//0 还有多少个能操作
      System.out.println(byteBuffer.hasRemaining());// false 是否还能有操作的数组

      // 如果缓存区存满后, 可以调整position位置可以重复写,这样会覆盖之前存入索引的对应的值
      byteBuffer.position(0);
      byteBuffer.put("012345".getBytes());
   }
  }


4.2.3、缓冲区对象读取数据

图解:flip()方法

图解:clear()方法

import java.nio.ByteBuffer;
/**
* 从缓冲区中读取数据
*/
public class GetBufferDemo {
  public static void main(String[] args) {

    //1.创建一个指定长度的缓冲区
    ByteBuffer allocate = ByteBuffer.allocate(10);
    allocate.put("0123".getBytes());
    System.out.println("position:" + allocate.position());//4
    System.out.println("limit:" + allocate.limit());//10
    System.out.println("capacity:" + allocate.capacity());//10
    System.out.println("remaining:" + allocate.remaining());//6

    //切换读模式
     System.out.println("读取数据--------------");
     allocate.flip();
     System.out.println("position:" + allocate.position());//4
     System.out.println("limit:" + allocate.limit());//10
     System.out.println("capacity:" + allocate.capacity());//10
     System.out.println("remaining:" + allocate.remaining());//6
     for (int i = 0; i < allocate.limit(); i++) {
        System.out.println(allocate.get());
     }

    //读取完毕后.继续读取会报错,超过limit值
    //System.out.println(allocate.get());
    //读取指定索引字节
    System.out.println("读取指定索引字节--------------");
    System.out.println(allocate.get(1));
    System.out.println("读取多个字节--------------");

    // 重复读取
    allocate.rewind();
    byte[] bytes = new byte[4];
    allocate.get(bytes);
    System.out.println(new String(bytes));

    // 将缓冲区转化字节数组返回
    System.out.println("将缓冲区转化字节数组返回--------------");
    byte[] array = allocate.array();
    System.out.println(new String(array));

    // 切换写模式,覆盖之前索引所在位置的值
    System.out.println("写模式--------------");
    allocate.clear();
    allocate.put("abc".getBytes());
    System.out.println(new String(allocate.array()));
 }
}

注意事项:

  • capacity:容量(长度)limit: 界限(最多能读/写到哪里)posotion:位置(读/写哪个索引)

  • 获取缓冲区里面数据之前,需要调用flip方法

  • 再次写数据之前,需要调用clear方法,但是数据还未消失,等再次写入数据,被覆盖了才会消失


5、通道(Channel)


5.1、基本介绍

通常来说NIO中的所有IO都是从 Channel(通道) 开始的。NIO 的通道类似于流,但有些区别如下:

  • 通道可以读也可以写,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)

  • 通道可以异步读写

  • 通道总是基于缓冲区Buffer来读写


5.2、Channel常用类以及API介绍

常用 的Channel实现类类 有 :FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel、FileChannel 用于文件的数据读写, DatagramChannel 用于 UDP 的数据读写,

ServerSocketChannel 和SocketChannel 用于 TCP 的数据读写。

SocketChannel 与ServerSocketChannel类似 Socke和ServerSocket,可以完成客户端与服务端数据的通信工作。


5.2.2、ServerSocketChannel实现服务端

服务端实现步骤:

1、打开一个服务端通道

2、绑定对应的端口号

3、通道默认是阻塞的,需要设置为非阻塞

4、检查是否有客户端连接 有客户端连接会返回对应的通道

5、获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中

6、给客户端回写数据

7、释放资源


示例代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
/**
* 服务端
*/
public class NIOServer {
  public static void main(String[] args) throws IOException,
  InterruptedException {

    //1. 打开一个服务端通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

    //2. 绑定对应的端口号
    serverSocketChannel.bind(new InetSocketAddress(9999));

    //3. 通道默认是阻塞的,需要设置为非阻塞
    // true 为通道阻塞 false 为非阻塞
    serverSocketChannel.configureBlocking(false);
    System.out.println("服务端启动成功..........");

    while (true) {
      //4. 检查是否有客户端连接 有客户端连接会返回对应的通道 , 否则返回null
      SocketChannel socketChannel = serverSocketChannel.accept();
      if (socketChannel == null) {
        System.out.println("没有客户端连接...我去做别的事情");
        Thread.sleep(2000);
        continue;
     }

      //5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
      ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

      //返回值:
      //正数: 表示本次读到的有效字节个数.
      //0  : 表示本次没有读到有效字节.
      //-1 : 表示读到了末尾
      int read = socketChannel.read(byteBuffer);
      System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read,StandardCharsets.UTF_8));

      //6. 给客户端回写数据
      socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));

      //7. 释放资源
      socketChannel.close();
   }
 }
}


5.2.3、SocketChannel

客户端实现步骤:

1、打开通道

2、设置连接IP和端口号

3、写出数据

4、读取服务器写回的数据

5、释放资源


示例代码:

  import java.io.IOException;
  import java.net.InetSocketAddress;
  import java.nio.ByteBuffer;
  import java.nio.channels.SocketChannel;
  import java.nio.charset.StandardCharsets;
  /**
  * 客户端
  */
  public class NIOClient {
    public static void main(String[] args) throws IOException {
      
      //1.打开通道
      SocketChannel socketChannel = SocketChannel.open();
      
      //2.设置连接IP和端口号
      socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
      
      //3.写出数据
      socketChannel.write(ByteBuffer.wrap("老板, 该还钱拉!".getBytes(StandardCharsets.UTF_8)));
      
      //4.读取服务器写回的数据
      ByteBuffer readBuffer = ByteBuffer.allocate(1024);
      int read=socketChannel.read(readBuffer);
      System.out.println("服务端消息:" + new String(readBuffer.array(), 0, read,StandardCharsets.UTF_8));
      
      //5.释放资源
      socketChannel.close();
   }
  }


6、Selector (选择器/多路复用器)


6.1、基本介绍

可以用一个线程,处理多个的客户端连接,就会使用到NIO的Selector(选择器). Selector 能够检测多个注册的服务端通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。


6.2、常用API介绍


6.2.1、Selector和SelectionKey 类

Selector 类是一个抽象类。常用方法:

  • Selector.open() : //得到一个选择器对象

  • selector.select() : //阻塞 监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放入集合内部并返回事件数量

  • selector.select(1000): //阻塞 1000 毫秒,监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放入集合内部并返回

  • selector.selectedKeys() : // 返回存有SelectionKey的集合


Selector是一个选择器,进行监控,而API中经常显示的SelectionKey 代表了监控到的具体事件,其核心API包括:

  • SelectionKey.isAcceptable(): 是否是连接继续事件

  • SelectionKey.isConnectable(): 是否是连接就绪事件

  • SelectionKey.isReadable(): 是否是读就绪事件

  • SelectionKey.isWritable(): 是否是写就绪事件


与之对应SelectionKey中定义的4种事件:

  • SelectionKey.OP_ACCEPT —— 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了

  • SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户端与服务器的连接已经建立成功

  • SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)

  • SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)


6.3、加入Selector 服务端实现步骤(客户端不变)

1、打开一个服务端通道

2、绑定对应的端口号

3、通道默认是阻塞的,需要设置为非阻塞

4、创建选择器

5、将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT

6、检查选择器是否有事件

7、获取事件集合

8、判断事件是否是客户端连接事件SelectionKey.isAcceptable()

9、得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ

10、判断是否是客户端读就绪事件SelectionKey.isReadable()

11、得到客户端通道,读取数据到缓冲区

12、给客户端回写数据

13、从集合中删除对应的事件, 因为防止二次处理.


示例代码 :

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* 服务端-选择器
*/
public class NIOSelectorServer {
  public static void main(String[] args) throws IOException,InterruptedException {

    //1. 打开一个服务端通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

    //2. 绑定对应的端口号
    serverSocketChannel.bind(new InetSocketAddress(9999));

    //3. 通道默认是阻塞的,需要设置为非阻塞
    serverSocketChannel.configureBlocking(false);

    //4. 创建选择器
    Selector selector = Selector.open();

    //5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    System.out.println("服务端启动成功...");

    while (true) {
      //6. 检查选择器是否有事件
      int select = selector.select(2000);
      if (select == 0) {
        continue;
     }

      //7. 获取事件集合
      Set<SelectionKey> selectionKeys = selector.selectedKeys();
      Iterator<SelectionKey> iterator = selectionKeys.iterator();

      while (iterator.hasNext()) {
           //8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
           SelectionKey key = iterator.next();

          //9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
          if (key.isAcceptable()) {
              SocketChannel socketChannel = serverSocketChannel.accept();
              System.out.println("客户端已连接......" + socketChannel);

              //必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
              socketChannel.configureBlocking(false);

              //并指定监听事件为OP_READ
              socketChannel.register(selector, SelectionKey.OP_READ);
          }

          //10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
          if (key.isReadable()) {

               //11.得到客户端通道,读取数据到缓冲区
               SocketChannel socketChannel = (SocketChannel) key.channel();
               ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
               int read = socketChannel.read(byteBuffer);
               if (read > 0) {
                  System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read,StandardCharsets.UTF_8));
            
                  //12.给客户端回写数据
                  socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
                  socketChannel.close();
               }
          }

          //13.从集合中删除对应的事件, 因为防止二次处理.
          iterator.remove();
     }
   }
 }
}


三、IO多路复用

IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出CPU。IO是指网络 IO,多路指多个TCP连接(即 socket),复用指复用一个或几个线程。


意思说一个或一组线程处理多个 TCP 连接。最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程。


IO 多路复用的三种实现方式:select、poll、epoll


举例、假设你是一名老师,让30个学生完成一道题目,并检查他们的结果是否正确,现在有以下几种情况:

场景1、按顺序逐个检查,先检查A,然后B,之后C、D...这中间如果有学生卡住,后面的学生都会被耽搁

场景2、找来30个助教,每个助教负责检查一个学生

场景3、站在讲台上,那个学生写完了举手示意,谁举手就去检查谁


上述场景1 对应传统单线程socket模型,缺点非常明显:同时只能处理一个客户端的请求,多余的请求可以通过队列的方式保存起来,后续一次遍历处理。但如果处理某个请求时阻塞了,那么后续所有请求的处理都会阻塞。


上述场景2 对应传统多线程socket模型,一般都是通过主线程阻塞等待客户端连接,每个客户端连接创建新的工作线程来处理请求的方式实现。当并发量不是很大时,这种处理方式还可以使用。一旦并发量很大,频繁创建的线程会带来巨大的资源消耗以及上下文切换消耗。


上述场景3 就可以理解为I/O多路复用技术︰将客户端对应socket的fd(文件描述符)注册到select或poll或epoll上,当socket流就绪时,select线程就会执行:轮询找到就绪的socket,将它返回给应用,执行相应流处理


四、Dubbo的底层原理


Dubbo 大致上分为三层,分别是:业务层、RPC 层、Remoting 层

业务层

Service,业务层,就是咱们开发的业务逻辑层


RPC 层

Config,配置层,主要围绕 ServiceConfig 和 ReferenceConfig,初始化配置信息

Proxy,代理层,服务提供者还是消费者都会生成一个代理类,使得服务接口透明化,代理层做远程调用和返回结果。

Register,注册层,封装了服务注册和发现。

Cluster,路由和集群容错层,负责选取具体调用的节点,处理特殊的调用要求和负责远程调用失败的容错措施。

Monitor,监控层,负责监控统计调用时间和次数。

Portocol,远程调用层,主要是封装 RPC 调用,主要负责管理 Invoker,Invoker代表一个抽象封装了的执行体,之后再做详解。


Remoting 层

Exchange,信息交换层,用来封装请求响应模型,同步转异步。

Transport,网络传输层,抽象了网络传输的统一接口,这样用户想用 Netty 就用 Netty,想用 Mina 就用 Mina。

Serialize,序列化层,将数据序列化成二进制流,当然也做反序列化。


五、dubbo原理-服务暴露


1、服务暴露起点

  • 自定义XML文件解析

  • 解析dubbo-service标签

由此可以看出,再解析完dubbo-service标签后,会创建ServiceBean对象。

  • ServiceBean

从类图可以看出,ServiceBean和Spring的关系很大,它继承了InitializingBean和ApplicationEvent。在bean初始化完成后,会调用InitializingBean.afterPropertiesSet方法来执行服务暴露的准备工作。

在spring的context完成初始化之后,会触发ApplicationEventListener事件,从而进行服务暴露。


2、ServiceBean.afterPropertiesSet

读取服务提供配置文件,初始化服务配置参数:protocol(dubbo 、hessian、http...) 、registry(注册中心类型: zk,地址,端口号)、monitor、module...


3、ServiceBean.onApplicationEvent

在Spring的容器初始完成之后,会触发ApplicationEventListener事件,开始export服务暴露。


4、export 暴露服务

  • doExport: check 参数

  • doExportUrls 执行暴露的URL

比如:registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=dubboProvider&dubbo=2.0.2&pid=22016&registry=zookeeper&timestamp=1561879716285

  • doExportUrlsFor1Protocol

  • 生成Invoker

默认使用JavassistProxyFactory代理工厂 生成Invoker 代理对象


5、暴露服务给Nett,同时注册服务到注册中心

先调用RegistryProtocol协议的export方法

    @Override
    public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);

        URL registryUrl = getRegistryUrl(originInvoker);

        //registry provider
        final Registry registry = getRegistry(originInvoker);
        final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);

        boolean register = registedProviderUrl.getParameter("register", true);

        // 注册服务到注册中心
        ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registedProviderUrl);

        if (register) {
            register(registryUrl, registedProviderUrl);
            ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
        }

      
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        //Ensure that a new exporter instance is returned every time export
        return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registedProviderUrl);
    }


5.1 执行 final ExporterChangeableWrapper exporter = doLocalExport(originInvoker)

将Invoker代理对象 转换成 Exporter暴露对象(使用的是DubboProtocol协议对象的 export() 进行转换)

 public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        URL url = invoker.getUrl();


        String key = serviceKey(url);

        //创建一个Exporter
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);

        exporterMap.put(key, exporter);

        Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
        Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
        if (isStubSupportEvent && !isCallbackservice) {
            String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
            if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
                if (logger.isWarnEnabled()) {
                    logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
                            "], has set stubproxy support event ,but no stub methods founded."));
                }
            } else {
                stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
            }
        }
        // 开启Netty 服务端
        openServer(url);

        optimizeSerialization(url);
        return exporter;
  }


总结

在读取配置文件生成服务实体以后,会通过 ProxyFactory 将 Proxy 转换成 Invoker。此时,Invoker 会被定义 Protocol,之后会被包装成 Exporter。

最后,Exporter 会发送到注册中心,作为服务的注册信息。上述流程主要通过 ServiceConfig中的 doExport 完成。


上面截取了服务提供者暴露服务的代码片段,整个暴露过程分为七个步骤:

  • 服务提供者在启动的时候,会通过读取一些配置将服务实例化。

  • Proxy 封装服务调用接口,方便调用者调用。客户端获取 Proxy 时,可以像调用本地服务一样,调用远程服务。

  • Proxy 在封装时,需要调用 Protocol 定义协议格式,例如:Dubbo Protocol。

  • 将 Proxy 封装成 Invoker,它是真实服务调用的实例。

  • 将 Invoker 转化成 Exporter,Exporter 只是把 Invoker 包装了一层,为了在注册中心中暴露,方便消费者使用。

  • 将包装好的 Exporter 注册到注册中心。

  • 一旦服务注册到注册中心以后,注册中心会通过RegistryProtocol 中的 Export 方法将服务暴露出去,并依次做以下操作:

    1、委托具体协议进行服务暴露,创建 NettyServer 监听端口,并保持服务实例。

    2、创建注册中心对象,创建对应的 TCP 连接。

    3、注册元数据到注册中心。

    4、订阅 Configurators 节点。

    5、如果需要销毁服务,需要关闭端口,注销服务信息。


六、dubbo原理-服务引用


七、dubbo原理-服务调用


八、SPI机制


1、什么是SPI

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

SPI全称为Service Provider Interface,是一种服务发现机制,其本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件。这样可以在运行时,动态为该接口替换实现类。


2、JAVA-SPI案例

首先,我们需要定义一个接口,SPIService

package com.viewscenes.netsupervisor.spi;
public interface SPIService {
    void execute();
}

然后,定义两个实现类,只输入一句话。

package com.viewscenes.netsupervisor.spi;
public class SpiImpl1 implements SPIService{
    public void execute() {
        System.out.println("SpiImpl1.execute()");
    }
}
----------------------我是乖巧的分割线----------------------
package com.viewscenes.netsupervisor.spi;
public class SpiImpl2 implements SPIService{
    public void execute() {
        System.out.println("SpiImpl2.execute()");
    }
}

然后我们就可以通过ServiceLoader.load或者Service.providers方法拿到实现类的实例。其中,Service.providers包位于sun.misc.Service,而ServiceLoader.load包位于java.util.ServiceLoader。

public class Test {
    public static void main(String[] args) {    
        Iterator<SPIService> providers = Service.providers(SPIService.class);
        ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);
 
        while(providers.hasNext()) {
            SPIService ser = providers.next();
            ser.execute();
        }
        System.out.println("--------------------------------");
        Iterator<SPIService> iterator = load.iterator();
        while(iterator.hasNext()) {
            SPIService ser = iterator.next();
            ser.execute();
        }
    }
}


3、源码分析

我们看到一个位于sun.misc包,一个位于java.util包,sun包下的源码看不到。我们就以ServiceLoader.load为例,通过源码看看它里面到底怎么做的。


A、ServiceLoader

首先,我们先来了解下ServiceLoader,看看它的类结构。


B、Load

load方法创建了一些属性,重要的是实例化了内部类,LazyIterator。最后返回ServiceLoader的实例。

public final class ServiceLoader<S> implements Iterable<S>
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //要加载的接口
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //访问控制器
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        //先清空
        providers.clear();
        //实例化内部类 
        LazyIterator lookupIterator = new LazyIterator(service, loader);
    }
}


C、查找实现类

查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。

  public Iterator<S> iterator() {
      return new Iterator<S>() {
          public boolean hasNext() {
              return lookupIterator.hasNext();
          }
          public S next() {
              return lookupIterator.next();
          }
          .......
  };

所以,我们重点关注lookupIterator.hasNext()方法,它最终会调用到hasNextService。

  private class LazyIterator implements Iterator<S>{
      Class<S> service;
      ClassLoader loader;
      Enumeration<URL> configs = null;
      Iterator<String> pending = null;
      String nextName = null; 
      private boolean hasNextService() {
          //第二次调用的时候,已经解析完成了,直接返回
          if (nextName != null) {
              return true;
          }
          if (configs == null) {
              //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
              //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
              String fullName = PREFIX + service.getName();
              //将文件路径转成URL对象
              configs = loader.getResources(fullName);
          }
          while ((pending == null) || !pending.hasNext()) {
              //解析URL文件对象,读取内容,最后返回
              pending = parse(service, configs.nextElement());
          }
          //拿到第一个实现类的类名
          nextName = pending.next();
          return true;
      }
 }


D、创建实例

当然,调用next方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。

private class LazyIterator implements Iterator<S>{
    private S nextService() {
        //全限定类名
        String cn = nextName;
        nextName = null;
        //创建类的Class对象
        Class<?> c = Class.forName(cn, false, loader);
        //通过newInstance实例化
        S p = service.cast(c.newInstance());
        //放入集合,返回实例
        providers.put(cn, p);
        return p; 
    }
}


4、Dubbo--SPI 机制

ExtensionLoader 是dubbo的SPI机制的实现类。每一个接口都会有一个自己的ExtensionLoader实例对象,这点跟Java的SPI机制是一样的。


同样地,Dubbo的SPI机制也做了以下几点约定:

  • 接口必须要加@SPI注解

  • 配置文件可以放在META-INF/services/、META-INF/dubbo/internal/ 、META-INF/dubbo/ 、META-INF/dubbo/external/这四个目录底下,文件名也是接口的全限定名

  • 内容为键值对,键为Bean的名称(可以理解为spring中Bean的名称),值为实现类的全限定名


5、Dubbo--SPI 机制案例

首先在LoadBalance接口上@SPI注解

@SPI
public interface LoadBalance {

}


然后,修改一下Java的 SPI机制 测试时配置文件内容,改为键值对,因为 Dubbo 的 SPI机制 也可以从 META-INF/services/ 目录下读取文件,所以这里就没重写文件

 random=com.sanyou.spi.demo.RandomLoadBalance


测试类:

  public class ExtensionLoaderDemo {

      public static void main(String[] args) {
          ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
          LoadBalance loadBalance = extensionLoader.getExtension("random");
          System.out.println("获取到random键对应的实现类对象:" + loadBalance);
      }
  }

通过ExtensionLoader的getExtension方法,传入短名称,这样就可以精确地找到短名称对的实现类。


所以从这可以看出Dubbo的SPI机制解决了前面提到的无法获取指定实现类的问题。


6、Dubbo SPI普通的使用方式

posted @ 2024-10-09 17:33  jock_javaEE  阅读(113)  评论(0)    收藏  举报