【RPC】

主要内容

项目结构变化RPC简介RMI实现RPCHttpClient实现RPCZookeeper安装Zookeeper客户端常用命令向Zookeeper中注册内容从Zookeeper中发现内容手写RPC框架

学习目标

知识点要求项目架构变化掌握RPC简介掌握RMI实现RPC掌握HttpClient实现RPC了解Zookeeper安装掌握Zookeeper客户端常用命令掌握向Zookeeper中注册内容掌握从zookeeper中发现内容掌握手写RPC框架掌握

1 项目架构变化

  现在学习RPC。因为后期学习的Dubbo是一个RPC框架,学习好现在的内容,学习Dubbo将会变得容易一些。

1.1 单体架构

  1. 架构图

    单体架构就是一个项目里面包含这个项目中全部代码。一个应用搞定全部功能。

    DNS 服务器可以是单映射,也可以配置多个映射。

  2. 软件代码结构

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

    总体包结构:com.uestc.*.分层包。

项目名:

-- com

--uestc

-- common

-- utils

--user

-- controller

-- service

-- mapper

-- sys

-- controller

-- service

-- mapper

  3. 优缺点

    优点:(1)部署简单;(2)维护方便;(3)成本低。

    缺点:当项目规模大、用户访问频率高、并发量大、数据量大时,会大大降低程序执行效率,甚至出现服务器宕机等情况。

  4. 适用项目:传统管理项目,小型互联网项目。

1.2 分布式架构

  1. 架构图(简易版)

  分布式架构会把一个项目按照特定要求(多按照模块或功能)拆分成多个项目,每个项目分别部署到不同的服务器上。

  2. 软件代码结构

项目1:

--com.uestc.xxx

-- controller

-- service

-- mapper

项目2

--com.uestc.mmm

-- controller

-- service

-- mapper

  3. 优缺点

    优点:(1)增大了系统可用性。减少单点故障,导致整个应用不可用;(2)增加重用性。因为模块化,所以重用性更高;(3)增加可扩展性。有新的模块增加新的项目即可;(4)增加每个模块的负载能力。因为每个模块都是一个项目,所以每个模块的负载能力更强。

    缺点:(1)成本更高;(2)架构更加复杂(3)整体响应之间变长,一些业务需要多项目通信后给出结果;(4)吞吐量更大。吞吐量= 请求数/秒。

  4. 待解决问题:分布式架构中各个模块如何进行通信?

    可以使用Http协议,也可以使用RPC协议通信,也可以使用其他的通信方式。我们本阶段使用的是RPC协议,因为它比HTTP更适合项目内部通信。

2 RPC简介

2.1 RFC

  RFC(Request For Comments) 是由互联网工程任务组(IETF)发布的文件集。文件集中每个文件都有自己唯一编号,例如:rfc1831。目前RFC文件由互联网协会(Internet Society,ISOC)赞助发型。

  RPC就收集到了rfc 1831中。可以通过下面网址查看:

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

2.2 RPC

  RPC在rfc 1831中收录 ,RPC(Remote Procedure Call) 远程过程调用协议

  RPC协议规定允许互联网中一台主机程序调用另一台主机程序,而程序员无需对这个交互过程进行编程。在RPC协议中强调当A程序调用B程序中功能或方法时,A是不知道B中方法具体实现的。

  RPC是上层应用层协议,底层可以基于TCP协议,也可以基于HTTP协议。一般我们说RPC都是基于RPC的具体实现,如:Dubbo框架。从广义上讲只要是满足网络中进行通讯调用都统称为RPC,甚至HTTP协议都可以说是RPC的具体实现,但是具体分析看来RPC协议要比HTTP协议更加高效,基于RPC的框架功能更多。

  RPC协议是基于分布式架构而出现的,所以RPC在分布式项目中有着得天独厚的优势。

2.3 RPC和HTTP对比

(1)具体实现

  RPC:可以基于TCP协议,也可以基于HTTP协议。

  HTTP:基于HTTP协议。

(2)效率

  RPC:自定义具体实现可以减少很多无用的报文内容,使得报文体积更小。

  HTTP:如果是HTTP 1.1 报文中很多内容都是无用的。如果是HTTP2.0以后和RPC相差不大,比RPC少的可能就是一些服务治理等功能。

(3)连接方式

  RPC:长连接支持。

  HTTP:每次连接都是3次握手。

(4)性能

  RPC可以基于很多序列化方式。如:thrift

  HTTP 主要是通过JSON,序列化和反序列效率更低。

(5)注册中心   RPC :一般RPC框架都带有注册中心。

  HTTP:都是直连。

(6)负载均衡

  RPC:绝大多数RPC框架都带有负载均衡测量。

  HTTP:一般都需要借助第三方工具。如:nginx

(7)综合结论

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

3 HttpClient实现RPC

3.1 HttpClient简介

  在JDK中java.net包下提供了用户HTTP访问的基本功能,但是它缺少灵活性或许多应用所需要的功能。

  HttpClient起初是Apache Jakarta Common 的子项目。用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本。2007年成为顶级项目。

  通俗解释:HttpClient可以实现使用Java代码完成标准HTTP请求及响应。

3.2 代码实现

(1)服务端   新建项目HttpClientServer

(2)新建控制器   com.uestc.controller.DemoController

@Controller

public class DemoController {

@RequestMapping("/demo")

@ResponseBody

public String demo(String param){

return "demo"+param;

}

}

(3)新建启动器

  新建启动器:com.msb.HttpClientServerApplication

@SpringBootApplication

public class HttpClientServerApplication {

public static void main(String[] args) {

SpringApplication.run(HttpClientServerApplication.class,args);

}

}

(4)客户端   新建HttpClientDemo项目

(5)添加依赖

org.apache.httpcomponents

httpclient

4.5.10

(6)新建类

  新建com.uestc.HttpClientDemo,编写主方法。

(7)使用GET方法访问

public static void main(String[] args) {

try {

//创建http工具(理解成:浏览器) 发起请求,解析响应

CloseableHttpClient httpClient = HttpClients.createDefault();

//请求路径

URIBuilder uriBuilder = new URIBuilder("http://localhost:8080/demo");

uriBuilder.addParameter("param", "get123");

//创建HttpGet请求对象

HttpGet get = new HttpGet(uriBuilder.build());

//创建响应对象

CloseableHttpResponse response = httpClient.execute(get);

//由于响应体是字符串,因此把HttpEntity类型转换为字符串类型,并设置字符编码

String result = EntityUtils.toString(response.getEntity(), "utf-8");

//输出结果

System.out.println(result);

//释放资源

response.close();

httpClient.close();

} catch (URISyntaxException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

}

(8)使用POST方式访问

public class HttpClientDemo {

public static void main(String[] args) {

try {

//创建http工具(理解成:浏览器) 发起请求,解析响应

CloseableHttpClient httpClient = HttpClients.createDefault();

//创建HttpPOST请求对象

HttpPost post = new HttpPost("http://localhost:8080/demo");

//所有请求参数

List params = new ArrayList<>();

params.add(new BasicNameValuePair("param","123"));

//创建HttpEntity接口的文本实现类的对象,放入参数并设置编码

HttpEntity httpEntity = new UrlEncodedFormEntity(params,"utf-8");

//放入到HttpPost对象中

post.setEntity(httpEntity);

//创建响应对象

CloseableHttpResponse response = httpClient.execute(post);

//由于响应体是字符串,因此把HttpEntity类型转换为字符串类型

String result = EntityUtils.toString(response.getEntity());

//输出结果

System.out.println(result);

//释放资源

response.close();

httpClient.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

3.3 Jackson用法

  简单来说,从服务器上写好的对象或者对象集合想要发送给浏览器,就要转化为json字符串,而浏览器中的数据发送给服务器也要从json字符串解析为对象或者对象集合以供后续使用。

  1. 把对象转换为json字符串

ObjectMapper objectMapper = new ObjectMapper();

People peo = new People();

objectMapper.writeValueAsString(peo);

  2. 把json字符串转换为对象

ObjectMapper objectMapper = new ObjectMapper();

People peo = objectMapper.readValue(content, People.class);

  3. 把json字符串转换为List集合

ObjectMapper objectMapper = new ObjectMapper();

JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, People.class);

List list = objectMapper.readValue(content, javaType);

3.4 HttpClient请求包含JSON

public class HttpClientDemo {

public static void main(String[] args) {

try {

CloseableHttpClient httpClient = HttpClients.createDefault();

HttpPost post = new HttpPost("http://localhost:8080/demo");

HttpEntity httpEntity= null;

String json = "{}";

StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);

post.setEntity(entity);

CloseableHttpResponse response = httpClient.execute(post);

String result = EntityUtils.toString(response.getEntity());

System.out.println(result);

response.close();

httpClient.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

3.5 控制器接口参数

  @RequestBody把请求体中流数据转换为指定的对象。多用在请求参数是json数据且请求的Content-Type=”application/json”(注意:流数据是指,除了文本类型之外的数据都统称为流数据)

@RequestMapping("/demo4")

@ResponseBody

public String demo4(@RequestBody List list) {

System.out.println(list);

return list.toString();

}

3.6 Ajax发送json参数写法

var json = '[{"id":123,"name":"msb"},{"id":123,"name":"mashibing"}]';

$.ajax({

url:'/demo5',

type:'post',

success:function(data){

alert(data);

for(var i = 0 ;i

alert(data[i].id +" "+data[i].name);

}

},

contentType:'application/json',//请求体中内容类型

dataType:'json',//响应内容类型。

data:json

});

3.7 跨域

  跨域:协议、ip、端口中只要有一个不同就是跨域请求。

  同源策略:浏览器默认只允许ajax访问同源(协议、ip、端口都相同)内容。

  解决同源策略:在控制器接口上添加@CrossOrigin。表示允许跨域。本质在响应头中添加Access-Control-Allow-Origin: *

var json = '[{"id":123,"name":"msb"},{"id":456,"name":"mashibing"}]';

$.ajax({

url:'/demo5',

type:'post',

success:function(data){

alert(data);

for(var i = 0 ;i

alert(data[i].id +" "+data[i].name);

}

},

contentType:'application/json',//请求体中内容类型

dataType:'json',//响应内容类型。

data:json

});

4 RMI实现RPC

4.1 RMI简介

  RMI(Remote Method Invocation) 远程方法调用。

  RMI是从JDK1.2推出的功能,它可以实现在一个Java应用中可以像调用本地方法一样调用另一个服务器中Java应用(JVM)中的内容。

  RMI 是Java语言的远程调用,无法实现跨语言。

4.2 执行流程

  Registry(注册表)是放置所有服务器对象的命名空间。 每次服务端创建一个对象时,它都会使用bind()或rebind()方法注册该对象。 这些是使用称为绑定名称的唯一名称注册的。

  要调用远程对象,客户端需要该对象的引用。即通过服务端绑定的名称从注册表中获取对象(lookup()方法)。

4.3 API介绍

(1)Remote

  java.rmi.Remote 定义了此接口为远程调用接口。如果接口被外部调用,需要继承此接口。

public interface Remote{}

(2)RemoteException

  java.rmi.RemoteException

  继承了Remote接口的接口中,如果方法是允许被远程调用的,需要抛出此异常。

(3)UnicastRemoteObject

  java.rmi.server.UnicastRemoteObject

  此类实现了Remote接口和Serializable接口。

  自定义接口实现类除了实现自定义接口还需要继承此类。

(4)LocateRegistry   java.rmi.registry.LocateRegistry

  可以通过LocateRegistry在本机上创建Registry,通过特定的端口就可以访问这个Registry。

(5)Naming

  java.rmi.Naming

  Naming定义了发布内容可访问RMI名称。也是通过Naming获取到指定的远程方法。

4.4 代码实现

4.4.1 服务器端

(1)服务端创建

  创建RmiServer项目

(2)编写接口

  com.uestc.service.DemoService 编写

public interface DemoService extends Remote {

String demo(String demo) throws RemoteException;

}

(3)编写实现类

  com.uestc.service.impl.DemoServiceImpl 编写。

  注意:构造方法是public的。默认生成protected

public class DemoServiceImpl extends UnicastRemoteObject implements DemoService {

public DemoServiceImpl() throws RemoteException {

}

@Override

public String demo(String demo) throws RemoteException {

return demo+"123";

}

}

(4)编写主方法

  编写com.uestc.DemoServer类,生成主方法

public class DemoServer {

public static void main(String[] args) {

try{

//创建接口实例

DemoService demoService = new DemoServiceImpl();

//创建注册表

LocateRegistry.createRegistry(8989);

//绑定服务

Naming.bind("rmi://localhost:8989/demoService", demoService);

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

}catch(Exception e ){

e.printStackTrace();

}

}

}

(5)运行项目

  运行后项目,项目一直处于启动状态,表示可以远程访问此项目中的远程方法。

4.4.2 客户端

(1)创建客户端代码

  创建项目RmiClient

(2)复制服务端接口

  把服务端com.uestc.service.DemoService粘贴到项目中

(3)创建主方法类

  新建com.uestc.DemoClient

public class ClientDemo {

public static void main(String[] args) {

try {

DemoService demoService =(DemoService)Naming.lookup("rmi://localhost:8989/demoService");

String result = demoService.demo("jayden ");

System.out.println(result);

} catch (NotBoundException e) {

e.printStackTrace();

} catch (MalformedURLException e) {

e.printStackTrace();

} catch (RemoteException e) {

e.printStackTrace();

}

}

}

(4)运行项目然后用浏览器访问查看结果。

5 Zookeeper安装

5.1 Zookeeper简介

  zookeeper分布式管理软件。常用它做注册中心(依赖zookeeper的发布/订阅功能)、配置文件中心、分布式锁配置、集群管理等。

  zookeeper一共就有两个版本。主要使用的是java语言写的。

5.2 安装

(1)使用你虚拟机或者云服务器的linux服务器上传压缩文件

  到官网https://zookeeper.apache.org/找到download下载zookeeper压缩包apache-zookeeper-3.5.5-bin.tar.gz,然后上传到 /usr/local/tmp中。

(2)解压

tar zxf apache-zookeeper-3.5.5-bin.tar.gz

cp -r apache-zookeeper-3.5.5-bin ../zookeeper

(3)新建data目录

  进入到zookeeper中

cd /usr/local/zookeeper

mkdir data

(4)修改配置文件

  进入conf中

cd conf

cp zoo_sample.cfg zoo.cfg

vim zoo.cfg

  修改dataDir为data文件夹路径

dataDir=/usr/local/zookeeper/data

(5)启动zookeeper

  进入bin文件夹

cd /usr/local/zookeeper/bin

./zkServer.sh start

  通过status查看启动状态。稍微有个等待时间

./zkServer.sh status

5.3 Zookeeper客户端常用命令

  进入到./zkCli.sh命令行工具后,可以使用下面常用命令

1 ls

ls [-s][-R] /path

  -s 详细信息,替代老版的ls2

  -R 当前目录和子目录中内容都罗列出来

  例如:ls -R / 显示根目录下所有内容

2 create

create /path [data]

  [data] 包含内容

  创建指定路径信息

  例如:create /demo 创建/demo

3 get

get [-s] /path

  [-s] 详细信息

  查看指定路径下内容。

  例如: get -s /demo

  null:存放的数据

  cZxid:创建时zxid(znode每次改变时递增的事务id)

  ctime:创建时间戳

  mZxid:最近一次更新的zxid

  mtime:最近一次更新的时间戳

  pZxid:子节点的zxid

  cversion:子节点更新次数

  dataversion:节点数据更新次数

  aclVersion:节点ACL(授权信息)的更新次数

  ephemeralOwner:如果该节点为ephemeral节点(临时,生命周期与session一样), ephemeralOwner值表示与该节点绑定的session id. 如果该节点不是ephemeral节点, ephemeralOwner值为0.

  dataLength:节点数据字节数

  numChildren:子节点数量

4 set

set /path data

  设置节点内容

5 delete

delete /path

  删除节点

5.4 向Zookeeper中注册内容

  新建项目ZookeeperClient

(1)创建/demo

  使用zookeeper的客户端命令工具创建/demo

./zkCli.sh

create /demos

(2)添加依赖

org.apache.zookeeper

zookeeper

3.5.5

(3)编写代码

  创建类com.uestc.MyApp。

  ZooDefs.Ids.OPEN_ACL_UNSAFE 表示权限,具体完全开放的ACL,任何连接的客户端都可以操作该属性znode。

  CreateMode.PERSISTENT_SEQUENTIAL 永久存储,文件内容编号递增。

public static void main(String [] args){

try {

ZooKeeper zookeeper = new ZooKeeper("192.168.32.128:2181", 10000, new Watcher() {

@Override

public void process(WatchedEvent watchedEvent) {

System.out.println("获取连接");

}

});

String content = zookeeper.create("/demo/nn", "content".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);

System.out.println("content"+content);

} catch (IOException e) {

e.printStackTrace();

} catch (KeeperException e) {

e.printStackTrace();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

(4)查看上传数据

​ ls -R / :查看列表

​ get /demo/nn0000000002 :查看内容

5.5 从zookeeper中发现内容

  在原有项目中新建一个类,类中编写主方法。

public static void main(String[] args) {

try {

ZooKeeper zookeeper = new ZooKeeper("192.168.32.128:2181", 10000, new Watcher() {

@Override

public void process(WatchedEvent watchedEvent) {

System.out.println("获取连接");

}

});

//获取列表

List list = zookeeper.getChildren("/demo", false);

for (String child : list) {

byte[] result = zookeeper.getData("/demo/" + child, false, null);

System.out.println(new String(result));

}

} catch (IOException e) {

e.printStackTrace();

} catch (KeeperException e) {

e.printStackTrace();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

6 手写RPC框架

  使用Zookeeper作为注册中心,RMI作为连接技术,手写RPC框架。

6.1 创建项目ParentDemo

  创建父项目ParentDemo。

  包含4个聚合子项目。

  pojo: service和consumer中需要的实体类

  service:包含被serviceimpl和consumer依赖的接口。

  provider:provider中写两个方法,一个是实现service的接口,第二个是作为服务器向zookeeper注册内容,并且用Naming绑定zookeeper内容和具体实现的实现类。

  consumer:消费者,调用服务内容。

6.2 创建pojo项目

  略,就是写一个可能用到的实力类,记得这个类要实现序列化Serializable。

6.2 创建service项目

  主要就是编写服务器provider需要实现的接口集合,之后服务器只需要针对service实现各个接口即可,方便管理。这里要用到pojo,所以要在依赖中依赖pojo项目。

com.uestc

pojo

1.0-SNAPSHOT

  创建com.uestc.DemoService

public interface MyPersonService extends Remote {

// 找到所有的Person

public List findAll() throws RemoteException;

}

6.3 创建provider项目

  此项目编写service项目中接口的具体实现,RMI服务发布和把信息发送到Zookeeper中。

  项目结构如下:

(1)在pom.xml中添加对service项目的依赖

com.uestc

service

1.0-SNAPSHOT

org.apache.zookeeper

zookeeper

3.5.7

(2)创建接口的实现类DemoServiceImpl,实现service的接口方法

  创建com.uestc.service.impl.DemoServiceImpl

public class MyPersonServiceImpl extends UnicastRemoteObject implements MyPersonService {

public MyPersonServiceImpl() throws RemoteException {

}

public List findAll() throws RemoteException {

List personList = new ArrayList();

personList.add(new Person(1, "jayden"));

personList.add(new Person(2, "kim"));

return personList;

}

}

(3)创建RmiRun   创建com.uestc.ProviderRun。实现RMI服务的发布和Zookeeper消息的发布。

public class ProviderRun {

public static void main(String[] args) {

try {

MyPersonService myPersonService = new MyPersonServiceImpl();

LocateRegistry.createRegistry(8089);

String url = "rmi://localhost:8089/myPersonService";

Naming.bind(url, myPersonService);

System.out.println("RMI服务启动成功");

//创建zookeeper并发布信息

ZooKeeper zooKeeper = new ZooKeeper("82.156.182.26:2181", 100000, new Watcher() {

public void process(WatchedEvent watchedEvent) {

System.out.println("获取链接");

}

});

//注意要在zookeeper注册一个文件夹/rpc

zooKeeper.create("/rpc/provider",url.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

System.out.println("注册成功");

} catch (Exception e) {

e.printStackTrace();

}

}

}

6.4 创建consumer项目

  新建consumer项目,此项目需要从zookeeper中获取rmi信息,并调用rmi服务

(1)在pom.xml中添加对service项目的依赖,因为service已经依赖过pojo项目了,所以根据继承Consumer项目也依赖了pojo项目

com.uestc

service

1.0-SNAPSHOT

org.springframework.boot

spring-boot-starter-web

2.6.6

org.apache.zookeeper

zookeeper

3.5.7

(2) 创建接口和实现类

  创建com.uestc.service.PersonService接口

  创建com.uestc.service.impl.PersonServiceImpl实现类

public interface PersonService {

public List show();

}

@Service

public class PersonServiceImpl implements PersonService {

public List show() {

try {

ZooKeeper zooKeeper = new ZooKeeper("82.156.182.26:2181", 100000, new Watcher() {

public void process(WatchedEvent watchedEvent) {

System.out.println("链接成功");

}

});

byte[] result = zooKeeper.getData("/rpc/provider", false, null);

MyPersonService myPersonService = (MyPersonService) Naming.lookup(new String(result));

return myPersonService.findAll();

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

}

(3)创建控制器

  创建com.uestc.controller.PersonController控制器

@Controller

public class PersonController {

@Autowired

private PersonService personService;

@RequestMapping("/show")

@ResponseBody

public List show(){

return personService.show();

}

}

(4)创建启动器

  创建com.uestc.ConsumerApplication

@SpringBootApplication

public class ConsumerApplication {

public static void main(String[] args) {

SpringApplication.run(ConsumerApplication.class,args);

}

}

6.5 测试

  在浏览器输入:http://localhost:8080/show。

  观察是否返回相应的json对象List

  注意provider如果第二次启动可能会出错,显示KeeperErrorCode = NodeExists for /rpc/provider,这个时候需要在zookeeper将这个/rpc/provider路径删除了。

文章来源

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: