学习RPC

目录

一、RPC框架原理

二、RMI介绍

1.RMI特性:

2.原生RMI代码示例

三、CXF/Axis2

1.CXF

2.Axis2

四、Thrift

1.Thrift工作原理

2.Thrift IDL语法说明

五、gRPC

1.protobuf3语法

2.message消息数据结构

3.enum枚举类型

4.map数据类型

5.Service服务定义

六、HTTP Client

1.HttpClient的使用流程:

2.构建HttpClient对象

3.构建URI对象

七、实现RPC框架

1.实现原理

2.代码实现

tips 

一、RPC框架原理

RPC(Remote Procedure Call,远程过程调用)一般用来实现部署在不同机器上的系统之间的方法调用,使得程序能够像访问本地系统资源一样,通过网络传输去访问远端系统资源。

 RPC框架实现的架构原理:

Client Code:客户端调用代码实现,负责发起RPC调用,为调用方用户提供使用API。

Serialization/Deserialization:负责对RPC调用通过网络传输的内容进行序列化与反序列化,不同的RPC框架有不同的实现机制。主要分为文本与二进制两大类。文本类别的序列化机制主要有XML与JSON两种格式,二进制类别的序列化机制常见的有Java原生的序列化机制, 以及Hessian、Thrift、Avro、Kryo、MessagePack等,不同的序列化方式在可读性、码流大小、支持的数据类型及性能等方面都存在较大的差异。

Stub Proxy:代理对象,屏蔽RPC调用过程中复杂的网络处理逻辑,使得RPC调用透明化,能够保持与本地调用一样的代码风格。

Transport:作为RPC框架底层的通信传输模块,一般通过Socket在客户端与服务端之间传递请求与应答信息。

Server Code:服务端读物业务逻辑具体的实现。

二、RMI介绍

    Java RMI是一种基于Java的远程方法调用技术,是Java特有的一种RPC实现,它能够使部署在不同主机上的Java对象之间进行透明的通信与方法调用。

1.RMI特性:

(1)支持真正的面向对象的多态性。

  1. Java语言独有,不支持其他语言。
  2. 使用了Java原生的序列化机制,所有的序列化对象必须实现java.io.Serializable接口。
  3. 底层通信基于BIO(同步阻塞I/O)实现的Socket完成。

 因Java原生序列化机制与BIO通信机制本身存在的性能问题,导致RMI的性能较差。

2.原生RMI代码示例

import java.rmi.Remote;
import java.rmi.RemoteException;
/*
* 定义RMI对外服务接口
* */
public interface HelloService extends Remote {
    //RMI接口方法定义必须显式声明抛出RemoteException异常
    String sayHello(String someone)throws RemoteException;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

/*
* 服务端接口实现
* 服务端方法实现必须继承UnicastRemoteObject类,该类定义了服务调用方法与服务提供方对象实例,并建立一对一的连接
*/
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
    public HelloServiceImpl() throws RemoteException{
        super();
    }
    public String sayHello(String someone)throws RemoteException{
        return "Hello," + someone;
    }
}
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

/*
* 服务端RMI服务启动
* */
public class ServerMain {
    public static void main(String args[]) throws RemoteException, AlreadyBoundException, MalformedURLException {
        //创建服务
        HelloService helloService = new HelloServiceImpl();
        //注册服务
        LocateRegistry.createRegistry(8801);
        Naming.bind("rmi://localhost:8801/helloService",helloService);
        System.out.println("ServerMain provide RPC service now");
    }
}
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

/*
* 客户端远程调用RMI服务
* */
public class ClientMain {
    public static void main(String args[]) throws RemoteException, MalformedURLException, NotBoundException {
        //服务引入
        HelloService helloService = (HelloService)Naming.lookup("rmi://localhost:8801/helloService");
        //调用远程方法
        System.out.println("RMI服务器返回的结果是:" + helloService.sayHello("xudt"));
    }
}

首先运行服务端ServerMain,然后运行ClientMain。

运行结果:

RMI的通信端口是随机产生的,因此可能会被防火墙拦截,为了防止被防火墙拦截,需要强制指定RMI的通信端口,一般通过自定义一个RMISocketFactory类来实现。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.server.RMISocketFactory;

public class CustomerSocketFactory extends RMISocketFactory {
    //指定通信端口,防止被防火墙拦截
    public Socket createSocket(String host, int port)throws IOException{
        return new Socket(host, port);
    }
    public ServerSocket createServerSocket(int port)throws IOException{
        if(port == 0){
            port = 8501;
        }
        System.out.println("rmi notify port:" + port);
        return new ServerSocket(port);
    }
}
import java.io.IOException;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.RMISocketFactory;

/*
* 服务端RMI服务启动
* */
public class ServerMain {
    public static void main(String args[]) throws IOException, AlreadyBoundException {
        LocateRegistry.createRegistry(8801);
        //将新定义的类注入带Rmiserver端,指定通信窗口,防止被防火墙拦截
        RMISocketFactory.setSocketFactory(new CustomerSocketFactory());
        HelloService helloService = new HelloServiceImpl();
        Naming.bind("rmi://localhost:8801/helloService",helloService);
        System.out.println("ServerMain provide RPC service now");
    }
}

 运行结果:

三、CXF/Axis2

WebService是一种跨平台的RPC技术协议。WebService技术栈由SOAP(简易对象访问协议,使用XML进行数据编码的通信协议,独立于平台,独立于语言,因为SOAP基于HTTP协议进行数据传输,所以能绕过防火墙,SOAP提供了一种标准发放,使得运行在不同的操作系统并使用不同技术和编程语言的应用程序可以互相通信)、UDDI(统一描述、发现与集成,是一个独立于平台的框架)、WSDL(网络服务描述语言,使用XML编写的网络服务描述语言,用来描述WebService,以及如何访问WebService)组成。

1.CXF

主要特点:

  1. 支持Web Services标准。
  2. 支持JSR相关的规范和标准。
  3. 支持多种传输协议和协议绑定(SOAP、REST/HTTP、XML)、数据绑定(JAXB2.X、Aegis、ApacheXMLBeans)。

2.Axis2

新一代SOAP引擎,同时存在Java与C语言两种实现。

主要特点:

  1. 高性能:Axis2具有自己的轻量级对象模型AXIOM,且采用StAX技术。
  2. 热部署:Axis2配备了在系统启动和运行时部署Web服务和处理程序的功能。可以将新服务添加到系统,而不必关闭服务器,只需要将所需的Web服务归档存储在存储库的services目录中,部署模型将自动部署该服务并使其可供使用。
  3. 异步服务支持:Axis2支持使用非阻塞客户端和传输的异步,以及Web服务和异步服务调用。
  4. WSDL支持:Axis2支持Web服务描述语言版本1.0和2.0,它允许轻松构建存根以访问远程服务,还可以自动导出来自Axis2的已部署读物的机器可读描述。

四、Thrift

Apache Thrift是跨越不同的平台和语言,协助构建可伸缩的分布式系统的一种RPC实现,它的特点是具备广泛的语言支持,以及高性能。

Thrift是采用二进制编码协议、使用TCP/IP传输协议的一种RPC实现,对比网络数据传输,TCP/IP协议的性能高于HTTP协议,不仅因为HTTP协议是应用层协议,HTTP协议传输内容除应用数据本身外,还带有不少描述本次请求上下文的数据(比如响应状态码、Header信息等),此外,HTTP协议一般使用文本协议对传输内容进行编码,相对于一般采用二进制编码协议的TCP/IP协议码流要大。

1.Thrift工作原理

工作流程:
(1)设计所需要的服务。

  1. 根据设计的服务,编写Thrift IDL服务描述文件。
  2. 根据编写的Thrift IDL服务描述文件使用Thrift提供的代码生成工具生成服务端与客户端代码。
  3. 实现服务端业务逻辑的编写,同事实现客户端调用代码的编写。
  4. 运行服务端与客户端。

Thrift运行时的网络堆栈包括Transport、Protocol、Processor和Server4个部分:

  1. Transport:提供了通过网络读写数据的方法。
  2. Protocol:提供了对网络传输数据进行序列化/反序列化的具体实现。
  3. Processor:Thrift通过使用编写的Thrift IDL描述文件来自动生成Processor。它从负责输入的Protocol读取数据,将其传递给处理程序,并将结果发送到负责输出的Protocol。
  4. Server:Server将Transport、Protocol、Processor组合在一起,将服务运行起来,在指定端口等待调用端的请求。

2.Thrift IDL语法说明

(1)Thrift常用的数据类型

①基本类型

bool:布尔值。

byte:byte类型或8位有符号整数。

i16:16位有符号整数。

i32:32位有符号整数。

i64:64位有符号整数。

double:64位双精度符号浮点数。

string:UTF-8格式编码的字符串。

②容器类型

List<type>:数组数据类型。

Set<type>:集合数据类型,不同于数组,集合内部的数据保持唯一。

Map<type1,type2>:对应于键值对数据结构Map,其中key保持唯一。

③结构体类型

struct:一组强类型对象的封装。

eg:

struct User{

1 : string name;

2 : i32 age;

}

④enum枚举类型

Thrift不支持枚举类嵌套,枚举常量必须是32位的正整数。

eg:

enum Status{

ONE,

TWO = 2,

THREE = 3

}

(2)声明服务

服务由一组函数声明组成,每个函数都有参数、返回类型和抛出关于异常的可选信息。也可以将函数声明为oneway,这种情况下调用端不会等待该服务的响应(此时,该函数的返回类型必须是void)。

eg:

//namespace是Thrift的统一命名空间,通过设置命名空间,可以指定Thrift生成文件的位置,Java语言中是通过namespace指定生成Java文件的包路径

namespace java thrift.gencode.server
service HelloService{
    string sayHello(1:UserModel.User user , 2:string content);
    oneway void notifyMessage();
    }

(3)服务升级保持兼容性

如果需要与表更之前Thrift文件产生的代码相兼容,需要注意以下两点:

①不要变更之前已经存在的字段的编号值。

②新添加的字段可以使用optional进行修饰,以便格式兼容。

(4)基于Java注解的实现

对于客户端和服务端都采用Java实现的Thrift服务,可以采用基于注解的方式简化整个开发过程,同时避免通过编写Thrift IDL服务描述文件来自动生成哪些巨大和复杂的服务实现依赖的类文件。

五、gRPC

gRPC是Google的一个高性能、开源和通用的RPC框架,面向移动和HTTP/2开发。其基于HTTP/2标准设计,带来注入双向流、流控、头部压缩、单TCP连接上的多复用请求等特性。序列化方式默认使用Protocol Buffers。

定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个gRPC服务器来处理客户端调用。gRPC客户端和服务端可以在多种环境中运行和交互。

1.protobuf3语法

protobuf3的IDL主要有message(消息)、enum(枚举)、map(映射)、service(服务)等数据结构或元素构成。

2.message消息数据结构

eg:

syntax = “proto3”; //指定正在使用“proto3”语法,若没指定这个,编译器会使用proto2,这个指定语法必须在文件的非空、非注释的第一行

message Request{

string name = 1;

int32 limit = 2;

}

完整的属性声明组成:[字段规则] 字段类型 变量名称 = 标识号

①其中[字段规则]是可选的,有singular(一个格式良好的消息应该有0个或1个这种字段,但是不能超过1个)与repeated(在一个格式良好的消息中,这种字段可以重复任意多次,包括0次,重复的值的顺序会被保留)规则。

②标识号是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变,如果一个已有的消息格式已无法满足新的需求,需要在消息中添加一个额外的字段,同事旧版本写的代码仍然可以用,这种情况下不能更改任何已有的字段的数值标识,否则会造成新旧消息格式不兼容。

3.enum枚举类型

每个枚举类型必须将其第一个类型映射为0,这个零值必须 为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。

eg:

enum STATUS{

UNKNOWN = 0;

KNOWN = 1;

}

4.map数据类型

map代表一种关系映射关系。

eg:

map<string , int32> nameIdMap = 2;

 map的字段可以是repeated。

序列化后的顺序和map迭代器的顺序是不确定的。当为.proto文件生成文本格式的时候,map会按照key的顺序排序,数值化的key会按照数值排序。

从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用。

5.Service服务定义

如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,Protocol Buffer编译器会根据所选择的不同语言生成服务接口代码及存根。

eg:该方法能够接收Request并返回Response。

service HelloService{

rpc sayHello(Request) returns (Response);

}

六、HTTP Client

HttpComponents分为HttpClient与HttpCore两个模块,其中HttpClient提供了可以直接使用的面向用户方法,而HttpCore提供了较低层次的HttpAPI,可以用来定制个性化的客户端与服务端HTTP服务。

1.HttpClient的使用流程:

    ①构建HttpClient对象。

    ②构建URI对象。

    ③根据请求类型(GET、POST等)创建相应的请求对象(HttpGet、HttpPost等),并设置请求参数。

    ④调用HttpClient对象的execute方法发起调用。

      ⑤从返回结果中获取调用结果。

2.构建HttpClient对象

在实际生产环境中,HttpClient对象一般只初始化一次,作为单例对象使用。

构建HttpClient对象eg1:

//默认值方式构建HttpClient对象

CloseableHttpClient httpclient = HttpClients.createDefault();

构建HttpClient对象eg2(设置常用的自定义参数初始化HttpClient对象):

CloseableHttpClient httpclient = HttpClients.custom()

//自定义连接管理策略

.setConnectionManager(...)

//设置最大连接数

.setMaxConnTotal(...)

//自定义重试策略

.setRetryHandle(...)

//自定义拦截器

.addInterceptorFirst(...)

//自定义连接过期策略

.setKeepAliveStrategy(...).build();

3.构建URI对象

HttpClient提供了基于Builder模式的URI构建方法。

eg:

//构建请求的URL,http://localhost:8080/hello/sayHello.json?userName=Jack

URI uri = new URIBuilder()

.setScheme(“localhost”)

.setPort(8080)

.setPath(“/hello/sayHello.json”)

.setParameter(“userName”, ”Jack”)

.build();

(1)构建请求对象(HttpGet、HttpPost)

 构建HttpGet对象时,请求参数是直接嵌入在uri字符串里面,HttpPost对象的请求参数则可以通过Entity对象设置。

(2)构建HttpGet对象

String uriGet = “http://localhost:8080/hello/sayHello.json?userName=Jack”;

HttpGet httpGet = new HttpGet(uriGet);

(3)构建HttpPost对象

String uriPost = “http://localhost:8080/hello/sayHello.json”;

HttpPost httpPost = new HttpGet(uriPost );

List<NameValuePair> nvps = new ArrayList<NameValuePair>();

nvps.add(new BasicNameValuePair(“userName”,”Jack”);

httpPost.setEntity(new UrlEncodedFormEntity(nvps));

七、实现RPC框架

1.实现原理

Service API:定义对外服务的接口规范。

ConsumerProxy:Service API接口的代理类,内部逻辑通过Socket与服务的提供方进行通信,包括写入调用参数与获取调用返回的结果对象,通过代理使通信及获取结果等复杂逻辑对接口调用方透明。

ProviderReflect:服务的提供方,通过接收ConsumerProxy通过Socket写入的参数,定位到具体的服务实现,并通过Java反射技术实现服务的调用,然后将调用结果写入Socket,返回到ConsumerProxy。

ServiceImpl:远程服务的实现类。

2.代码实现

项目结构

ConsumerProxy

package framework;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;

/*
* 服务消费代理类实现
* */
public class ConsumerProxy {
    //服务消费代理接口
    public static <T> T consume(final Class<T> interfaceClass, final String host, final int port)throws Exception{
        //通过实现服务接口的动态代理对象获得服务接口的动态代理实例Proxy.newProxyInstance
        return (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[]{interfaceClass}, new InvocationHandler() {
            //通过实现InvocationHandler接口中的 public Object invoke(Object proxy, Method method, Object[] arguments)方法来实现远程RPC调用
            public Object invoke(Object proxy, Method method, Object[] arguments)throws Throwable{
            Socket socket = new Socket(host, port);
            try{
                //通过输出流ObjectOutputStream将调用接口的方法及参数写入Socket,发起远程调用
                ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                try{
                    output.writeUTF(method.getName());
                    output.writeObject(arguments);
                    //通过输入流ObjectInputStream从Socket中获得返回结果
                    ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                    try{
                        Object result = input.readObject();
                        if(result instanceof Throwable){
                            throw (Throwable) result;
                        }
                        return result;
                    }finally {
                        input.close();
                    }
                }finally{
                    output.close();
                }
            }finally {
                socket.close();
            }
        }
        });
    }
}

ProviderReflect

package framework;

import org.apache.commons.lang3.reflect.MethodUtils;
import sun.reflect.misc.MethodUtil;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
* 服务发布实现
* */
public class ProviderReflect {
    private static final ExecutorService executoeService = Executors.newCachedThreadPool();
    //服务发布
    public static void provider(final Object service, int port)throws Exception{
        ServerSocket serverSocket = new ServerSocket(port);
        while(true){
            final Socket socket = serverSocket.accept();
            executoeService.execute(new Runnable() {
                @Override
                public void run() {
                    try{
                        //通过输入流ObjectInputStream从Socket中按照ConsumerProxy的写入顺序逐一获取调用方法名称和参数
                        ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                        try{
                            try{
                                //方法名称
                                String methodName = input.readUTF();
                                //方法参数
                                Object[] arguments = (Object[])input.readObject();
                                ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                                try{
                                    //通过MethodUtils.invokeExactMethod对服务实现类发起反射调用,将调用结果写入Socket返回给调用方
                                    Object result = MethodUtils.invokeExactMethod(service,methodName,arguments);
                                    output.writeObject(result);
                                }catch (Throwable t){
                                    output.writeObject(t);
                                }finally {
                                    output.close();
                                }
                            }catch (Exception e){
                                e.printStackTrace();
                            }finally {
                                input.close();
                            }
                        }finally {
                            socket.close();
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

RpcConsumerMain

package invoke;

import framework.ConsumerProxy;
import service.HelloService;

/*
* 发起调用
* */
public class RpcConsumerMain {
    public static void main(String args[])throws Exception{
        HelloService service = ConsumerProxy.consume(HelloService.class,"127.0.0.1",8083);
        for(int i = 0; i < 1000; i++){
            String hello = service.sayHello("xudt" + i);
            System.out.println(hello);
            Thread.sleep(1000);
        }
    }
}

RpcProviderMain

package invoke;

import framework.ProviderReflect;
import service.HelloService;
import service.HelloServiceImpl;

/*
* 发布服务
* */
public class RpcProviderMain {
    public static void main(String args[])throws Exception{
        HelloService service = new HelloServiceImpl();
        ProviderReflect.provider(service,8083);
    }
}

HelloService

package service;

/*
* 服务接口
* */
public interface HelloService {
    public String sayHello(String content);
}

HelloServiceImpl

package service;

/*
* 远程服务接口实现
* */
public class HelloServiceImpl implements HelloService{
    @Override
    public String sayHello(String content) {
        return "Hello," + content;
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>xdt</groupId>
    <artifactId>rpctest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
    </dependencies>

</project>

    运行结果:先运行RpcProviderMain启动服务,然后运行RpcConsumerMain调用服务。

tips 

tips

1.什么是序列化?

序列化是将数据结构或对象转换成二进制串的过程,也就是编码的过程。

2.什么是反序列化?

反序列化是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。

3.为什么需要序列化?

转换为二进制串后才好进行网络传输。

4.为什么需要反序列化?

将二进制转换为对象才好进行后续处理。

5.如何选择序列化方案

从RPC的角度上看,主要看三点:

  1. 通用性,比如是否能支持Map等复杂的数据结构。
  2. 性能,包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少。
  3. 可扩展性,对互联网公司而言,业务变化快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,删除老的字段,而不影响老的服务,这将大大提供系统的健壮性。

没有更多推荐了,返回首页