欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java—RPC:远程过程调用(1)

程序员文章站 2022-06-15 17:12:21
...

Java—RPC:远程过程调用(1)

在我们学习RPC的过程中,首先我们先认识一下项目结构在发展中干的变化

一、项目结构变化

1、单体结构

​ 单体结构又叫单一项目,在我们所认识的传统项目基本上都是单一项目,j可是在互联网逐步发展的过程中,逐渐的淘汰 。单一项目的架构过于简单,改一个东西都要重新的启动,重新编译。

​ 在单体架构项目中,团队都是通过包进行区分每个模块。

优点:

​ 部署简单、维护方便、开发成本低

缺点:

​ 当项目规模过大,用户的访问量频率高,并发量大;数据量大,会大大的降低执行效率,甚至会出现宕机。

适用项目:

​ 传统管理项目、小型互联网项目。

2.分布式架构

​ 分布式架构就是将一个项目按照特定的情况和要求(或者按照模块和功能)拆分成若干个项目,把每一个项目分别的部署到不同的服务器中。

优点:

​ 增了系统可用性、增加了重用性、增加了可扩展性、增加每个模块的负载能力。

缺点:

​ 成本高、架构更加复杂、整体响应时间变长、吞吐量更大。吞吐量=请求数/秒

适用项目:

​ 中、大型互联网项目。客户多、数据多、并高发、压力大,吞吐量高的项目。

​ 通过以上的项目结构的分析,我们可以很清楚的看出每个项目结构的优缺点,在项目选择上可以更加清楚的做出选择。

​ 通过分布式架构的简介我们可以看出的是:分布式架构中的各个模块是如何进行通信?

​ 在现阶段中我们可以使用Http协议,也可以使用RPC协议通信,也可以使用其他的通信方式,在这里我们着重的介绍使用RPC协议,因为它比Http协议更适合内部通信。

二、RPC简介

​ 2.1、RFC:互联网工程任务组(IEIF)发布的文件集,每一个文件集都有自己唯一的编号,其中我们所介绍的RPC就收集在rfc 1831中。 可以通过下面的网址查看:

​ https://datatracker.ietf.org/doc/rfc1831/

​ 2.2、RPC:协议规定允许互联网中一台主机程序调用另一台主机程序,而程序员无需对这个交互过程进行编程。在RPC协议中强调的是:当X程序调用Y程序的 更能或方法时,X是不知道同时也不需要知道Y中方法具体的实现。

​ RPC是上层协议,但是它的底层是基于TCP协议,也可以基于HTTP协议。一般我们说RPC都是基于RPC的具体实现。

​ RPC框架一般都带有丰富事务服务治理功能,更加的适合企业内部接口调用。而HTTP协议更适合多平台之间相互调用。

​ 在这里我们借用HttpClient来实现RPC协议。HttpClient起初是Apache Jakarta Common 的子项目。是用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持最新的HTTP版本。到2007年的时候已经成为了*项目。

下面我们就HttpClient来简单的实现RPC从协议:

首先第一步就是创建一个项目以便我们进行测试:

Java—RPC:远程过程调用(1)

第二步导依赖:

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

第三步书写代码:

3.1:创建一个启动类:

package com.bjsxt.httpclicentserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @SpringBootApplication:是一个组合注解,用于快捷配置启动类
 *
 * 此注解等同于@aaa@qq.comaaa@qq.com的合集
 */
@SpringBootApplication
public class ServerApp {
    public static void main(String[] args) {
        SpringApplication.run(ServerApp.class,args);
    }
}
/*
@SpringBootApplication:是一个组合注解,用于快捷的培训hi启动类可配置多个启动类,但启动时需要选择以那个类作为启动类来启动项目
*/

3.2:创建一个控制类:

package com.bjsxt.httpclicentserver.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TestController {

    @RequestMapping(value="/test" ,produces={"application/json;charset=UTF-8"})
    @ResponseBody
    public  String test(){
        return "{\"msg\":\"处理返回\"}";
    }
}
/*
@Controller注解:是Spring框架提供的注解,Controller标识类,该类代表控制器(控制层/表现层)。
@RequestMapping注解:是一个用来处理请求地址映射的注解,可以用于类或方法上。用在类上表示类中所有请求的方法都是以该地址作为父路径。
    value属性:指定请求的实际地址
    method属性:指定请求方式,get\post
    consumes属性:指定处理请求的提交内容类型
    produces属性:指定返回内容类型,可以防止中文乱码
	params属性:指定request中必须包含某些参数值时,才让该方法处理
	headers属性:指定request中必须包含某些指定的header值
 @ResponseBody注解:就是将返回的对象通过MessageConverter处理之后,写入response的outputStream中返回。
*/

3.3:测试代码

以上的代码完成后,我们需要通过启动类进行测试返回的结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E4NsmY5h-1596204049432)(D:\桌面文件\RPC\05.png)]

现在我们暂时是用浏览器进行访问,将来我们就需要用代码进行访问

3.4:创建一个客户端

httpclient_rpc_client

Java—RPC:远程过程调用(1)

3.4.1:导入httpClient依赖

<dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.12</version>
        </dependency>
    </dependencies>

在我们拥有了这个代码以后我们就可以尝试的写客户端代码了

3.4.2:GET无参数客户端代码

package com.bjsxt.httpclient;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.IOException;

/**
 *使用Main方法,测试HttpClient技术
 */
public class TestHttpClient {
    public static void main(String[] args) throws IOException {
        //调用无参数GET请求
        testGetNoParams();
    }
/**
 *在这里我们创建一个静态的没有返回值的类
 * 无参数GET请求
 * 在我们用HttpClient发送请求的时候我们回忆一下
 * 使用浏览器访问网站的过程
 * 1、打开浏览器
 * 2、输入网址
 * 3、访问
 * 4、看结果
 * 使用HttpClient,访问WEB服务器的过程
 * 1、创建客户端,相当于打开浏览器
 * 2、创建请求地址,相当于输入网址
 * 3、发送请求,相当于访问王网址(回车键)
 * 4、返回处理结果,相当于浏览器返回结果
 *通过以上的对比我们可以看出两者之间的相似的程度
 */
    public static void testGetNoParams() throws IOException {
        /*
        注意的是不要导错包了,我们所导入的是
        import org.apache.http.client.HttpClient;包下的HttpClient
         */
        //创建客户端对象
        HttpClient  client = HttpClients.createDefault();

        /*
        创建请求地址
        我们所请求的地址是以字符串形式展现的
        IP:localhost
        端口号:8080
        请求路径:test
         */
        HttpGet get = new HttpGet("http://localhost:8080/test");
        //发起请求,接收响应对象
        HttpResponse response = client.execute(get);
        /*
        获取响应体。响应数据是一个基于HTTP协议标准字符串封装的对象
        响应体和响应头,都是封装的HTTP协议数据。直接使用可能会出现乱码或解析错误.
        所以我们要对数据进行转换。
         */
        HttpEntity entity = response.getEntity();

        /*
        通过HTTP实体类工具,转换响应体数据,使用的字符集是UTF-8
        UTF-8,也可以省略不写,因为默认的就是UTF-8字符集
         */
        String responseString = EntityUtils.toString(entity, "UTF-8");

        System.out.println("服务器响应的结果是-[" + responseString +"]");

        //回收资源
        client = null;
    }
}

3.4.3:返回结果


服务器响应的结果是-[{"msg":"处理返回"}]

Process finished with exit code 0

以上的代码是我们运用的是无参数的请求方式。

3.5:有参数的客户端代码

3.5.1:首先到控制器中增加一个有参数的控制类

 @RequestMapping(value="/params",produces = "application/json;charset=UTF-8")
    @ResponseBody
    public String params(String name,String password){
        System.out.println("name - "+name +";password -" +password);
        return "{\"msg\":\"登录成功\",\"user\"{\"name\":\""+name+"\";\"password\":\""+password+"\"}}";
    }

有参数的控制器写完之后,重新启动一下启动类。

3.5.2:GET有参数客户端代码

//调用有参数GET请求
        testGetParams();
    }

    /*
    有参数的GET请求
     */
    public static void testGetParams() throws URISyntaxException, IOException {
        //创建客户端对象
        HttpClient client = HttpClients.createDefault();

        //基于Builder构建请求地址,有异常直接抛出
        URIBuilder builder = new URIBuilder("http://localhost:8080/params");

        /*//基于单参数传递,构建请求地址
        builder.addParameter("name","bjsxt");
        builder.addParameter("password","123456");
        URI uri= builder.build();*/

        /*基于多参数传递,构建请求地址
        在导入NameValuePair的时候需要注意不要导错包了
        我们所导入的是一个接口在http下:import org.apache.http.NameValuePair;
        NameValuePair是一个接口,自己构建的话直接return返回字符串就行。
        也可以使用BasicNameValuePair,BasicNameValuePair是NameValuePair接口的实现类,
        可以直接new对象,也可以直接传name和value

        或者直接在http请求地址的后面直接加上?
         */
        List<NameValuePair> nvps = new ArrayList<>();
        nvps.add(new BasicNameValuePair("name","bjsxt"));
        nvps.add(new BasicNameValuePair("password","123456"));
        builder.addParameters(nvps);
        URI uri = builder.build();
        System.out.println(uri.toASCIIString());

        /*
        在无参数中我们分开请求的,在这里我们是一起进行处理的。
         */
        String result = EntityUtils.toString(client.execute(new HttpGet(uri)).getEntity());
        System.out.println(result);
    }

3.5.3:返回的结果

http://localhost:8080/params?name=bjsxt&password=123456
{"msg":"登录成功""user"{"name":"bjsxt";"password":"123456"}}

Process finished with exit code

上面的代码操作的都是GET请求的数据,下面我么们开始介绍POST请求

3.6:POST无参数客户端代码

post请求和get请求唯一的区别就是在于我们请求的对象,其它的区别上不大。

public static void testPost() throws IOException {
        //创建客户端对象
        HttpClient client = HttpClients.createDefault();

        /*
        无参的post请求,test对应的是无参的请求
        与get请求唯一的区别就是这里用的是HttpPost
         */
        HttpPost post = new HttpPost("http://localhost:8080/test");
        /*
        有异常直接抛出
         */
        HttpResponse response = client.execute(post);
        System.out.println(EntityUtils.toString(response.getEntity(),"UTF-8"));
    }

3.6:POST有参数的客户端代码:

/*
        有参数的post请求。请求头携带参数。和get请求携带的参数的方式一致
       有异常直接抛出,咱们的对象不用重新创建,就像浏览器页面一样,不需要关了在启动。
       在这里还可以在请求的地址后面加上?参数&参数
         */
        URIBuilder builder = new URIBuilder("http://localhost:8080/params");
        builder.addParameter("name","post");
        builder.addParameter("password","post123456");
        HttpResponse postResponse = client.execute(new HttpPost(builder.build()));
        System.out.println(EntityUtils.toString(postResponse.getEntity(),"UTF-8"));

        /*
        请求体传递参数
        在我们使用post请求的时候,我们一般都是使用请求体进行传参,在非专业人事的眼中是隐藏路径的
         */
        HttpPost bodyParamsPost = new HttpPost("http://localhost:8080/params");

        /*
        定义请求协议体,设置请求参数,这是传字符串形式
        使用请求体传递参数的时候。需要定义请求格式,默认的是表单格式,
        通常我们都会把默认的请求格式进行修改成阶层格式
        使用URIbuilder构建的URI对象,就是请求体传递参数
         */
        HttpEntity entity = new StringEntity("name=bjsxt&password=123");

        /*
        给响应体是同一个类型,它所代表的就是协议体,当两个子类型分别是响应协议体和请求协议体
        面向父类的方式进行开发的
         */
        bodyParamsPost.setEntity(entity);

        //输出
        System.out.println(EntityUtils.toString(client.execute(bodyParamsPost).getEntity(),"UTFa-8"));


    }

返回结果

{"msg":"处理返回"}
{"msg":"登录成功""user"{"name":"post";"password":"post123456"}}
{"msg":"登录成功""user"{"name":"null";"password":"null"}}

Process finished with exit code 0

其中第三的参数属于没有传过来,原因是请求体格式不符合标准要求。这种的请求方式对应的是另一种格式。

3.7:解决参数为空

3.7.1在控制器类增加一个控制类

 //使用请请求体传递请求参数
 @RequestMapping( value = "/bodyParams",produces ="application/json;charset=UTF-8")
    @ResponseBody
    @CrossOrigin
//在这里我先传入了实体类User,这个实体类应该在实体类创建完毕后再传入的。注意一下。
    public String bodyParams(@RequestBody List<User> users){
        System.out.println(users);
        return users.toString();
    }
/*
 @CrossOrigin注解:是用来处理跨域请求的注解
 @RequestBody注解:作用其实是将json格式的数据转为java对象
*/

​ 在这里导入包的时候请注意包是不是导入错误,在这里我们应该导入的是自己的User,而不是导入catalina的User。这样的话会报一个错误:

控制台报的错误:

{"timestamp":"2020-07-31T12:29:27.686+00:00","status":500,"error":"Internal Server Error","message":"","path":"/bodyParams"}

服务器日志:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.apache.catalina.User` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.apache.catalina.User` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

3.7.2:创建一个实体类项目

Java—RPC:远程过程调用(1)

3.7.3:在实体类项目中书写实体类的代码

package com.bjsxt.httpclient;

import java.io.Serializable;
import java.util.Objects;

public class User implements Serializable {
    private String name;
    private String password;

    public User() {
    }

    public User(String name, String password) {
        this.name = name;
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name) &&
                Objects.equals(password, user.password);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, password);
    }

    /*
    在这里我把toString代码进行了修改
    */
    @Override
    public String toString() {
        return "{\"name\":\""+name+"\", \"password\":\""+password+"\"}";
    }
}

3.7.4:实体类结束之后需要在另外的两个项目中分别增加依赖

	   <dependency>
            <groupId>org.example</groupId>
            <artifactId>httpclient_rpc_pojo</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

书写完毕后重启一下项目。

再做请求的时候我们就会拿请求体传递数据了。

3.7.5:这样的话我们下面就需要做一些代码上的修改,修改如下,暂时有bug还为解决,已解决。

原因:

​ 控制类导包错误,上面已有解释造成的原因。

/*
        请求体传递参数
        在我们使用post请求的时候,我们一般都是使用请求体进行传参,在非专业人事的眼中是隐藏路径的
         */
        HttpPost bodyParamsPost = new HttpPost("http://localhost:8080/bodyParams");

        /*
        定义请求协议体,设置请求参数,这是传字符串形式
        使用请求体传递参数的时候。需要定义请求格式,默认的是表单格式,
        通常我们都会把默认的请求格式进行修改成阶层格式
        使用URIbuilder构建的URI对象,就是请求体传递参数
         */
        User u1 = new User();
        u1.setName("name1");
        u1.setPassword("password1");
        User u2 = new User();
        u2.setName("name2");
        u2.setPassword("password2");

        /*这是一个集合的表达形式json格式字符串,表示请求参数,一个List<User>
        但是这里面的toString方法不能直接用,这里是我修=修改过的toString方法。
         */
        String paramsString = "["+u1.toString()+","+u2.toString()+"]";
        /*
        StringEntity除了可以传字符串(String)、字符集(Charset)和请求体的格式(mimType)
         */
        HttpEntity entity = new StringEntity(paramsString,"application/json","UTF-8");

        /*
        给响应体是同一个类型,它所代表的就是协议体,当两个子类型分别是响应协议体和请求协议体
        面向父类的方式进行开发的
         */
        bodyParamsPost.setEntity(entity);

        //输出
        System.out.println(EntityUtils.toString(client.execute(bodyParamsPost).getEntity(),"UTF-8"));


    }

3.7.6:返回的结果

{"msg":"处理返回"}
{"msg":"登录成功""user"{"name":"post";"password":"post123456"}}
[{"name":"name1", "password":"password1"}, {"name":"name2", "password":"password2"}]

Process finished with exit code 0

jackson和gson在性能和效率上没有区别。Spring底层和json转换对象用的都是jackson。而我们开发的时候基本上都会用到Spring,尽量和他的应用和规则进行匹配。

3.8:下面我们做一下String字符串和Json字符串之间的转换。

3.8.1:首先在实体类中增加数据

private Date birth;

    public int getAge(){
        if (birth == null){
            return -1;
        }
        //生日的年份
        int brithYear = birth.getYear();
        //当前的年份
        int currentYear = new Date().getYear();
        //年龄
        return  currentYear - brithYear;
    }

    public void setAge(int age){

    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

3.8.2:控制台代码:

/*
        请求体传递参数
        在我们使用post请求的时候,我们一般都是使用请求体进行传参,在非专业人事的眼中是隐藏路径的
         */
        HttpPost bodyParamsPost = new HttpPost("http://localhost:8080/bodyParams");

        /*
        定义请求协议体,设置请求参数,这是传字符串形式
        使用请求体传递参数的时候。需要定义请求格式,默认的是表单格式,
        通常我们都会把默认的请求格式进行修改成阶层格式
        使用URIbuilder构建的URI对象,就是请求体传递参数
         */
        User u1 = new User();
        u1.setName("name1");
        u1.setPassword("password1");
        User u2 = new User();
        u2.setName("name2");
        u2.setPassword("password2");
        List<User> users = new ArrayList<>();
        users.add(u1);
        users.add(u2);
        // 把集合users -> JSON字符串
        // 创建Jackson中的转换器对象
        ObjectMapper objectMapper = new ObjectMapper();

        /*
        java对象返回json格式字符串,不是所有的java对象都能转换为json格式的字符串
        java字符串转换为json格式字符串的时候是遵循一个原则
        默认get对应的property进行转换拼一个字符串。
        补充:
        property - 类型中getter和setter的命名。去除set|get,剩余部分首字母转小写。
        <bean><property name="abc"></property></bean>
        field - 类型中定义的实例变量。
         */
        String paramsString = objectMapper.writeValueAsString(users);

        System.out.println(paramsString);



        /*这是一个集合的表达形式json格式字符串,表示请求参数,一个List<User>
        但是这里面的toString方法不能直接用,这里是我修=修改过的toString方法。
        String paramsString = "["+u1.toString()+","+u2.toString()+"]";
         */
        /*
        StringEntity除了可以传字符串(String)、字符集(Charset)和请求体的格式(mimType)
         */
        HttpEntity entity = new StringEntity(paramsString,"application/json","UTF-8");

        /*
        给响应体是同一个类型,它所代表的就是协议体,当两个子类型分别是响应协议体和请求协议体
        面向父类的方式进行开发的
         */
        bodyParamsPost.setEntity(entity);
        System.out.println(EntityUtils.toString(client.execute(bodyParamsPost).getEntity(),"UTF-8"));

3.8.3:返回的结果:

{"msg":"处理返回"}
{"msg":"登录成功""user"{"name":"post";"password":"post123456"}}
[{"name":"name1","password":"password1","birth":null,"age":-1},{"name":"name2","password":"password2","birth":null,"age":-1}]
[{"name":"name1", "password":"password1"}, {"name":"name2", "password":"password2"}]

上面是已完成的代码:把String字符串转换为json格式字符串,如果我们想把json格式字符串转换为String对象怎么办?反向处理具体的操作如下:

 /*
        给响应体是同一个类型,它所代表的就是协议体,当两个子类型分别是响应协议体和请求协议体
        面向父类的方式进行开发的
         */
        bodyParamsPost.setEntity(entity);
        /*System.out.println(EntityUtils.toString(client.execute(bodyParamsPost).getEntity(),"UTF-8"));*/
        String responseString  = EntityUtils.toString(client.execute(bodyParamsPost).getEntity(), "UTF-8");

        //这是一个但对象,从下标1开始到,结束。包含开始下标不包含结束下标
        String userstring = responseString.substring(1, responseString.indexOf("},") + 1);

        User responseUser = objectMapper.readValue(userstring,User.class);

        System.out.println(responseUser);

        //构建一个Jackson识别的Java类型映射,以下是集合的转换方式
        JavaType valueType =
                objectMapper.getTypeFactory().constructParametricType(List.class, User.class);
        List<User> list = objectMapper.readValue(responseString, valueType);
        System.out.println(list);

返回的结果如下:

{"msg":"处理返回"}
{"msg":"登录成功""user"{"name":"post";"password":"post123456"}}
[{"name":"name1","password":"password1","birth":null,"age":-1},{"name":"name2","password":"password2","birth":null,"age":-1}]
{"name":"name1", "password":"password1"}
[{"name":"name1", "password":"password1"}, {"name":"name2", "password":"password2"}]

Process finished with exit code 0

System.out.println(responseUser);

    //构建一个Jackson识别的Java类型映射,以下是集合的转换方式
    JavaType valueType =
            objectMapper.getTypeFactory().constructParametricType(List.class, User.class);
    List<User> list = objectMapper.readValue(responseString, valueType);
    System.out.println(list);

返回的结果如下:

```java
{"msg":"处理返回"}
{"msg":"登录成功","user"{"name":"post";"password":"post123456"}}
[{"name":"name1","password":"password1","birth":null,"age":-1},{"name":"name2","password":"password2","birth":null,"age":-1}]
{"name":"name1", "password":"password1"}
[{"name":"name1", "password":"password1"}, {"name":"name2", "password":"password2"}]

Process finished with exit code 0

相关标签: java rpc