北京尚学堂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();
        }
    }
}
posted @   明卿册  阅读(74)  评论(1编辑  收藏  举报
编辑推荐:
· 基于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加密过程(密码学_古典密码学_多图加密算法)
点击右上角即可分享
微信分享提示