资料来源:https://www.bilibili.com/video/BV11i4y1N7LQ?p=8

主要内容

  1. 项目结构变化
    2. RPC简介
    3. RMI实现RPC
    4. HttpClient实现RPC
    5. Zookeeper安装
    6. Zookeeper客户端常用命令
    7. 向Zookeeper中注册内容
    8. 从Zookeeper中发现内容
    9. 手写RPC框架

    一、项目架构变化

    1、单体架构

    1.1 架构图

    单体架构就是一个项目里面包含这个项目中全部代码。一个应用搞定全部功能。
    DNS 服务器可以是单映射,也可以配置多个映射。
    image.png

    1.2 软件代码结构

    在单体架构项目中,团队都是通过包(package)进行区分每个模块。
    总体包结构:com.bjsxt.*.分层包。

    1. 项目名:
    2. -- com
    3. --bjsxt
    4. -- common
    5. -- utils
    6. --user
    7. -- controller
    8. -- service
    9. -- mapper
    10. -- sys
    11. -- controller
    12. -- service
    13. -- mapper

    1.3 优缺点

    1、优点:部署简单、维护方便、开发成本低
    2、缺点:当项目规模大、用户访问频率高、并发量大、数据量大时,会大大降低程序执行效率,甚至出现服务器宕机等情况。

    1.4 适用项目

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

    2、分布式架构

    2.1 架构图(简易版)

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

    2.2 软件代码结构

    1. 项目1
    2. --com.bjsxt.xxx
    3. -- controller
    4. -- service
    5. -- mapper
    6. 项目2
    7. --com.bjsxt.mmm
    8. -- controller
    9. -- service
    10. -- mapper

    2.3 优缺点

    1、优点

    • 增大了系统可用性。减少单点故障,导致整个应用不可用。
    • 增加重用性。因为模块化,所以重用性更高。高内聚、低耦合。
    • 增加可扩展性。有新的模块增加新的项目即可。
    • 增加每个模块的负载能力。因为每个模块都是一个项目,所以每个模块的负载能力更强。

2、缺点

  • 成本更高。因为技术多、难,所以开发成本、时间成本、维护成本都在变高。
  • 架构更加复杂。
  • 整体响应之间变长,一些业务需要多项目通信后给出结果。
  • 吞吐量更大。吞吐量= 请求数/秒。

    2.4 适用项目

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

    2.5 待解决问题

    分布式架构中各个模块如何进行通信?
    可以使用Http协议,也可以使用RPC协议通信,也可以使用其他的通信方式。我们本阶段使用的是RPC协议,因为它比HTTP更适合项目内部通信。

    二、RPC简介

    1、RFC

    RFC(Request For Comments) 是由互联网工程任务组(IETF)发布的文件集。文件集中每个文件都有自己唯一编号,例如:rfc1831。目前RFC文件由互联网协会(Internet Society,ISOC)赞助发行。
    RPC就收集到了rfc 1831中。可以通过下面网址查看:https://datatracker.ietf.org/doc/rfc1831/

    2、RPC

    RPC在rfc 1831中收录 ,RPC(Remote Procedure Call) 远程过程调用协议
    image.png
    RPC协议规定允许互联网中一台主机程序调用另一台主机程序,而程序员无需对这个交互过程进行编程。在RPC协议中强调当A程序调用B程序中功能或方法时,A是不知道B中方法具体实现的
    RPC是上层协议,底层可以基于TCP协议,也可以基于HTTP协议。一般我们说RPC都是基于RPC的具体实现,如:Dubbo框架。从广义上讲只要是满足网络中进行通讯调用都统称为RPC,甚至HTTP协议都可以说是RPC的具体实现,但是具体分析看来RPC协议要比HTTP协议更加高效,基于RPC的框架功能更多。
    RPC协议是基于分布式架构而出现的,所以RPC在分布式项目中有着得天独厚的优势。

    3、RPC和HTTP对比

    3.1 具体实现

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

    3.2 效率

    RPC:自定义具体实现可以减少很多无用的报文内容,使得报文体积更小。
    HTTP:如果是HTTP 1.1 报文中很多内容都是无用的。如果是HTTP2.0以后和RPC相差不大,比RPC少的可能就是一些服务治理等功能。

    3.3 连接方式

    RPC:支持长连接。
    HTTP:每次连接都是3次握手。(断开链接为4次挥手)

    3.4 性能

    RPC可以基于很多序列化方式。如:thrift
    HTTP 主要是通过JSON,序列化和反序列效率更低。

    3.5 注册中心

    RPC :一般RPC框架都带有注册中心。
    HTTP:都是直连。

    3.6 负载均衡

    RPC:绝大多数RPC框架都带有负载均衡测量。
    HTTP:一般都需要借助第三方工具。如:nginx

    3.7 综合结论

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

    三、HttpClient实现RPC

    1、HttpClient简介

    在JDK中java.net包下提供了用户HTTP访问的基本功能,但是它缺少灵活性或许多应用所需要的功能。
    HttpClient起初是Apache Jakarta Common 的子项目。用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本。2007年成为顶级项目。
    通俗解释:HttpClient可以实现使用Java代码完成标准HTTP请求及响应。

    2、代码实现

    2.1 服务端

    image.png

    1、新建控制器

    ```java package com.bjsxt.httpclientserver.controller;

@Controller public class TestController { /**

  1. * CrossOrigin - 跨域请求注解。在响应头上增加跨域处理允许。 可以让ajax跨域请求当前的服务方法。
  2. * 如果用于注解类型(必须是控制器),代表当前控制器中所有的方法,都允许跨域访问。
  3. * @param users
  4. * @return
  5. */
  6. // 使用请求体传递请求参数
  7. @RequestMapping(value="/bodyParams", produces = {"application/json;charset=UTF-8"})
  8. @ResponseBody
  9. @CrossOrigin
  10. public String bodyParams(@RequestBody List<User> users){
  11. System.out.println(users);
  12. return users.toString();
  13. }
  14. // GET有参请求访问
  15. @RequestMapping(value="/params", produces = {"application/json;charset=UTF-8"})
  16. @ResponseBody
  17. public String params(String name, String password){
  18. System.out.println("name - " + name + " ; password - " + password);
  19. return "{\"msg\":\"登录成功\", \"user\":{\"name\":\"" + name + "\",\"password\":\"" + password + "\"}}";
  20. }
  21. // GET无参请求访问
  22. @RequestMapping(value="/test", produces = {"application/json;charset=UTF-8"})
  23. @ResponseBody
  24. public String test(){
  25. return "{\"msg\":\"处理返回\"}";
  26. }

}

  1. <a name="LZQiq"></a>
  2. #### 2、新建启动器
  3. ```java
  4. package com.bjsxt.httpclientserver;
  5. @SpringBootApplication
  6. public class ServerApp {
  7. public static void main(String[] args) {
  8. SpringApplication.run(ServerApp.class, args);
  9. }
  10. }

2.2 客户端

image.png

1、添加依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.apache.httpcomponents</groupId>
  4. <artifactId>httpclient</artifactId>
  5. <version>4.5.12</version>
  6. </dependency>
  7. </dependencies>

2、使用GET无参请求访问

  1. package com.bjsxt.httpclient;
  2. /**
  3. * 使用Main方法,测试HttpClient技术。
  4. */
  5. public class TestHttpClient {
  6. public static void main(String[] args) throws Exception {
  7. testGetNoParams();
  8. }
  9. /**
  10. * 无参数GET请求
  11. * 使用浏览器,访问网站的过程是:
  12. * 1、 打开浏览器
  13. * 2、 输入地址
  14. * 3、 访问
  15. * 4、 看结果
  16. * 使用HttpClient,访问WEB服务的过程:
  17. * 1、 创建客户端,相当于打开浏览器
  18. * 2、 创建请求地址, 相当于输入地址
  19. * 3、 发起请求, 相当于访问网站(回车键)
  20. * 4、 处理响应结果, 相当于浏览器显示结果
  21. */
  22. public static void testGetNoParams() throws IOException {
  23. // 创建客户端对象
  24. HttpClient client = HttpClients.createDefault();
  25. // 创建请求地址
  26. HttpGet get = new HttpGet("http://localhost:80/test");
  27. // 发起请求,接收响应对象
  28. HttpResponse response = client.execute(get);
  29. // 获取响应体。 响应数据是一个基于HTTP协议标准字符串封装的对象。
  30. // 所以,响应体和响应头,都是封装的HTTP协议数据。直接使用可能有乱码或解析错误
  31. HttpEntity entity = response.getEntity();
  32. // 通过HTTP实体工具类,转换响应体数据。 使用的字符集是UTF-8
  33. String responseString = EntityUtils.toString(entity, "UTF-8");
  34. System.out.println("服务器响应数据是 - [ " + responseString + " ]");
  35. // 回收资源
  36. client = null;
  37. }
  38. }

image.png

2、使用GET有参请求访问

  1. package com.bjsxt.httpclient;
  2. /**
  3. * 使用Main方法,测试HttpClient技术。
  4. */
  5. public class TestHttpClient {
  6. public static void main(String[] args) throws Exception {
  7. testGetParams();
  8. }
  9. /**
  10. * 有参数GET请求
  11. * @throws IOException
  12. */
  13. public static void testGetParams() throws IOException, URISyntaxException {
  14. HttpClient client = HttpClients.createDefault();
  15. // 基于Builder构建请求地址
  16. URIBuilder builder = new URIBuilder("http://localhost/params");
  17. // 基于单参数传递,构建请求地址
  18. // builder.addParameter("name", "bjsxt");
  19. // builder.addParameter("password", "admin123");
  20. // URI uri = builder.build();
  21. // 基于多参数传递,构建请求地址
  22. List<NameValuePair> nvps = new ArrayList<>();
  23. nvps.add(new BasicNameValuePair("name","bjsxt"));
  24. nvps.add(new BasicNameValuePair("password", "admin123"));
  25. builder.addParameters(nvps);
  26. URI uri = builder.build();
  27. System.out.println(uri.toASCIIString());
  28. HttpGet httpGet = new HttpGet(uri);
  29. HttpResponse response = client.execute(httpGet);
  30. HttpEntity entity = response.getEntity();
  31. String result = EntityUtils.toString(entity);
  32. System.out.println(result);
  33. }
  34. }

image.png

3、使用POST请求访问

  1. package com.bjsxt.httpclient;
  2. /**
  3. * 补充:
  4. * property - 类型中getter和setter的命名。去除set|get,剩余部分首字母转小写。
  5. * <bean><property name="abc"></property></bean>
  6. * field - 类型中定义的实例变量。
  7. *
  8. * ajax -> 请求, 参数的json格式的字符串对象。
  9. *
  10. * 使用Main方法,测试HttpClient技术。
  11. */
  12. public class TestHttpClient {
  13. public static void main(String[] args) throws Exception {
  14. testPost();
  15. }
  16. /**
  17. * POST请求
  18. */
  19. public static void testPost() throws Exception{
  20. HttpClient client = HttpClients.createDefault();
  21. //1、无参数Post请求
  22. HttpPost post = new HttpPost("http://localhost/test");
  23. HttpResponse response = client.execute(post);
  24. System.out.println(EntityUtils.toString(response.getEntity(), "UTF-8"));
  25. //2、有参数的Post请求
  26. //2.1 请求头传递参数。和Get请求携带参数的方式一致。
  27. URIBuilder builder = new URIBuilder("http://localhost/params");
  28. builder.addParameter("name", "post");
  29. builder.addParameter("password", "postPassword");
  30. URI uri = builder.build();
  31. HttpPost httpPost = new HttpPost(uri);
  32. HttpResponse postResponse = client.execute(httpPost);
  33. System.out.println(EntityUtils.toString(postResponse.getEntity(), "UTF-8"));
  34. //2.2 请求体传递参数
  35. HttpPost bodyParamsPost = new HttpPost("http://localhost/bodyParams");
  36. // 定义请求协议体,设置请求参数。 使用请求体传递参数的时候,需要定义请求体格式。默认是表单格式。
  37. // 使用URIBuilder构建的URI对象,就是请求体传递参数的。
  38. // 方式一:使用这种方式传递参数不符合要求,输出结果为null
  39. // HttpEntity entity = new StringEntity("name=abc&password=123");
  40. // bodyParamsPost.setEntity(entity);
  41. // String responseString = EntityUtils.toString(client.execute(bodyParamsPost).getEntity(), "UTF-8");
  42. User u1 = new User();
  43. u1.setName("name1");
  44. u1.setPassword("password1");
  45. User u2 = new User();
  46. u2.setName("name2");
  47. u2.setPassword("password2");
  48. // 方式二:拼接一个JSON格式字符串,表示请求参数, 一个List<User>,请求头设置为application/json
  49. // String paramsString = "[" + u1.toString() + ", " + u2.toString() + "]";
  50. // HttpEntity entity = new StringEntity(paramsString, "application/json", "UTF-8");
  51. // bodyParamsPost.setEntity(entity);
  52. // String responseString = EntityUtils.toString(client.execute(bodyParamsPost).getEntity(), "UTF-8");
  53. // 方式三:
  54. List<User> users = new ArrayList<>();
  55. users.add(u1);
  56. users.add(u2);
  57. // 把集合users -> JSON字符串
  58. // 创建Jackson中的转换器对象
  59. ObjectMapper objectMapper = new ObjectMapper();
  60. // java对象转换成JSON格式字符串
  61. String paramsString = objectMapper.writeValueAsString(users);
  62. HttpEntity entity = new StringEntity(paramsString, "application/json", "UTF-8");
  63. bodyParamsPost.setEntity(entity);
  64. String responseString = EntityUtils.toString(client.execute(bodyParamsPost).getEntity(), "UTF-8");
  65. String userString = responseString.substring(1, responseString.indexOf("},")+1);
  66. User responseUser = objectMapper.readValue(userString, User.class);
  67. System.out.println(responseUser);
  68. // 构建一个Jackson识别的Java类型映射。
  69. JavaType valueType = objectMapper.getTypeFactory().constructParametricType(List.class, User.class);
  70. List<User> responseList = objectMapper.readValue(responseString, valueType);
  71. System.out.println(responseList);
  72. }
  73. }

image.png

3、HttpClient请求包含JSON

  1. package com.bjsxt.httpclient;
  2. /**
  3. * 补充:
  4. * property - 类型中getter和setter的命名。去除set|get,剩余部分首字母转小写。
  5. * <bean><property name="abc"></property></bean>
  6. * field - 类型中定义的实例变量。
  7. *
  8. * ajax -> 请求, 参数的json格式的字符串对象。
  9. *
  10. * 使用Main方法,测试HttpClient技术。
  11. */
  12. public class TestHttpClient {
  13. public static void main(String[] args) throws Exception {
  14. testPost();
  15. }
  16. /**
  17. * POST请求
  18. */
  19. public static void testPost() throws Exception{
  20. HttpClient client = HttpClients.createDefault();
  21. HttpPost bodyParamsPost = new HttpPost("http://localhost/bodyParams");
  22. // 定义请求协议体,设置请求参数。 使用请求体传递参数的时候,需要定义请求体格式。默认是表单格式。
  23. // 使用URIBuilder构建的URI对象,就是请求体传递参数的。
  24. User u1 = new User();
  25. u1.setName("name1");
  26. u1.setPassword("password1");
  27. User u2 = new User();
  28. u2.setName("name2");
  29. u2.setPassword("password2");
  30. List<User> users = new ArrayList<>();
  31. users.add(u1);
  32. users.add(u2);
  33. // 把集合users -> JSON字符串
  34. // 创建Jackson中的转换器对象
  35. ObjectMapper objectMapper = new ObjectMapper();
  36. // java对象转换成JSON格式字符串
  37. String paramsString = objectMapper.writeValueAsString(users);
  38. System.out.println(paramsString);
  39. HttpEntity entity = new StringEntity(paramsString, "application/json", "UTF-8");
  40. bodyParamsPost.setEntity(entity);
  41. String responseString = EntityUtils.toString(client.execute(bodyParamsPost).getEntity(), "UTF-8");
  42. // 将单一对象转换为字符串
  43. String userString = responseString.substring(1, responseString.indexOf("},") + 1);
  44. User responseUser = objectMapper.readValue(userString, User.class);
  45. System.out.println(responseUser);
  46. // 构建一个Jackson识别的Java类型映射。
  47. JavaType valueType = objectMapper.getTypeFactory().constructParametricType(List.class, User.class);
  48. List<User> responseList = objectMapper.readValue(responseString, valueType);
  49. System.out.println(responseList);
  50. }
  51. }
  1. public class HttpClientDemo {
  2. public static void main(String[] args) {
  3. try {
  4. CloseableHttpClient httpClient = HttpClients.createDefault();
  5. HttpPost post = new HttpPost("http://localhost:8080/demo");
  6. HttpEntity httpEntity= null;
  7. String json = "{}";
  8. StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
  9. post.setEntity(entity);
  10. CloseableHttpResponse response = httpClient.execute(post);
  11. String result = EntityUtils.toString(response.getEntity());
  12. System.out.println(result);
  13. response.close();
  14. httpClient.close();
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }

4、服务端控制器接口参数

@RequestBody把请求体中流数据转换为指定的对象。多用在请求参数是json数据且请求的Content-Type=”application/json”

  1. @RequestMapping("/demo4")
  2. @ResponseBody
  3. public String demo4(@RequestBody List<People> list) {
  4. System.out.println(list);
  5. return list.toString();
  6. }

5、Jackson用法

5.1 把对象转换为json字符串

  1. ObjectMapper objectMapper = new ObjectMapper();
  2. People peo = new People();
  3. String jsonStr = objectMapper.writeValueAsString(peo);

5.2 把json字符串转换为对象

  1. ObjectMapper objectMapper = new ObjectMapper();
  2. People peo = objectMapper.readValue(jsonStr, People.class);

5.3 把json字符串转换为List集合

  1. ObjectMapper objectMapper = new ObjectMapper();
  2. JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, People.class);
  3. List<People> list = objectMapper.readValue(jsonStr, javaType);

6、Ajax发送json参数写法

  1. var json = '[{"id":123,"name":"bjsxt"},{"id":123,"name":"bjsxt"}]';
  2. $.ajax({
  3. url:'/demo5',
  4. type:'post',
  5. success:function(data){
  6. alert(data);
  7. for(var i = 0 ;i<data.length;i++){
  8. alert(data[i].id +" "+data[i].name);
  9. }
  10. },
  11. contentType:'application/json',//请求体中内容类型
  12. dataType:'json',//响应内容类型。
  13. data:json
  14. });
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <script type="text/javascript" src="jquery.min.js"></script>
  7. <script type="text/javascript">
  8. var url = "123";
  9. // javascript中默认的,ajax请求不能跨域。
  10. // 跨域 - ajax所属的站点,和被请求的站点,不是同一个域。
  11. // 域 - ip,端口,域名,主机名,任何一个有变化,都是不同域。
  12. // 需要服务器,返回的响应中增加跨域请求头。
  13. function sendBodyParams(){
  14. $.ajax({
  15. "url": "http://localhost:80/bodyParams",
  16. "type":"post",
  17. "data":"[{\"name\":\"abc\", \"password\":\"123\"},{\"name\":\"def\", \"password\":\"321\"}]",
  18. "contentType":"application/json", // 必须设定,代表请求体的格式。默认是text/plain; 默认是 参数名=参数值&参数名=参数值 的字符串
  19. "dataType":"json",
  20. "success":function(data){
  21. alert(data);
  22. console.log(data);
  23. }
  24. });
  25. }
  26. </script>
  27. </head>
  28. <body style="text-align: center">
  29. <button onclick="sendBodyParams()">测试AJAX请求,请求体传递JSON参数</button>
  30. </body>
  31. </html>

7、跨域

跨域:协议、ip、端口中只要有一个不同就是跨域请求。
同源策略:浏览器默认只允许ajax访问同源(协议、ip、端口都相同)内容。
解决同源策略:在控制器接口上添加@CrossOrigin。表示允许跨域。本质在响应头中添加Access-Control-Allow-Origin: *

  1. @RequestMapping("/demo5")
  2. @ResponseBody
  3. @CrossOrigin
  4. public List<People> demo5(@RequestBody List<People> list) {
  5. System.out.println(list);
  6. return list;
  7. }

四、RMI实现RPC

1、RMI简介

RMI(Remote Method Invocation)远程方法调用。
RMI是从JDK1.2推出的功能,它可以实现在一个Java应用中可以像调用本地方法一样调用另一个服务器中Java应用(JVM)中的内容。可以跨机器、跨系统、跨网段
RMI 是Java语言的远程调用,无法实现跨语言

2、执行流程

image.png
Registry(注册表)是放置所有服务器对象的命名空间。 每次服务端创建一个对象时,它都会使用bind()或rebind()方法注册该对象。 这些是使用称为绑定名称的唯一名称注册的。
要调用远程对象,客户端需要该对象的引用。即通过服务端绑定的名称从注册表中获取对象:lookup()方法。

3、API介绍

3.1 Remote

java.rmi.Remote:定义了此接口为远程调用接口。如果接口被外部调用,需要继承此接口。服务器和客户端都需要实现此接口

3.2 RemoteException

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

3.3 UnicastRemoteObject

java.rmi.server.UnicastRemoteObject:适配器类。此类实现了Remote接口和Serializable接口。
自定义接口实现类除了实现自定义接口还需要继承此类。

3.4 LocateRegistry

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

3.5 Naming

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

4、代码实现

4.1 创建服务端RmiServer

image.png

1、编写接口,定义标准

定义标准:服务器实现此接口。客户端基于此接口创建动态代理

  1. package com.bjsxt.rmi.api;
  2. // 定义一个远程服务接口。RMI强制要求,必须是Remote接口的实现。
  3. public interface FirstInterface extends Remote {
  4. // RMI强制要求,所有的远程服务方法,必须抛出RemoteException。
  5. String first(String name) throws RemoteException;
  6. }

2、服务端编写实现类

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

  1. package com.bjsxt.rmi.impl;
  2. // 实现远程服务接口。 所有的远程服务实现,必须是Remote接口直接或间接实现类。
  3. // 如果不会创建基于RMI的服务标准实现,可以直接继承UnicastRemoteObject来实现此部分功能。
  4. public class FirstRMIImpl extends UnicastRemoteObject implements FirstInterface, Remote {
  5. // RMI强制要求,所有的方法必须抛出RemoteException,包括构造方法。
  6. public FirstRMIImpl() throws RemoteException{
  7. super();
  8. }
  9. // 服务实现内容
  10. @Override
  11. public String first(String name) throws RemoteException {
  12. System.out.println("客户端请求参数是:" + name);
  13. return "你好," + name;
  14. }
  15. }

3、编写主方法,服务创建与注册

  1. package com.bjsxt.rmi;
  2. // 主方法,创建一个服务实现对象,提供服务,并注册到Registry上。
  3. // RMI的Registry在创建的时候,会自动启动一个子线程,并升级为守护线程(服务线程|精灵线程)。提供持久的服务。
  4. public class MainClass {
  5. public static void main(String[] args) {
  6. try {
  7. System.out.println("服务器启动中...");
  8. // 创建服务对象
  9. FirstInterface first = new FirstRMIImpl();
  10. // 注册到Registry(注册中心)上。
  11. LocateRegistry.createRegistry(9999);
  12. // 绑定一个服务到注册中心。提供命名,格式为:rmi://ip:port/别名
  13. // 如果服务重复,抛出异常。 重复的定义是命名冲突。
  14. // Naming.bind("rmi://localhost:9999/first", first);
  15. // 重新绑定一个服务到自注册中心。 和bind的区别是,命名冲突,直接覆盖。
  16. Naming.rebind("rmi://localhost:9999/first", first);
  17. System.out.println("服务器启动完毕!");
  18. }catch (Exception e){
  19. e.printStackTrace();
  20. }
  21. }
  22. }

4、运行项目

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

4.2 创建客户端代码

image.png

1、复制服务端接口

把服务端com.bjsxt.rmi.api.FirstInterface粘贴到项目中。
注意:复制代码仅为快速完成案例,学习技术,在商业开发中,FirstInterface接口应该使用独立的工程定义,并在服务端和客户端工程中通过依赖的方式引入。

2、创建主方法类

  1. package com.bjsxt.rmi;
  2. // 客户端主方法
  3. public class ClientMainClass {
  4. public static void main(String[] args) {
  5. // 代理对象的创建。
  6. FirstInterface first = null;
  7. try{
  8. // 使用lookup找服务。通过名字找服务,并自动创建代理对象。
  9. // 类型是Object,对象一定是Proxy的子类型,且一定实现了服务接口。
  10. first = (FirstInterface) Naming.lookup("rmi://localhost:9999/first");
  11. System.out.println("对象的类型是:" + first.getClass().getName());
  12. String result = first.first("S106,今天课程讲不完了");
  13. System.out.println(result);
  14. }catch (Exception e){
  15. e.printStackTrace();
  16. }
  17. }
  18. }

image.png
image.png

五、 Zookeeper基础

1、Zookeeper简介

zookeeper分布式管理软件。常用它做注册中心(依赖zookeeper的发布/订阅功能)、配置文件中心、分布式锁配置、集群管理等。
zookeeper一共就有两个版本。主要使用的是java语言写的。

2、Zookeeper安装

2.1 上传压缩文件
上传到 /usr/local/tmp中
2.2 解压
# tar zxf apache-zookeeper-3.5.5-bin.tar.gz
# cp -r apache-zookeeper-3.5.5-bin /usr/local/zookeeper
2.3 新建data数据目录
进入到zookeeper中
# cd /usr/local/zookeeper
# mkdir data
2.4 修改配置文件
进入conf中
# cd conf
# cp zoo_sample.cfg zoo.cfg
# vim zoo.cfg
修改dataDir为data文件夹路径,用作Zookeeper的数据存储目录。
image.png
2.5 启动zookeeper
进入bin文件夹
# cd /usr/local/zookeeper/bin
# ./zkServer.sh start

通过status查看启动状态。稍微有个等待时间
# ./zkServer.sh status
image.png

3、Zookeeper客户端常用命令

进入到./zkCli.sh命令行工具后,可以使用下面常用命令。【Zookeeper有个非常好的命令提示方式,只要输入的命令是Zookeeper不识别的命令,Zookeeper会立刻显示所有有效命令提示列表】
zkCli.sh 默认连接的是 localhost:2181 Zookeeper主机。
zkCli.sh -server ip:port 指定连接到 ip:port Zookeeper主机。
3.1、quit
退出ZK客户端控制台。
3.2、ls
ls [-s] [-R] /path
-s 详细信息,替代老版的ls2
-R 当前目录和子目录中内容都罗列出来
例如:ls -R / 显示根目录下所有内容
3.3、create
create [-e] [-s] /path [data]
[data] 包含内容
创建指定路径信息
-e 创建临时节点, 代表当前会话断开,自动删除。
-s 创建节点时,自动为节点命名增加后缀。
例如:
create /demo 创建/demo
create -e /demo 创建临时节点 /demo
create -s /demo 创建命名前缀为/demo的节点
create -e -s /demo 创建命名前缀为/demo的临时节点

3.4、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:子节点数量
3.5、set
set /path data
设置节点内容
3.6、delete
delete /path
删除节点,不能删除有子节点的节点。
deleteall /path
删除节点,可以删除带有子节点的节点。
rmr /path
删除节点,可以删除带有子节点的节点。已过时,建议使用deleteall。 在旧版本的zk中,没有deleteall命令。

六、Zookeeper增删查内容

1、添加依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.apache.zookeeper</groupId>
  4. <artifactId>zookeeper</artifactId>
  5. <version>3.6.1</version>
  6. </dependency>
  7. </dependencies>

2、创建节点

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

  1. ./zkCli.sh
  2. create /demos

ZooDefs.Ids.OPEN_ACL_UNSAFE:表示权限。
CreateMode.PERSISTENT_SEQUENTIAL:永久存储,文件内容编号递增。

  1. /**
  2. * 什么是会话?
  3. * 持久、长期、有状态的对象。
  4. * 使用Java远程访问zk,步骤是:
  5. * 1、 创建客户端
  6. * 2、 使用客户端发送命令
  7. * 3、 处理返回结果
  8. * 4、 回收资源
  9. */
  10. public static void create() throws IOException, KeeperException, InterruptedException {
  11. // 创建客户端对象
  12. ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, new Watcher() {
  13. @Override // 创建或回收资源
  14. public void process(WatchedEvent watchedEvent) {
  15. System.out.println("watch中的方法执行");
  16. }
  17. });
  18. // 创建一个节点
  19. String result = zooKeeper.create("/parent", "parent data".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  20. System.out.println("创建parent节点结果:" + result);
  21. // 创建一个临时节点
  22. String tmpResult = zooKeeper.create("/parent/tmp", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  23. System.out.println("创建/parent/tmp节点结果:" + tmpResult);
  24. // 创建一个带序号的节点
  25. String seqResult = zooKeeper.create("/parent/sequence", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
  26. System.out.println("创建/parent/sequence节点结果:"+ seqResult);
  27. Thread.sleep(6000);
  28. // 关闭客户端
  29. zooKeeper.close();
  30. }

image.png
image.png

3、查询节点

  1. /**
  2. * 查询节点, 相当于遍历Key
  3. */
  4. public static void list() throws Exception{
  5. ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, new Watcher() {
  6. @Override
  7. public void process(WatchedEvent watchedEvent) {
  8. }
  9. });
  10. // 遍历zk中所有的节点。
  11. listAll(zooKeeper, "/");
  12. }
  13. private static void listAll(ZooKeeper zooKeeper, String path) throws Exception{
  14. // 获取当前节点的所有子节点。
  15. List<String> children = zooKeeper.getChildren(path, false);
  16. for(String child : children){
  17. String currentNodeName = "/".equals(path) ? (path + child) : (path + "/" + child);
  18. System.out.println(currentNodeName);
  19. listAll(zooKeeper, currentNodeName);
  20. }
  21. }

image.png
image.png

4、查询节点中存储的数据

  1. /**
  2. * 查询节点中存储的数据。相当于根据key获取value
  3. */
  4. public static void get() throws Exception{
  5. ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, new Watcher() {
  6. @Override
  7. public void process(WatchedEvent watchedEvent) {
  8. }
  9. });
  10. // 获取数据
  11. byte[] datas = zooKeeper.getData("/parent", false, null);
  12. System.out.println("节点/parent中存储的数据是:" + new String(datas));
  13. }

image.png
image.png

5、删除节点

  1. /**
  2. * 删除节点.
  3. * 删除节点前,需要先查询节点的状态(cversion)。通过getData来查询这个版本。
  4. * 设计是为了保证删除的节点是你想删除的那个。
  5. */
  6. public static void delete() throws Exception{
  7. ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, new Watcher() {
  8. @Override
  9. public void process(WatchedEvent watchedEvent) {
  10. }
  11. });
  12. Stat stat = new Stat();
  13. System.out.println(stat);
  14. zooKeeper.getData("/parent/sequence0000000001", false, stat);
  15. System.out.println(stat.getCversion());
  16. zooKeeper.delete("/parent/sequence0000000001", stat.getCversion());
  17. }

image.png
image.png

七、手写RPC框架

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

1 创建项目ParentDemo

创建父项目ParentDemo。
包含4个聚合子项目。
commons:包含Zookeeper访问工具类。
service:包含被serviceimpl和consumer依赖的接口。
serviceimpl:provider提供的服务内容
consumer:消费者,调用服务内容。

8 在父项目中添加依赖

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies>

9 创建commons项目

此项目中的重点是Zookeeper连接工具类

13.6 创建ZooKeeperUtils工具类

创建com.bjsxt.commons.ZooKeeperUtils类

| // 工具public class ZooKeeperUtils {
private static ZooKeeper _zooKeeper
;
static {
// 初始化ZK客户端对象 try {
_zooKeeper
= new ZooKeeper(“192.168.89.140:2181”, 10000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {

  1. }<br /> });<br /> } **catch **(IOException e) {<br /> e.printStackTrace();<br /> _// 初始化代码块发生错误,后续代码没有任何执行的价值。_ _**throw new **ExceptionInInitializerError(**"初始化ZK客户端错误"**);<br /> }<br /> }<br /> _// 存URL到ZK; 注册服务, 就是把本地LocateRegistry中注册的URL存储到ZK。_ // 存储URL的节点,有一个规则, 必须是 /myrpc/接口名称/providers_ // /myrpc/com.bjsxt.service.FirstService/providers_ _**public static boolean **register(String URL, String className){<br /> String path = **"/myrpc/" **+ className + **"/providers"**;
  2. **try **{<br /> _// 判断是否需要初始化_ _List<String> children = _zooKeeper_.getChildren(**"/"**, **false**);
  3. **if**(!children.contains(**"myrpc"**)){<br /> _// 没有节点,创建_ zooKeeper_.create(**"/myrpc"**, **""**.getBytes(),<br /> ZooDefs.Ids.**_OPEN_ACL_UNSAFE_**,<br /> CreateMode.**_PERSISTENT_**);_// 临时的节点_ zooKeeper_.create(**"/myrpc/"**+className, **""**.getBytes(),<br /> ZooDefs.Ids.**_OPEN_ACL_UNSAFE_**,<br /> CreateMode.**_PERSISTENT_**);_// 临时的节点_ _}**else**{<br /> _// 有节点,判断子节点是否存在_ _children = _zooKeeper_.getChildren(**"/myrpc"**, **false**);<br /> **if**(!children.contains(className)){<br /> _// 没有节点,创建_ zooKeeper_.create(**"/myrpc/"**+className, **""**.getBytes(),<br /> ZooDefs.Ids.**_OPEN_ACL_UNSAFE_**,<br /> CreateMode.**_PERSISTENT_**);_// 临时的节点_ _}<br /> }
  4. _zooKeeper_.create(path, URL.getBytes(**"UTF-8"**),<br /> ZooDefs.Ids.**_OPEN_ACL_UNSAFE_**,<br /> CreateMode.**_PERSISTENT_**);_// 节点_ // 代码没有异常。创建成功, 返回true_ _**return true**;<br /> } **catch **(KeeperException e) {<br /> e.printStackTrace();<br /> } **catch **(InterruptedException e) {<br /> e.printStackTrace();<br /> } **catch **(UnsupportedEncodingException e) {<br /> e.printStackTrace();<br /> }<br /> _// 代码发生异常。创建失败,返回false_ _**return false**;<br /> }
  5. _// 从ZK中读取URL_ // 从ZK中,查询服务的URL地址。 节点名称有规则,规则是/myrpc/类型名称/providers_ _**public static **String getURL(String className){<br /> **try **{<br /> **byte **[] datas =<br /> _zooKeeper_.getData(**"/myrpc/"**+className+**"/providers"**, **false**, **null**);<br /> **return new **String(datas, **"UTF-8"**);<br /> } **catch **(KeeperException e) {<br /> e.printStackTrace();<br /> } **catch **(InterruptedException e) {<br /> e.printStackTrace();<br /> } **catch **(UnsupportedEncodingException e){<br /> e.printStackTrace();<br /> }<br /> **return null**;<br /> }

}
| | —- |

10 创建service项目

此项目中重点编写需要被两个项目依赖的接口。

13.7 创建DemoService接口

创建com.bjsxt.DemoService,具体内容如下:

public interface DemoService extends Remote {
String demo(String param) throws RemoteException;
}

11 创建serviceimpl项目

此项目编写接口具体实现,RMI服务发布和把信息发送到Zookeeper中。
在pom.xml中添加对service和commons项目的依赖

<dependencies>
<dependency>
<artifactId>service</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<artifactId>commons</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

13.8 创建DemoServiceImpl

创建com.bjsxt.service.impl.DemoServiceImpl

public class DemoServiceImpl extends UnicastRemoteObject implements DemoService {
public DemoServiceImpl() throws RemoteException {
}
@Override public String demo(String param) throws RemoteException{
return param+“123”**;
}
}

13.9 创建RmiRun

创建com.bjsxt.RmiRun。实现RMI服务的发布和Zookeeper消息的发布。

| public class MainClass {
public static void main(String[] args) {
try {
System.out.println(“服务器正在启动…”);
DemoService service = new DemoServiceImpl();
String url = “rmi://localhost:9999/demoService”;
// 创建Registry LocateRegistry._createRegistry(9999);
// 注册服务 Naming._rebind(url, service);
// 存储url到ZK,其他的使用服务的客户端,访问zk获取url。 boolean isSuccess =
ZooKeeperUtils._register
(url, DemoService.class.getName());
if(isSuccess){
System.out.println(“服务器启动成功…”);
}else{
System.out.println(“服务器启动失败… 请检查后重新尝试!”);
System.exit(9);
}

  1. }**catch**(Exception e){<br /> e.printStackTrace();<br /> }<br /> }<br />} |

| —- |

12 创建Consumer项目

新建consumer项目,此项目需要从zookeeper中获取rmi信息,并调用rmi服务
在pom.xml中添加对service和commons项目的依赖

<dependencies>
<dependency>
<artifactId>service</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<artifactId>commons</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

13 创建接口和实现类

创建com.bjsxt.service.ConsumerService接口
创建com.bjsxt.service.impl.ConsumerServiceImpl实现类

public interface ConsumerService {
String consumerService(String param);
}
@Servicepublic class ConsumerServiceImpl implements ConsumerService {
@Override public String consumerService(String param) {
try {
String url = ZooKeeperUtils.getURL(DemoService.class.getName());
if(url == null){
System.out.println(“服务获取失败!”);
return;
}
DemoService demoService =(DemoService) Naming.lookup(_url);
String result = demoService.demo(param);
System.**_out
.println(result);
return result;
}
catch (IOException e) {
e.printStackTrace();
}
catch (KeeperException e) {
e.printStackTrace();
}
catch (InterruptedException e) {
e.printStackTrace();
}
catch (NotBoundException e) {
e.printStackTrace();
}
return null**;
}
}

14 创建控制器

创建com.bjsxt.controller.DemoController控制器

| @Controllerpublic class **DemoController {

  1. **@Autowired** private **ConsumerService **consumerService**;
  2. **@RequestMapping**(**"/demo"**)<br /> **@ResponseBody** public **String demo(String param){<br /> **return consumerService**.consumerService(param);<br /> }<br />} |

| —- |

15 创建启动器

创建com.bjsxt.ConsumerApplication

@SpringBootApplicationpublic class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.
class**,args);
}
}

16 测试

在浏览器输入:http://localhost:8080/demo?param=demo
观察结果是否是:demo123