北京尚学堂2020java_RPC
第一节 项目架构变化
Dubbo是RPC的一个框架。
单体结构流行到15年左右,逐渐向分布式过渡。
1.2 分布式架构
高内聚:只在项目内部互相调用
分布式架构各个模块如何进行通信?
可以使用http协议,也可以使用RPC协议,本阶段使用RPC协议,因为他比HTTP更适合项目内部通信。
使用项目:大型互联网项目、客户多、数据多、访问并发高、压力大、吞吐量大。
第二节 RPC简介
RFC(Request for Comments)是由互联网工程任务组(IETF)发布的文件集。RPC在rfc1831中收录。
RPC协议规定允许互联网中一台主机程序调用另一台主机程序,程序员无需对这个交互过程进行编程。当A调用B中功能或方法时,A是不知道B中方法具体实现的。
RPC是上层协议,底层可以基于TCP协议,也可以基于HTTP协议。一般说RPC都是基于RPC的具体实现,如Dubbo框架。
PRC
RPC在rfc1831中收录,remote procedure call 远程过程调用协议
当A程序调用B程序中功能或方法时,A是不知道B中方法具体实现的。RPC是上层协议,底层可以基于TCP协议,也可以基于HTTP协议,一般我们说都是基于RPC的具体实现,如Dubbo框架。
第三节 RPC和HTTP对比
3.1 具体实现
RPC:可以基于TCP协议,也可以基于HTTP协议
HTTP:基于HTTP协议
3.2 效率
RPC:自定义具体实现可以减少很多无用的报文内容,使得报文体积更小
HTTP:如果是HTTP1.1,报文中很多内容都是无用得。如果是HTTP2.0以后和RPC相差不大,比RPC少得可能就是一些服务治理的功能。
3.3 链接方式
RPC:长连接支持
HTTP:每次链接都是3次握手(断开连接为四次)
3.4 性能
RPC可以基于很多序列化方式,如thrift
HTTP主要是通过JSON,序列化和反序列化效率更低
3.5 注册中心
RPC:一般RPC框架都带有注册中心
HTTP:都是直连
3.6 负载均衡
RPC:绝大多数RPC框架都带有负载均衡测量
HTTP:一般都需要借助第三方工具,如nginx
3.7 综合结论
RPC框架一般都带有丰富的服务治理的功能,更适合企业内部接口调用。而HTTP更适合多平台之间相互调用。
第四节 HttpClient实现RPC
4.1 HttpClient简介
HttpClient可以实现使用java代码完成标准HTTP请求和响应。
当前工程:使用httpclient实现rpc远程过程调用
client:定义客户端代码。模拟一个网站平台的登陆页面,其中可以实现三方登录
server: 定义服务器代码
commons: 定义公用代码,如:实体类型
4.2 代码实现
新建一个maven project,然后在pom.xml中加入:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
再分别新建三个modules:
首先在rpc_httpclient/client/src/main/resources/templates下创建login.html:
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body style="text-align: center">
<a href="/localLogin"><h3>使用用户名本地登录</h3></a>
<hr>
<a href="/wechatLogin"><h3>使用微信三方登录</h3></a>
</body>
</html>
然后写代码,允许两种方式登录:
package com.bjsxt.controller;
import com.bjsxt.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@Autowired
private LoginService loginService;
/**
* 远程登录,实现微信三方登录
* @param model
* @return
*/
@GetMapping("/wechatLogin")
public String wechatLogin(Model model) {
model.addAttribute("flag", "微信");
loginService.wechatLogin();
return "success";
}
/**
* 本地登录方法
* @param model
* @return
*/
@GetMapping("/localLogin")
public String localLogin(Model model) {
model.addAttribute("flag", "本地");
loginService.localLogin();
return "success";
}
/**
* 登录页面跳转方法
* @return
*/
@GetMapping(value={"/login", "/"})
public String toLogin() {
return "login";
}
}
4.2.2 server类的实现
rpc_httpclient/server/src/main/java/com/bjsxt/controller/WechatLoginController.java
/**
* 模拟提供微信登录的控制器
*/
@Controller
public class WechatLoginController {
@Autowired
private WechatLoginService wechatLoginService;
/**
* 给远程访问提供一个登录方法
* 可以由任意的客户端访问本方法
* 本方法会请求参数,查询数据库,提供用户是否登录成功
* @return {'code': 200, 'msg':'ok'} -成功; {‘code':500, 'msg':'error'}
*/
@RequestMapping("/remoteCall4Login")
@ResponseBody
public Object remoteCall4Login(String username, String password) {
Map<String, Object> result = new HashMap<String, Object>();
try {
wechatLoginService.remoteCall4Login(username, password);
result.put("code", 200);
result.put("msg", "ok");
}catch (Exception e) {
e.printStackTrace();
result.put("code", 500);
result.put("msg", "error");
}
return result;
}
}
其底层的service类也非常简单:
@Service
public class WechatLoginServiceImpl implements WechatLoginService {
/**
* 登录方法
* @param username 用户名 模拟登录 用户名固定user
* @param password 密码 模拟登录 密码固定123
* @throws Exception
*/
public void remoteCall4Login(String username, String password) throws Exception {
if("guest".equals(username) && "123".equals(password)) {
// 登录成功
System.out.println("登录成功");
return;
}
System.out.println("登录失败");
throw new Exception("登录失败");
}
}
4.2.3 client和server的通信
总共由三种方式:1.GET 2.POST 3.json
在client项目中的controller直接写,使用GET方法
@Service
public class LoginServiceImpl implements LoginService {
/**
* 远程登录,微信登录
* 用java代码,模拟一个浏览器,发送请求到远程的服务器server,地址是
* http://ip:port/remoteCall4Login?username=xxx&password=xxx
*/
public void wechatLogin() {
// 使用GET请求,访问远程
doLoginWithGet("guest", "123");
// 使用POST请求,访问远程
}
protected Object doLoginWithGet(String username, String password) {
// 创建默认HttpClient客户端 相当于打开浏览器
CloseableHttpClient httpClient = HttpClients.createDefault();
// 维护一个请求地址,相当于在浏览器中输入路径
try {
URIBuilder builder = new URIBuilder("http://localhost:8091/remoteCall4Login");
// 请求参数
builder.addParameter("username", username);
builder.addParameter("password", password);
// 讲请求地址封装为具体的请求方式
HttpGet get = new HttpGet(builder.build());
// 发送请求,并等待服务器返回响应
CloseableHttpResponse response = httpClient.execute(get);
// 处理响应体内容
// 获取响应对象
HttpEntity httpEntity = response.getEntity();
// 转换响应体为需要的字符串
String resultStr = EntityUtils.toString(httpEntity, "UTF-8");
System.out.println(resultStr);
return resultStr;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
使用POST方法:
@Service
public class LoginServiceImpl implements LoginService {
/**
* 远程登录,微信登录
* 用java代码,模拟一个浏览器,发送请求到远程的服务器server,地址是
* http://ip:port/remoteCall4Login?username=xxx&password=xxx
*/
public void wechatLogin() {
// 使用POST请求,访问远程
doLoginWithPost("guest", "123");
}
protected Object doLoginWithPost(String username, String password) {
try {
// 创建客户端对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建一个POST请求对象
HttpPost post = new HttpPost("http://localhost:8091/remoteCall4Login");
// 提供请求参数,请求传递参数
// 一个基础的名值对请求参数 也就是name=value
BasicNameValuePair usernameParam = new BasicNameValuePair("username", username);
BasicNameValuePair passwordParam = new BasicNameValuePair("password", password);
// 创建一个请求体 做过编码处理的表单实体
StringEntity entity = new UrlEncodedFormEntity(Arrays.asList(usernameParam, passwordParam), "UTF-8");
// 把请求体保存到请求方式post对象中
post.setEntity(entity);
// 请求,等待响应
CloseableHttpResponse httpResponse = httpClient.execute(post);
// 处理响应
/**
* HttpClinet中对协议内容处理的方式
* 一次性处理 如:处理请求参数的时候,无论触发哪一个请求参数获取方法,都是一次性把所有的请求参数处理完
* request.getParameter() getParameterValues() getParameterNames()
* 如:处理响应内容的时候,无论出发什么逻辑,都是一次性把所有响应体内容都处理完
* response.getEntity() 会把响应内容加载到内存,关闭响应相关的所有IO流
*/
String resultString = EntityUtils.toString(httpResponse.getEntity(), "UTF-8");
System.out.println(resultString);
// 处理响应
return resultString;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
使用json方法,首先需要的是一个User类,在commons modules中新建一个User类:
public class User implements Serializable {
private String username;
private String password;
}
然后把commons加入client和server的pom.xml中:
<dependency>
<groupId>com.bjsxt</groupId>
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然后重写这个方法:rpc_httpclient/client/src/main/java/com/bjsxt/service/impl/LoginServiceImpl.java
public void wechatLogin() {
// 使用GET请求,访问远程
//doLoginWithGet("guest", "123");
// 使用POST请求,访问远程
// doLoginWithPost("guest", "123");
doLoginWithJson("guest", "123");
}
/**
* 请求参数为JSON,要求必须使用POST请求,且参数在请求体中传递
* 服务端处理请求体中的JSON参数的时候,必须使用被@RequestBody注解描述的参数才可以
*/
protected Object doLoginWithJson(String username, String password) {
// 如: User.username
User user = new User();
user.setPassword(username);
user.setUsername(password);
// java对象->json字符串
// 使用jackson实现java对象和JSON相互转化
ObjectMapper objectMapper = new ObjectMapper();
String jsonUser = "";
try {
jsonUser = objectMapper.writeValueAsString(user);
} catch (JsonProcessingException e) {
e.printStackTrace();
return "java对象转换为JSON字符串时错误";
}
// 创建POST请求对象
HttpPost post = new HttpPost("http://localhost:8091/remoteCall4JSON");
// 创建请求体,携带要提供的JSON参数
StringEntity entity = new StringEntity(jsonUser, ContentType.APPLICATION_JSON);
post.setEntity(entity);
// 发起请求,等待响应
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
CloseableHttpResponse httpResponse = httpClient.execute(post);
// 处理结果
String result = EntityUtils.toString(httpResponse.getEntity(), "UTF-8");
System.out.println(result);
return result;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
处理该请求的函数也要重写,用@RequestBody来修饰请求的参数,rpc_httpclient/server/src/main/java/com/bjsxt/controller/WechatLoginController.java:
/**
* 处理请求参数为JSON的服务方法
* RequestBody注解——描述当前方法参数处理的是请求体中的对象(JSON数据)
* @return
*/
@RequestMapping("/remoteCall4JSON")
@ResponseBody
public Object remoteCall4Json(@RequestBody User user){
Map<String, Object> result = new HashMap<String, Object>();
try {
wechatLoginService.remoteCall4Login(user.getUsername(), user.getPassword());
result.put("code", 200);
result.put("msg", "ok");
}catch (Exception e) {
e.printStackTrace();
result.put("code", 500);
result.put("msg", "error");
}
return result;
}
需要将资源进行回收,创建顺序是httpClient->httpResponse,那么销毁顺序就是httpResponse->httpClient:
// 发起请求,等待响应
CloseableHttpClient httpClient = null;
CloseableHttpResponse httpResponse = null;
try {
httpClient = HttpClients.createDefault();
httpResponse = httpClient.execute(post);
// 处理结果
String result = EntityUtils.toString(httpResponse.getEntity(), "UTF-8");
System.out.println(result);
return result;
} catch (IOException e) {
e.printStackTrace();
} finally {
if(httpResponse != null) { // 回收响应对象
try {
httpResponse.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(httpClient != null) {
try {
httpClient.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.3 Jackson常用API学习
public static void main(String[] args) throws JsonProcessingException {
List<User> list = new ArrayList<User>();
User user = new User();
user.setUsername("admin");
user.setPassword("123");
// 保存到集合中
list.add(user);
// 创建第二个对象,guest
user = new User();
user.setUsername("guest");
user.setPassword("abc");
// 保存到集合中
list.add(user);
// 创建Jackson中的转换器
ObjectMapper objectMapper = new ObjectMapper();
// java对象 -> JSON字符串
String strObject = objectMapper.writeValueAsString(user);
System.out.println("user对象->json: " + strObject + "");
// java集合 -> JSON字符串
String listObject = objectMapper.writeValueAsString(list);
System.out.println("list集合 -> json: " + listObject + "");
// json字符串 -> java对象
User uObj = objectMapper.readValue(strObject, User.class);
System.out.println("java对象:" + uObj);
// json字符串 -> map集合 jackson集合中,可以把任意对象描述json字符转成map集合对象
Map mapObj = objectMapper.readValue(strObject, HashMap.class);
System.out.println("mapObj类型 : " + mapObj.getClass().getName());
System.out.println("mapObj内容 : " + mapObj);
// json字符串-> list集合
// 创建一个Jackson中用于描述集合和集合泛型的类型
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, User.class);
List<User> l = objectMapper.readValue(listObject, javaType);
System.out.println("集合长度为:" + l.size() + "。开始输出>>>>>>>>>>>>>>>");
for(User u : l) {
System.out.println(u);
}
System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<输出结束");
}
4.4 AJAX跨域请求——AJAX请求携带JSON参数
用户点击某个按钮,直接向server发起请求绕过了client,是不是也算是一次RPC?
ajax有一个策略叫同源策略——你看到的视图是哪个项目的,只能访问哪个服务,这些是通过域名来确定的。除此以外的都叫跨域。返回来的响应头需要加上一个"users-control-allow",
首先在页面上添加jquery(rpc_httpclient/client/src/main/resources/static/js/jquery.min.js)如果启动之后没有的话需要maven->lifecycle->clean
然后在rpc_httpclient/client/src/main/resources/templates/login.html上写一个可以发送ajax请求的按钮:
<head>
<meta charset="UTF-8">
<script src="/js/jquery.min.js"></script>
<script type="text/javascript">
// 跨域请求,访问server服务中的登录
function loginWithServer() {
var param = '{"username":"ajax", "password":"999"}';
$.ajax({
'url': 'http://localhost:8091/remoteCall4JSON',
'data': param,
'type': 'post',
'dataType': 'json',
'contentType': 'application/json',
'success': function (data) {
alert("code - " + data.code + " ;msg - " + data.msg);
}
});
}
</script>
<title>登录</title>
</head>
<body style="text-align: center">
<a href="/localLogin"><h3>使用用户名本地登录</h3></a>
<hr>
<a href="/wechatLogin"><h3>使用微信三方登录</h3></a>
<input type="button" value="AJAX跨域登录" onclick="loginWithServer();">
</body>
如果你的方法是要接收跨域请求的,那么需要加上注解,这里在server端的controller中加上@CrossOrigin
/**
* 处理请求参数为JSON的服务方法
* RequestBody注解——描述当前方法参数处理的是请求体中的对象(JSON数据)
* CrossOrigin注解——代表当前方法可以被AJAX跨域访问,会在响应头中增加access-control-allow-origin
* @return
*/
@RequestMapping("/remoteCall4JSON")
@ResponseBody
@CrossOrigin
public Object remoteCall4Json(@RequestBody User user){
System.out.println("username - " + user.getUsername() + " ; password - " + user .getPassword());
return result;
}
第五节 RMI实现RPC
5.1 RMI简介及其执行流程
RMI(remote method Invocation)远程方法调用,是从JDK1.2推出的功能,它可以实现在一个JAVA应用中可以向调用本地方法一样调用另一个服务器Java应用(JVM)中的内容,RMI是java语言的远程调用,无法实现跨语言。
server会将能提供的服务发布到RMI Registry中,server每创建一个对象时,它都会使用bind()或者rebind()方法注册改对象。
lookup():发布、订阅服务,client会从Registry中订阅和发布服务。
5.2 API介绍
5.2.1 Remote
java.rmi.Remote定义了此接口为远程调用接口。如果接口被外部调用,需要继承此接口。
无任何方法,仅代表实现了该接口的类可以被远程调用。
5.2.2 RemoteException
java.rmi.RemoteException
继承了Remote接口的接口中,如果方法是允许被远程调用的,需要抛出此异常
5.2.3 UnicastRemoteObject
做了远程调用的底层实现
java.rmi.server.UnicastRemoteObject
此类实现了Remote接口和Serialize接口,自定义接口实现类除了实现自定义接口还需要继承此类
5.2.4 LoateRegistry
java.rmi.registry.LocateRegistry
可以通过LocateRegistry在本机上创建Registry
5.2.5 Naming
命名规则
java.rmi.naming
Naming定义了发布内容可访问RMI名称,也是通过Naming获取到指定的远程方法
5.4 代码实现
4.1
首先创建三个modules,然后在commons modules中创建一个公共的接口:
/**
* 定义一个可以被远程调用的服务接口
* 这是一个标准,服务端实现这个接口,提供服务。客户端使用这个端口,消费服务。
*/
public interface MyService extends Remote {
/**
* 定义一个可以被远程调用的服务方法
* @param name
* @return
* @throws RuntimeException - 远程调用过程中可能发生的异常
*/
String sayHello(String name) throws RemoteException;
}
然后将其加入双方的pom.xml:
<dependencies>
<dependency>
<groupId>com.bjsxt</groupId>
<artifactId>rmi_commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
19_rpc_rmi/rmi_server/src/main/java/com/bjsxt/service/impl/MyServiceImpl.java中:
/**
* 可以被远程调用的服务类型,要求必须实现接口Remote
* 必须继承父类UnicastRemoteObject,定义了无参构造方法,且构造方法抛出异常:RemoteException
*/
public class MyServiceImpl extends UnicastRemoteObject implements MyService {
public MyServiceImpl() throws RemoteException {
super();
}
@Override
public String sayHello(String name) throws RemoteException {
return "你好:" + name;
}
}
然后在main方法中绑定服务:
/**
* 用于启动服务端程序的主类型
*/
public class MainClass {
public static void main(String[] args) {
// 启动当前应用,创建一个注册表Registry,并非发布服务MyServiceImpl
try {
MyService myService = new MyServiceImpl();
// 创建注册表Registry,创建的注册表需要绑定一个端口
// 这个端口可以让客户端访问的时候通过端口传递数据
// 也可以让客户端使用Nameing lookup的时候,通过端口找注册信息
// 创建了Registry后,jvm会开启一个子线程作为守护线程持续的维护注册表的有效性。
Registry registry = LocateRegistry.createRegistry(9999);
// 绑定服务 不推荐使用 使用服务名称绑定
//registry.rebind("rmi://localhost:9999/myService", myService);
// 使用naming机制实现服务绑定 使用协议+Registry所在主机的IP和端口+服务名称实现绑定
Naming.rebind("rmi://localhost:9999/myService", myService);
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
19_rpc_rmi/rmi_client/src/main/java/com/bjsxt/rmi/main/ClientMain.java:
/**
* 测试RMI客户端
*/
public class ClientMain {
public static void main(String[] args) {
// 找注册表发现要使用的服务mainService 创建代理对象,调用方法,得到远程结果
// 代理对象的获取
try {
MyService myService = (MyService) Naming.lookup("rmi://localhost:9999/myService");
// 调用代理中的方法,相当于调用了远程方法
System.out.println(myService.getClass().getName());
String result = myService.sayHello("王阳明传人张智昭");
System.out.println(result);
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
第六节 Zookeeper安装
6.1 Zookeeper简介
是属于apache的,常用它来做注册中心(依赖zookeeper的发布/订阅功能),配置文件中心、分布式锁配置、集群管理等。zookeeper一共就两版本,主要是使用java语言写的。
安装的方式很简单,解压一下源码并移动到某个文件即可/usr/local/zk,然后再/usr/local/zk/conf中cp一下zoo_sample.cfg为zoo.cfg作为配置文件。
bin/zkServer.sh start
bin/zkServer.sh status
bin/zkServer.sh stop
6.2 ZooKeeper常见命令
首先指定连接到ip:port ZooKeeper主机
/bin/zkCli.sh -server ip:port
随便输入命令会出现Help,可以看到zk命令还是很简单的。
6.2.1 quit和ls
ls:
-s 详细信息
-R 当前目录和子目录中内容都罗列出来
两个是互斥的,只能有一个能够使用
6.2.2 create和get
每一个文件夹不被称为directory,而是节点。可以通过creta创建节点,节点用来存储数据。
create /my_node2 "create node datas"
可以用get获取node中存储的数据
get /my_node2
cZxid=0xd 创建该节点事物的唯一标记id
cztime 创建该节点的时间
mZxid
mtime 最近一次改变的时间
pZxid 子节点的id,否则就是自己
cversion 子节点变成了多少次
dataVersion 数据的版本
aclVersion 访问权限的变化
Owner 这个节点是谁创建的,只不过记录的是节点的标记。如果是临时节点,那么就是会话的id
dataLength 数据的长度
numChildren 孩子的数量
6.2.3 set与delete
set:赋予某个节点以数据
set /my_node "if there is blank, please use double quotation marks"
delte:删除某个节点,只能删除叶子节点
deleteall:删除某个节点,如果该节点有叶子那么一起删除
第七节 向Zookeeper中注册内容
可以通过java代码来模拟bin/zkCli.sh,首先将zookeeper的依赖加入pom中
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.8.0</version>
</dependency>
然后写调用它的代码即可,不过一直有bug我没de出来呜呜呜呜18_RPC/rpc_zookeeper/src/test/java/com/bjsxt/test/AccessZk.java:
public class AccessZk {
private static String zkAddress = "192.168.124.128:2181";
public static void main(String[] args) {
// 创建临时节点,命名为/tmp,无数据
//createTmpNode("tmp", new byte[]{});
// 创建一个命名为/java的数据,无内容
//createNode("path", new byte[]{});
// 创建一个命名为/path/node的节点,数据内容是:rmi://192.168.1.4:9999/service
//String content = "rmi://192.168.1.4:9999/service";
//createNode("path/node", content.getBytes());
// 查询节点/my_node的数据内容
byte[] bytes = getNode("zookeeper");
System.out.println(new String(bytes));
}
// 创建节点
public static void createNode(String nodeName, byte[] bytes){
ZooKeeper zooKeeper = null;
try {
// 创建一个zk客户端连接对象
zooKeeper = new ZooKeeper(zkAddress, 10000, new Watcher() {
// 当连接对象创建成功的时候,做什么事情。 回调。
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("begin");
}
});
// 创建节点
String result = zooKeeper.create("/"+nodeName, bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
// 返回结果
System.out.println("创建节点的结果:" + result);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}finally {
if(zooKeeper != null){
try {
zooKeeper.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 删除节点 删除指定的节点及其所有的子节点
public static void deleteAll(String nodeName) {
ZooKeeper zooKeeper = null;
try {
zooKeeper = new ZooKeeper(zkAddress, 10000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
// 检查当前节点有没有子节点,有子节点则递归删除
List<String> childrenNames = zooKeeper.getChildren("/" + nodeName, false);
for(String childName: childrenNames) {
// 如果有子节点,先删除子节点 只有直接子节点的名称
deleteAll(nodeName + "/"+ childName);
}
// 查询要删除的节点版本
Stat stat = new Stat();
zooKeeper.getData("/" + nodeName, false, stat);
int version = stat.getCversion();
System.out.println("[" + nodeName + "]节点版本是:" + version);
// 删除节点
zooKeeper.delete("/" + nodeName, stat.getVersion());
System.out.println("删除节点完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
if(zooKeeper != null) {
try {
zooKeeper.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 创建临时节点
public static void createTmpNode(String nodeName, byte[] bytes) {
ZooKeeper zooKeeper = null;
try {
zooKeeper = new ZooKeeper(zkAddress, 50000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
// 创建一个临时的节点, 给节点定义一个随机的编号,编号自增
/**
*
* PERSISTENT - 持久保存,永久保存
* PERSISTENT_SEQUENTIAL - 永久保存,节点命名zk会自动追加一个编号
* EPHEMERAL - 临时保存,会话链接断开,自动删除
* EPHEMERAL_SEQUENTIAL- 临时保存,会话链接断开,自动删除,节点命名zk会自动追加一个编号
*/
zooKeeper.create("/"+nodeName, bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL); // 可自增
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(zooKeeper != null) {
try {
zooKeeper.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 查询节点并获得节点的数据内容
public static byte[] getNode(String nodeName){
ZooKeeper zooKeeper = null;
byte[] bytes = null;
try{
zooKeeper = new ZooKeeper(zkAddress, 10000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("地址为:" + zkAddress);
}
});
// 查询节点数据内容
bytes = zooKeeper.getData("/"+nodeName, false, null);
}catch (Exception e){
e.printStackTrace();
}finally {
if(zooKeeper != null){
try {
zooKeeper.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return bytes;
}
}
第八节 手写RPC框架
使用Zookeeper作为注册中心,RMI作为连接技术,手写RPC技术
创建三个Modules
8.1 公共端的实现
然后在18_RPC/rpc_zookeeper/rpc_zk_service_interface/src/main/java/com/bjsxt/service/rmi/FirstService.java创建一个公共的接口:
/**
* 可以被远程调用的服务接口
*/
public interface FirstService extends Remote {
// 可以被远程调用的服务方法
String sayHello(String name) throws RemoteException;
}
8.2 服务端的实现
首先是最简单的实现了这个接口18_RPC/rpc_zookeeper/rpc_zk_server/src/main/java/com/bjsxt/serivce/impl/FirstServiceImpl.java:
public class FirstServiceImpl extends UnicastRemoteObject implements FirstService {
public FirstServiceImpl() throws RemoteException {
super();
}
public String sayHello(String name) throws RemoteException {
return "你好," + name;
}
}
然后是用rmi运行并注册服务18_RPC/rpc_zookeeper/rpc_zk_server/src/main/java/com/bjsxt/server/ServerStarter.java:
/**
* 服务启动程序
* 当前的代码启动执行后,会创建一个Registry
* 在Registry中注册发布服务FirstService
* 链接zookeeper,把注册服务的rmi地址,保存在zookeeper中
* 保存rmi地址zk节点是/rmi/包名.类型/provider
*/
public class ServerStarter {
public static void main(String[] args) {
try {
// 创建Registry
LocateRegistry.createRegistry(9999);
// 创建服务对象
FirstService service = new FirstServiceImpl();
// 发布服务
String rmiUri = "rmi://10.181.18.140:9999/service";
Naming.rebind(rmiUri, service);
// 访问zk,保存数据 rmiUrl,所在节点位置/rmi/类名包名.类名/provider
String nodeName = "/rmi/" + FirstService.class.getName() + "/provider";
createNode(nodeName, rmiUri );
System.out.println("服务已启动");
} catch (Exception e) {
e.printStackTrace();
}
}
protected static void createNode(String nodeNames, String rmiUri) {
try {
ZooKeeper zooKeeper = new ZooKeeper("192.168.2.128:2181", 10000, new Watcher() {
public void process(WatchedEvent watchedEvent) {
}
});
String[] names = nodeNames.split("/");
System.out.println(Arrays.toString(names));
String path = "";
for(int i = 0; i < names.length-1; i++) {
path = path + "/" + names[i];
Stat stat = zooKeeper.exists("/" + names[i], false);
if(null == stat) {
// 节点不存在,创建
zooKeeper.create(path, new byte[]{}, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
// 保存rmiUri在zk中
zooKeeper.create("/" + nodeNames, rmiUri.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
8.3 client
首先还是要引入interface模块
<dependencies>
<dependency>
<groupId>com.bjsxt</groupId>
<artifactId>rpc_zk_service</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
不过client也需要一个接口18_RPC/rpc_zookeeper/rpc_zk_client/src/main/java/com/bjsxt/service/client/MyService.java:
/**
* 客户端的本地接口
*/
public interface MyService {
String sayHello(String name);
}
及其实现类:
public class MyServiceImpl implements MyService {
private FirstService firstService;
/**
* 当前的方法,需要调用远程的服务提供的sayhello方法
* @param name
* @return
*/
public String sayHello(String name) {
try {
return firstService.sayHello(name);
} catch (RemoteException e) {
e.printStackTrace();
return "本地 - 你好" + name;
}
}
public FirstService getFirstService() {
return firstService;
}
public void setFirstService(FirstService firstService) {
this.firstService = firstService;
}
}
然后要在新开的客户端类里面,从zk中拿到远程代理对象的uri,然后再来调用它从而产生结果
/**
* 客户端启动程序
* 访问远程zk,查询rmi地址
* 根据rmi地址,创建服务接口的代理对象
* 创建本地服务对象,并把远程的服务代理对象注入到本地服务对象的属性中
* 执行本地服务方法
*/
public class ClientStarter {
public static void main(String[] args) {
// 访问zk,获取rmi。创建接口代理对象
String interfaceName = FirstService.class.getName();
String zkPath = "/rmi/" + interfaceName + "/provider";
ZooKeeper zooKeeper = null;
try {
zooKeeper = new ZooKeeper("192.168.2.168:2181", 10000, new Watcher() {
public void process(WatchedEvent watchedEvent) {
}
});
// 查询rmi地址
byte[] bytes = zooKeeper.getData(zkPath, false, null);
// rmi地址信息
String rmiUrl = new String(bytes);
// 创建代理对象,访问远程Registry,创建本地一个代理对象
FirstService firstService = (FirstService) Naming.lookup(rmiUrl);
// 创建本地的代理对象
MyService myService = new MyServiceImpl();
// 注入属性对象
((MyServiceImpl) myService).setFirstService(firstService);
// 调用方法
String str = myService.sayHello("北京尚学堂");
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
2019-03-22 playfair加密过程(密码学_古典密码学_多图加密算法)