本文是孙哥说分布式系列课程笔记,B站搜索孙哥说分布式有试看视频。
1.引言
1.1.架构演变过程
1.单体架构 2.垂直架构 3.RPC架构
1.2.PRC架构
其实对于RPC这种编程方式称之为架构,有点过高的评价了,RPC仅仅是SOA的其中一个组成,不仅在SOA架构亦或者是微服务架构都有使用,它是从垂直架构中演变而来。从宏观的角度来讲,RPC也属于垂直架构,我们来看看PRC到底解决了什么问题。
在垂直架构中,我们说每个子系统都要独立部署,假如我们把系统划分前台门户,和后端管理,我们把每一个service理解成子系统的一个一个的服务,或者说一个一个的模块,在这我们有一个需求,在这个门户中,有一个用户自服务(我的),这个功能有一个需求,查看我自己的订单,我们要实现这个功能的思路就是:1.在门户用户自服务中,直接查询数据库。 2. 调用后台子系统中订单功能来完成这个业务。 这两种方式孰优孰劣? 显然是2好,1这种实现订单功能升级改造的话,涉及到两个地方的改造。如果调用后台子系统中订单功能来完成这个业务都在这一个模块中直接处理完,符合我们软件设计的一个原则,单一职责原则。显然我们认为2这种方式更为合理。
我们选定了第二种方式,两个不同的JVM中的Servive的相互调用就是一个问题了,跨虚拟机意味着就是跨进程间的调用,我们就不能直接在SelfService中直接调用OrderService中的方法了。调用本地方法我们直接创建对象,调用对象中的方法,但是跨进程调用,像调本地方法那样调用就行不通了,我们面临的最核心的问题就是进程间调用怎么像调用本地方法那样调用。唯一的解决思路就是走网络,进行网络编程,那势必就涉及到我们使用什么样的协议来进行网络通信呢?HTTP还是TCP协议呢?
实际上,HTTP还是TCP都是可行的,他们各有利弊。TCP协议和HTTP协议是有关系的,底层其实都是TCP协议,HTTP 1.0 是短链接协议,HTTP1.1是有限的长连接。这里不做深入的研究咯。
如果我选择用HTTP,我们应该如何解决这种跨进程间的通信呢???
1.HTTP对于开发者最方便的地方是有现成的服务器---tomcat/resin/weblogic/jboss,我们就不用开发。
2.在服务器端中我们使用HTTP协议(这里的服务器是指用户自服务模块需要调用订单功能,这里的OrderService就被看做为服务器)不能直接访问OrderService。我们使用的是javaEE的服务器,中间需要一层Controller,服务要通过控制器进行暴露。
3.在客户端的角度来说,我们怎么来发起调用? 其实就是发HTTP请求,不一定是通过浏览器,有可能是内部调用(OKHttp,HttpClient,RestTemplate,WebClient)。
问题:1.包一层controller,不能通过网络直接调用service
2.客户端代码过于繁琐,比如说HttpClient(new HttpClient() --> GET POST) ,不能直接表达业务含义。
3.走HTTP协议,传输的数据量大(比如传输一个json{"name":"suns"},HTTP协议传输的数据比TCP传输的数据量更大 )。因为HTTP包了很多的协议头,比如Cookie。而且他会逐层传输,每一层都要加相应的头,所以数据量更大。
典型使用HTTP实现方案:1.SpringCloud Feign 2.Hessian RPC
优势:他是文本类型,字符串协议,跨语言平台好。
如果我们用TCP协议,需要了解些什么?
明确跨进程调用的本质是什么,其实就是网络通信,网络通信什么呢? 我把我的参数给你传过来,你呢把处理好的结果再返回给我。
坏处:服务器端得自己开发(Socket NIO/Netty等)。接受客户端传递过来的参数,你需要调用哪个方法的名字。调用完成之后,返回结果。客服端也得自己开发,没有标准协议,都是私有的自定义协议,有大量的编码,还要自己解决半包粘包的问题。
好处:1.可以直接进行service的调用,比如阿里的RPC框架Dubbo从来没有controller层。
2.效率高(不会频繁的三次握手,四次挥手),协议自定义,可以使用二进制的协议,数据传输更小,连接的复用性好,线程管理好。
典型TCP的技术实现方案:Dubbo
这两种方式都可以称之为RPC跨进程调用。
1.3.RPC的设计
设计目标:让调用者像调用本地方法一样去调用远端服务的方法。前面的案例中调用者:SelfService 远端的服务方法:OrderService#findUserOrderByUserId()
//SelfService
findUserOrderByUserId() {
OrderService#findUserOrderByUserId()
}
这样调用很清晰的知道了这个方法的主要功能,那我们当然不能直接这样写。
1.网络通信。
2.传递数据,接受返回值。
我们在设计PRC的时候,我们会把这两点封装,在调用者使用的时候相当于是一个黑盒。
自定义协议: 协议头: 幻数(魔数) 版本号 指令类型(登录、注册) 序列化方式(json,protobuf,xml,hessian) 正文的长度:避免半包和粘包,头体分离。 协议正文:需要传输的数据。
RPC在设计的过程中
1.通信方式TCP(NIO/Netty)
2.自定义协议。
3.序列化方式(json,protobuf,xml,hessian)。
4.怎么设计和远端实现同样的类和方法。
这个第四点像不像代理设计模式呢?所谓的代理就是通过代理类为原始类增加额外功能(这个孙哥的spring5讲过的)
原始类(Skeleton):为远端的服务方法。
额外功能:网络通信,传输数据。
代理类(Stub):本地和远端实现想同名字的类。
所以应该在调用者这方创建远端服务类的代理类。
衍生的方案:
(一)注册中心核心的作用是服务的治理。
1.可以进行负载均衡,轮询,加权等。
2.管理服务,监测可用健康的服务(心跳 重试【延迟队列】)。
3.解耦合,客户端调用注册中心,不依赖某个特定的服务。
(二) 熔断,限流,降级。
2.Hessian RPC
2.1.为什么学习Hessian RPC
Hessian是基于Java开发的RPC,技术比较老,不涉及到衍生的方案,那为什么我们还要学习呢?
1.原始4种方案实现(1.网络通信 2.协议 3.序列化 4.代理)的非常经典
2.而且在Dubbo中,还保留了Hessian的系列化方式。
3.基于Java语言编写。
2.2.Hessian RPC的基本概念
1.Resin服务器的衍生产品。是一个web服务器,曾经新浪大量使用。
2.基于Java语言设计的PRC框架,只支持Java编程语言使用,服务的调用者 服务者都必须是Java开发。(像gRPC,Thirft这样的支持多语言)。
3.它的序列化 协议是二进制的。
4.Hessian RPC的官网 http://hessian.caucho.com。
2.3.Hessian RPC的设计思想
2.3.1 服务的提供者设计
1.服务器 Tomcat resin 2.创建服务,就是原来开发的service,必须定义接口。service 和 impl实现类 2.1 为什么有这样的要求,因为调用者需要一个代理类,必须和服务端都实现想同的接口。 2.2 在数据类型定义时,对象类型的数据必须诗仙女Serliazble接口。
3.服务的发布。让调用者知道我提供了哪些服务,以及这些服务如何访问。
3.1 HessianServlet
3.2 只能是POST请求
web.xml
<servlet>
<servlet-name>hessianServlet
<servlet-class>xxxx.xxxxx.HessianServlet
<init-param>
<param-name>home-api</param-name>
<param-value>com.suns.service.OrderService</param-value>
<init-param>
<param-name>home-class</param-name>
<param-value>com.suns.service.OrderServiceImpl</param-value>
</servlet>
<servlet-mapping>
<servlet-name>hessianServlet
<url-pattern>/orderService
<servlet>
<servlet-name>hessianServlet1
<servlet-class>xxxx.xxxxx.HessianServlet
<init-param>
<param-name>home-api</param-name>
<param-value>com.suns.service.UserService</param-value>
<init-param>
<param-name>home-class</param-name>
<param-value>com.suns.service.UserServiceImpl</param-value>
</servlet>
<servlet-mapping>
<servlet-name>hessianServlet1
<url-pattern>/userService
2.3.2. 服务的调用者设计
1.hessian提供了一个工具类,HessianProxyFactory 我们需要提供两个要素
1.需要接口类型 比如OrderService.class
2.URL
2.服务的接口要做成公共模块。
2.4.HessianRPC的开发
2.4.1 环境搭建
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.tang</groupId>
<artifactId>rpc-lession-tcy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>org.tang</groupId>
<artifactId>rpc-hessian</artifactId>
<packaging>war</packaging>
<name>rpc-hessian Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!-- 编译的时候会使用这个jar包,部署到运行环境不会引入这个jar 因为Tomcat里面有servlet的jar包-->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.38</version>
</dependency>
</dependencies>
<build>
<finalName>rpc-hessian</finalName>
</build>
</project>
2.4.2开发步骤
1.服务端
-
开发服务 需要DAO-mybatis
-
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>userServiceRPC</servlet-name>
<servlet-class>com.caucho.hessian.server.HessianServlet</servlet-class>
<init-param>
<param-name>home-api</param-name>
<param-value>com.tang.service.UserService</param-value>
</init-param>
<init-param>
<param-name>home-class</param-name>
<param-value>com.tang.service.UserServiceImpl</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>userServiceRPC</servlet-name>
<url-pattern>/userServiceRPC</url-pattern>
</servlet-mapping>
</web-app>
2.客服端(远端服务调用者)
1.HessianProxyFactory
2.参数:远端服务的接口,远端服务的URL
package com.tang.client;
import com.caucho.hessian.client.HessianProxyFactory;
import com.tang.service.UserService;
import lombok.extern.slf4j.Slf4j;
import java.net.MalformedURLException;
@Slf4j
public class HessianRPCClient {
public static void main(String[] args) throws MalformedURLException {
String url = "http://localhost:8989/rpc-hessian/userServiceRPC";
HessianProxyFactory hessianProxyFactory = new HessianProxyFactory();
UserService userService = (UserService) hessianProxyFactory.create(UserService.class, url);
boolean loginRes = userService.login("xiaohei", "123456");
log.debug("login res is : {}", loginRes);
}
}
2.4.3 HessianRPC核心源码分析
1.HessianRPC client 创建代理的方式: JDK Proxy.newProxyInstance()
2.在代理中:通过网络HTTP请求连接远端RPC服务。
通过流写数据出去。
public Object create(Class<?> api, URL url, ClassLoader loader) {
if (api == null) {
throw new NullPointerException("api must not be null for HessianProxyFactory.create()");
} else {
InvocationHandler handler = null;
handler = new HessianProxy(url, this, api);
return Proxy.newProxyInstance(loader, new Class[]{api, HessianRemoteObject.class}, handler);
}
}
我们知道Proxy.newProxyInstance的第三个参数是InvocationHandler,它的作用是为原始对象增加额外功能。
1.网络连接
2.通过网络把序列化的数据传给远端RPC并接受远端PRC的调用返回值。
下面我们来看看InvocationHandler中的invoke方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String mangleName; // 存储方法名
synchronized(this._mangleMap) { // 加锁保证线程安全
mangleName = (String)this._mangleMap.get(method);
}
if (mangleName == null) { // 如果方法名未存储在 _mangleMap 中
String methodName = method.getName(); // 获取方法名
Class<?>[] params = method.getParameterTypes(); // 获取参数类型
if (methodName.equals("equals") && params.length == 1 && params[0].equals(Object.class)) {
// 处理 equals 方法
Object value = args[0];
if (value != null && Proxy.isProxyClass(value.getClass())) {
Object proxyHandler = Proxy.getInvocationHandler(value);
if (!(proxyHandler instanceof HessianProxy)) {
return Boolean.FALSE; // 不同代理对象返回 false
}
HessianProxy handler = (HessianProxy)proxyHandler;
return new Boolean(this._url.equals(handler.getURL())); // 比较代理的 URL 是否相同
}
return Boolean.FALSE;
}
if (methodName.equals("hashCode") && params.length == 0) {
return new Integer(this._url.hashCode()); // 返回 URL 的哈希值
}
if (methodName.equals("getHessianType")) {
return proxy.getClass().getInterfaces()[0].getName(); // 返回代理类的类型名
}
if (methodName.equals("getHessianURL")) {
return this._url.toString(); // 返回代理的 URL 字符串表示
}
if (methodName.equals("toString") && params.length == 0) {
return "HessianProxy[" + this._url + "]"; // 返回代理对象的字符串描述
}
if (!this._factory.isOverloadEnabled()) {
mangleName = method.getName(); // 不允许方法重载时,直接使用方法名
} else {
mangleName = this.mangleName(method); // 允许方法重载时,对方法名进行处理
}
synchronized(this._mangleMap) {
this._mangleMap.put(method, mangleName); // 存储方法名到 _mangleMap 中
}
}
InputStream is = null; // 输入流
HessianConnection conn = null; // Hessian 连接
try {
if (log.isLoggable(Level.FINER)) {
log.finer("Hessian[" + this._url + "] calling " + mangleName);
}
conn = this.sendRequest(mangleName, args); // 发送HTTP请求并获取连接
is = this.getInputStream(conn); // 获取输入流
if (log.isLoggable(Level.FINEST)) {
PrintWriter dbg = new PrintWriter(new LogWriter(log));
HessianDebugInputStream dIs = new HessianDebugInputStream((InputStream)is, dbg);
dIs.startTop2(); // 开始读取 Hessian 数据
is = dIs;
}
int code = ((InputStream)is).read(); // 读取字节码
AbstractHessianInput in;
int major;
int minor;
Object value;
if (code != 72) {
if (code == 114) {
// 读取远程方法调用的返回结果
major = ((InputStream)is).read();
minor = ((InputStream)is).read();
in = this._factory.getHessianInput((InputStream)is);
in.startReplyBody();
value = in.readObject(method.getReturnType());
if (value instanceof InputStream) {
value = new ResultInputStream(conn, (InputStream)is, in, (InputStream)value);
is = null;
conn = null;
} else {
in.completeReply();
}
return value;
}
throw new HessianProtocolException("'" + (char)code + "' is an unknown code");
}
major = ((InputStream)is).read();
minor = ((InputStream)is).read();
in = this._factory.getHessian2Input((InputStream)is);
value = in.readReply(method.getReturnType());
return value;
} catch (HessianProtocolException var30) {
throw new HessianRuntimeException(var30);
} finally {
try {
if (is != null) {
((InputStream)is).close();
}
} catch (Exception var27) {
log.log(Level.FINE, var27.toString(), var27);
}
try {
if (conn != null) {
conn.destroy();
}
} catch (Exception var26) {
log.log(Level.FINE, var26.toString(), var26);
}
}
}
2.4.4 Hessian序列化
序列化是属于协议的一部分,把我们的消息的正文以一种特定的格式传给服务端,服务端再以这种特定的格式再把数据回传回来。所以他就是传递数据的一种格式。Hessian序列化在Dubbo还在使用,只是它改过。