RPC(Remote Procedure Call):远程过程调用。客户端能够像调用本地方法一样去调用服务器的服务。

常见的RPC框架有阿里的Dubbo、Google的gRPC、Twitter 的Finagle,Facebook 的 Thrift,等等。

那么RPC中有一些什么技术呢?

框架:Netty

通信协议:Socket、RMI。

服务发布与订阅:Zookeeper

Spring:使用Spring配置服务,加载Bean,扫描注解

消息编码与解码:使用Protostuff序列化与反序列化消息。

实现的流程图:

RPC的本质 rpc技术有哪些_客户端

 核心流程:

1、服务消费方(client)调用以本地调用方式调用服务;

2、client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;

3、client stub 找到服务地址,并将消息发送给服务端;

4、server stub收到消息后进行解码;

5、server stub 根据解码结果调用本地服务;

6、本地服务执行并将结果返回给serverstub;

7、server stub 将返回的结果打包成消息发送至消费方;

8、client stub 接收到消息,并进行解码;

9、服务消费方得到最终结果

 

RPC的目标就是将2~8这些步骤封装起来,让用户不需要了解这些细节。Java一般使用动态代理的方式实现远程调用。

 

RMI(Java Remote Method Invocation)实现方式

是Java编程语言里面,一种用于实现远程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

 

实现步骤:

1、编写远程服务接口,该接口必须继承java.rmi.Remote接口,方法必须抛出java.rmi.RemoteException异常;

2、编写远程接口实现类,该类必须继承java.rmi.server.UnicastRemoteObject类。

3、运行RMI编译器,创建客户端stub类和服务端skeleton类;

4、启动一个RMI注册表,以便驻留这些服务;

5、在RMI注册表中注册服务;

6、客服端查找远程对象,并调用远程方法;

1:创建远程接口,继承java.rmi.Remote接口
public interface GreetServer extends java.rmi.Remote{
     String sayHello(String name) throws RemoteException;
}
2:实现远程接口,继承java.rmi.server.UnicastRemoteObject类
public class GreeServerImpl extends java.rmi.UnicastRemoteObject
implements GreetServer{
    private static final long serilVersionUID = 343406015238720004l;
    public GreetServer() throws RemoteException{
      super();
   }
@Override
public String sayHello(String name) throws RemoteException{
      return "Hello"+name;
    }
}
3、生成Stub和Skeleton;
4、执行rmiregistry命令注册服务
5、启动服务
LocateRegistry.createRegistry(1098);
Naming.bind("rmi://10.108.1.1.138:1098/GreetServer",New GreetServerImpl());
6、客服端调用
GreetServer greetServer = (GreetServer)Naming.out.println(greetServer.sayHello("Jobs"));

Zookeeper

Zookeeper是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。Zookeeper提供了类似于Linux文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。

Zookeeper角色

Zookeeper集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色的一种

Leader:

1、一个Zookeeper集群同一时间只会只有一个实际工作的leader,它会发起并维护与各Follwer及Observer间的心跳。

2、所有的写操作必须通过Leader完成再由Leader将写操作广播给其他服务器。只要有超过半数节点(不包括observeer节点)写入成功。该请求就会被提交。

Follower:

1、一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳

2、Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理

3、并且负责在Leader处理写请求时对请求进行投票。

Netty架构:

服务端通讯流程

1、ServerSocketChannel

2、绑定InetSocketAdress

3、创建Selector,启动线程

4、将ServerSocketChannel注册到Selector监听

5、Selector轮询就绪的Key

6、handleAccept()处理新的客户端接入

7、设置新建客户连接的Socket

8、向selector注册监听读操作SelectionKey.OP_READ

9、handleRead()异步读请求消息到ByteBuffer

10、decode请求

11、异步写ByteBuffer到SocketChannel

package NettyTest;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
//服务器端(端口号为63932,192.168.1.4)
public class NIOServer {
    private Selector selector;
    /*
     * 获得一个ServerSocket通道,并对通道进行一些初始化的工作
     * @param port 绑定端口号
     * @Throws IOException
     */
    public void initServer(int port) throws IOException{
        //获得一个ServerSocket的通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        //设置通道为非阻塞
        serverChannel.configureBlocking(false);
        //将对应的ServerSocket绑定到port端口
        serverChannel.socket().bind(new InetSocketAddress(port));
        //获得一个管道处理器
        this.selector = Selector.open();
        //将通道管理器与该通道绑定,并为该通道注册SelectorKey.OP_ACCEPT事件,注册该事件后
        //当该事件到达时,selector.select()会返回,如果事件没有到达,则selector.select()会一直阻塞
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        /*
         * 采用轮询的方式监听selector上是否有需要处理的事件,如果有则进行处理
         * @throws IOException
         */
    }
    public void listen() throws IOException{
        System.out.println("服务端启动成功!");
        //轮询selector
        while(true){
            //当注册事件到来时返回方法,否则一直阻塞
            selector.select();
            //获得selector中的迭代器,选中的项为注册事件
            Iterator ite = this.selector.selectedKeys().iterator();
            while(ite.hasNext()){
                SelectionKey key = (SelectionKey) ite.next();
                //删除已选用的Key,防止重复处理
                ite.remove();
                //客户端连接请求事件
                if(key.isAcceptable()){
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    //获得和客户端连接通道
                    SocketChannel channel = server.accept();
                    //设置成非阻塞
                    channel.configureBlocking(false);
                    //在这可以给客户端发送消息
                    channel.write(ByteBuffer.wrap(new String("向客户端发送的一条消息").getBytes()));
                    //在和客户端连接成功后,为了可以接收客户端的信息,需要给通道设置成读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);
                    //获得可读事件
                    
                    }else if(key.isReadable()){
                        read(key);
                    }
            }
            
        }
    }
    private void read(SelectionKey key)throws IOException {
        //服务器可读取消息:得到的消息发生的socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        //创建读取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        channel.read(buffer);
        byte[] data = buffer.array();
        String msg = new String(data).trim();
        System.out.println("服务器端收到的消息"+msg);
        ByteBuffer outbuffer = ByteBuffer.wrap(msg.getBytes());
        channel.write(outbuffer);//将消息送回给客户端
        
    }
    //启动服务端测试
    public static void main(String[] args) throws IOException{
        NIOServer server = new NIOServer();
        server.initServer(63932);
        server.listen();
    }
    
}

代码中缺少handle的处理

 

客户端通信

1、打开SocketChannel

2、设置SocketChannel为非阻塞模式,同时设置TCP参数

3、异步连接服务端

4、判断连接结果,如果连接成功,则调到步骤10,否则执行步骤5

5、向Reactor线程多路复用器注册OP CONNECT事件

6、创建Selector启动线程

7、Selector轮询就绪的Key

8、handerConnect()

9、判断连接完成,则执行步骤10

10、向多路复用器注册读事件OP_READ

11、handerConnect()

12、decode请求

13、异步写ByteBuffer到SocketChannel

package NettyTest;

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.SocketChannel;
import java.util.Iterator;

public class NIOClient {
    //通道管理器
    private Selector selector;
    /*
     * 获得一个Socket通道,并对通道进行一些初始化工作
     * @param ip 连接客户端IP
     * @param port 连接服务器的端口
     * @throws IOException
     */
    public void initClient(String ip,int port) throws IOException{
        //获取一个socket通道
        SocketChannel channel = SocketChannel.open();
        //设置通道位非阻塞
        channel.configureBlocking(false);
        //获取一个通道管理器
        this.selector=Selector.open();
        
        //客户端连接服务器,其实方法执行并没有连接服务器,需要在listen()方法中调用
        //用channel.finishConnet();才能完成连接
        channel.connect(new InetSocketAddress(port));
        //将通道管理器与通道绑定,并为该通道注册SelectionKey.OP_CONNET事件。
        channel.register(selector, SelectionKey.OP_CONNECT);
    }
    /*
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有则进行处理
     * @throws IOException
     */
    public void listen() throws IOException{
        //轮询访问selector
        selector.select();
        Iterator ite = this.selector.selectedKeys().iterator();
        while(ite.hasNext()){
            SelectionKey key = (SelectionKey) ite.next();
            
            ite.remove();
            //连接该事件发生
            if(key.isConnectable()){
                SocketChannel channel = (SocketChannel) key.channel();
                if(channel.isConnectionPending()){
                    channel.finishConnect();
                }
                //设置成非阻塞
                channel.configureBlocking(false);
                channel.write(ByteBuffer.wrap(new String("向服务器发送了一条消息").getBytes()));
                //在服务器连接成功后,为了可以接收到服务端消息,需要设置管道的读取权限
                channel.register(this.selector, SelectionKey.OP_READ);
                
                //获取可读事件
                
            }else if(key.isReadable()){
                read(key);
            }
        }
    }
    private void read(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        //创建读取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer);
        byte[] data = buffer.array();
        String msg = new String(data).trim();
        System.out.println("服务器端收到的消息"+msg);
        ByteBuffer outbuffer = ByteBuffer.wrap(msg.getBytes());
        channel.write(outbuffer);//将消息送回给客户端
        
    }
    //启动客户端测试
    public static void main(String[] args) throws IOException{
        NIOClient client = new NIOClient();
        client.initClient("localhost", 63932);
        client.listen();
    }

}

Netty的IO线程由于聚合了 多路复用器Selector,可以同时并发处理成百上千个客户端Channel,由于读写操作都是非阻塞的,这就可以充分提升IO的运行效率,避免频繁的IO阻塞导致线程的挂起。

 

零拷贝:

1、Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFER)进行Socket的读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

2、Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便对组合buffer进行操作,避免了传统通过内存拷贝的方式将几个小的Buffer合并成一个大的Buffer。

3、Netty的文件传输采用了transferTo的方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过write方式导致内存拷贝问题。