RPC : 远程方法调用 (Remote Procedure Call),是一个计算机通信协议,该协议允许运行于一台计算机的程序调用另一个地址空间的子程序,就想调用本地程序一样,无需额外的为这个交互作用编程(无需关注细节),是一种服务器-客户端(Client/Server)模式,经典实现是通过发送请求-接受回应进行信息交互的系统。是指两个进程内的方法相互调用。后续会用RPC基于Http协议来传输数据的方式手写实现。
Dubbo: 一开始的定位是RPC,专注于两个服务之间的调用,但随着微服务的盛行,除开服务调用外,Dubbo也在逐步的涉猎服务治理、服务监控、服务网关等,所以现在的Dubbo目标已经不止是RPC框架了,而是一个微服务的的服务框架
简图:
1.定义好服务提供的接口及实现类及方法
public interface HelloService {
String sayHello(String userName);
}
public class HelloServerImpl implements HelloService{
@Override
public String sayHello(String userName) {
return "hello:"+userName;
}
}
正常情况下 本地可直接 new 一个实例来调用方法,或者注入这个Bean来调用方法;这里先定义好,便于请求方调用
2.作为服务端,需要启动一个能接收Http网络请求的服务,例如Jetty/Tomcat/Netty这里用Tomcat来实现,写一个Httpserver类,用于启动Tomcat
public class Provider {
public static void main(String[] args) {
//1.接口 实现类 2.通过网络来访问我这个服务 需要有服务来接收网络请求 例如:jetty/tomcat/netty 接收http 这里服务端需要有相应的服务
//启动http服务 才能接收http请求 自己的启动方法 定义好服务的ip 端口
Httpserver httpserver = new Httpserver();
httpserver.start("localhost",8080);
//3.需要双方协商好 交互的 内容 请求服务需要给 ->接口名字、方法名、方法参数类型列表(可能重载)、参数值列表、version 可加版本实现多版本功能
//这里不考虑多实现类多版本问题
//请求内容可封装为一个对象 双方统一序列化方式都用JDK
//请求过来后 会传接口名过来,那么到底对应的是哪个具体实现类,只有provider知道 provider需要有一个本地注册,将自己的实现类记录下来
}
}
public class Httpserver {
//启动服务
public void start(String hostname ,Integer port){
//tomcat 启动 用内嵌的tomcat 本身就是java写的
Tomcat tomcat = new Tomcat();
Server server= tomcat.getServer();
Service service =server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(port);
Engine engine = new StandardEngine();
engine.setDefaultHost(hostname);
Host host = new StandardHost();
host.setName(hostname);
String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
//tomcat只是一个servlet容器 没法处理,还需要添加servlet进行处理 交给servlet处理
//相当于所有tomcat收到的请求都会到 DispatcherServletMyself里面去
tomcat.addServlet(contextPath,"dispatcher",new DispatcherServletMyself());
context.addServletMappingDecoded("/*","dispatcher");
try {
tomcat.start();
tomcat.getServer().await();
} catch (LifecycleException e) {
throw new RuntimeException(e);
}
}
}
3.但实际上Tomcat只是一个容器,并不能直接处理,需要引入servlet来处理,所有tomcat收到的请求都会到DispatcherServletMyself类里面去 我自定义处理
public class DispatcherServletMyself extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//请求都到这里来具体进行处理
// super.service(req, resp);
//自定义处理方式 可以用多个handler 来处理 有的用于处理心跳 有的处理鉴权等等~~更灵活便于扩展
new HttpServerHandler().hanler(req,resp);
}
}
3.2在继承了HttpServlet以后,又建了一个Handler类来专门处理,以后可以多个Handler来处理不同请求不同的数据,便于扩展,
public class HttpServerHandler {
public void hanler(HttpServletRequest req, HttpServletResponse resp) {
//处理请求的核心逻辑 req 需要知道 req里面有什么 将我们自定义的invocation对象拿出来
try {
Invocation invocation = (Invocation) new ObjectInputStream(req.getInputStream()).readObject();
//执行client想要执行的方法 拿到接口名
String interfaceName = invocation.getInterfaceName();
//将注册的具体实现类 获取到 如果有多个实现类 需要由客户端传个版本号过来服务端才能识别
Class classImpl = LoclRegister.get(interfaceName);
//将方法名字,方法参数类拿到 获取到具体的方法对象
Method method = classImpl.getMethod(invocation.getMethodName(), invocation.getParamType());
String result = (String)method.invoke(classImpl.newInstance(), invocation.getParams());
//结果返回出去 这个工具 这里只支持写string 写到响应,返回结果出去
IOUtils.write(result,resp.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
}
3.3 上面代码的Invocation 对象,就是客户端和服务端约定好的 交互对象,如果要处理请求并返回结果,双方就必须约定好相互传输的数据内容格式这些东西,否则也无法解析(可以理解为一种协议),这里用JDK的序列化即可实现
public class Invocation implements Serializable {
//接口名字 全限定名
private String interfaceName;
//方法名字
private String methodName;
//参数类型列表
private Class[] paramType;
//参数值列表
private Object[] params;
public Invocation(String interfaceName, String methodName, Class[] paramType, Object[] params) {
this.interfaceName = interfaceName;
this.methodName = methodName;
this.paramType = paramType;
this.params = params;
}
public String getInterfaceName() {
return interfaceName;
}
public void setInterfaceName(String interfaceName) {
this.interfaceName = interfaceName;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class[] getParamType() {
return paramType;
}
public void setParamType(Class[] paramType) {
this.paramType = paramType;
}
public Object[] getParams() {
return params;
}
public void setParams(Object[] params) {
this.params = params;
}
}
3.4 Client想要实现RPC像本地调用一样的得到结果,就必须要传递数据给服务端,服务端才知道客户端具体调用的哪个接口的哪个方法,入参等等。这里最基本的就是客户端调用的时候,要告诉服务端,调用的 1.接口名字(这里只有一个实现类如果服务端多实现类就需要加上版本version识别) 2.方法名字(一个接口可能有多个方法的) 3.由于服务端可能存在重载,还需要加上参数类型列表 4.参数值列表 5.(可能需要version)这里暂不考虑多实现类的情况
4.服务端可以再加上本地服务注册,具体哪些接口和实现类是暴露出去可以被调用的
/**
* Author:Eric
* DATE:2023/8/2-21:58
* Decription: 本地注册 服务端用于识别具体接口对应的实现类
*/
public class LoclRegister {
// key 是接口的名字 value 是具体对应的实现类
private static Map map=new HashMap<>();
//注册接口及其实现类
public static void regist(String interfaceName,Class implClass){
map.put(interfaceName,implClass);
}
//获得接口的实现类
public static Class get(String interfaceName){
return map.get(interfaceName);
}
}
public class Provider {
public static void main(String[] args) {
//1.接口 实现类 2.通过网络来访问我这个服务 需要有服务来接收网络请求 例如:jetty/tomcat/netty 接收http 这里服务端需要有相应的服务
//本地注册 暴露接口出去
LoclRegister.regist(HelloService.class.getName(), HelloServerImpl.class);
//可进行操作,如果不符合的接口请求 返回对应提示信息
//启动http服务 才能接收http请求
Httpserver httpserver = new Httpserver();
httpserver.start("localhost",8080);
//3.需要双方协商好 交互的 内容 请求服务需要给 ->接口名字、方法名、方法参数类型列表(可能重载)、参数值列表、version 可加版本实现多版本功能
//这里不考虑多实现类多版本问题
//请求内容可封装为一个对象 双方统一序列化方式都用JDK
//请求过来后 会传接口名过来,那么到底对应的是哪个具体实现类,只有provider知道 provider需要有一个本地注册,将自己的实现类记录下来
}
}
5.消费端 正常发送Http请求来访问,不关心具体的请求方式是post还是get
/**
* Author:Eric
* DATE:2023/8/2-23:06
* Decription: 发送数据出去
*/
public class HttpClient {
public String send(String hostname, Integer port, Invocation invocation){
//配置
try {
URL url =new URL("http",hostname,port,"/");
HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection();
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
OutputStream outputStream = httpURLConnection.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(invocation);
oos.flush();
oos.close();
InputStream inputStream = httpURLConnection.getInputStream();
String res = IOUtils.toString(inputStream);
return res;
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (ProtocolException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
6.消费端启动 发请求过去
public class Consumer {
public static void main(String[] args) {
Invocation invocation = new Invocation(HelloService.class.getName(),"sayHello",
new Class[]{String.class}, new String[]{"lily65465"});
String result = new HttpClient().send("localhost",8080,invocation);
System.out.println("获取返回结果:"+result);
}
}
7.先启动服务端,再启动消费端发送请求
8.消费端每次这样调用也很麻烦,这里可以引入代理此处用jdk的动态代理,来代理实现具体的接口,传入需要访问调用的接口类,即可对其进行代理的访问。在代理中 对localhost:8080的服务端进行访问
代理类:
public class ProxyFactory {
public static T getProxy(final Class interfaceClass){
//读取用户的配置,看用户用什么,这里用jdk
//得到代理对象
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class[]{interfaceClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),
method.getParameterTypes(), args);
String result = new HttpClient().send("localhost",8080,invocation);
return result;
}
});
}
}
改造后的Client:因为如果要真的像本地调用一样的方式去用,不用代理,就需要将服务端打成jar包放进去了,就是单体应用了。生成代理对象后,执行代理对象的方法,就会进入invoke方法,invoke方法负责 发送http请求给服务端
public class Consumer {
public static void main(String[] args) {
//Invocation invocation = new Invocation(HelloService.class.getName(),"sayHello",
// new Class[]{String.class}, new String[]{"lily65465"});
// String result = new HttpClient().send("localhost",8080,invocation);
// System.out.println("获取返回结果:"+result);
//##########################################
//获取返回结果:hello:lily
// HelloService helloService 如果直接得到这个对象 就可以直接调用方法 如果要得到就需要将provider整体打jar包放进去,就成了
//单体应用了 所以可以用代理对象 得到helloservice的代理对象
HelloService helloService = ProxyFactory.getProxy(HelloService.class);
//执行的时候会进入invoke方法中去
String res2 = helloService.sayHello("lalala");
System.out.println("res2用了代理:"+res2);
}
}
结果:
9.进一步优化 实现注册中心获取数据,通过注册中心发现调用的接口,同时针对集群服务,需要负载均衡获取对应的url
**
* Author:Eric
* DATE:2023/8/4-20:38
* Decription: 将ip及端口具象化出来
*/
public class URL implements Serializable {
private String hostname;
private Integer port;
public URL(String hostname, Integer port) {
this.hostname = hostname;
this.port = port;
}
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
}
模拟注册中心需要共享数据zk,redis,nacos
**
* Author:Eric
* DATE:2023/8/2-21:58
* Decription: 模拟注册中心
*/
public class RemoteRegister {
// key 是接口的名字 value 是具体对应的具体访问的url列表
private static Map> REGISTER=new HashMap<>();
//注册接口及其实现类
public static void regist(String interfaceName,URL url){
List urls = REGISTER.get(interfaceName);
if(urls==null){
urls=new ArrayList<>();
}
urls.add(url);
REGISTER.put(interfaceName,urls);
//解决目前两个进程无法共享 注册的Map数据,这里将注册的数据保存到本机的 文件中
saveFile();
}
private static void saveFile() {
try {
FileOutputStream fileOutputStream = new FileOutputStream("/temp.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(REGISTER);
} catch (IOException e) {
e.printStackTrace();
}
}
//获得接口的实现类
public static List get(String interfaceName){
//读取数据的时候先从本机读
REGISTER=getFile();
List list = REGISTER.get(interfaceName);
return list;
}
private static REGISTER getFile() {
try {
FileInputStream fileIutputStream = new FileInputStream("/temp.txt");
ObjectInputStream objectinputStream = new ObjectInputStream(fileIutputStream);
REGISTER o = (REGISTER)objectinputStream.readObject();
return o;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
将接口名字,url存入map中,可能有集群服务,会有多个url,但每次只能访问一个,引入模拟的负载均衡
/**
* Author:Eric
* DATE:2023/8/4-20:47
* Decription: 负载均衡
*/
public class LoadBalance {
public static URL random(List list){
Random random = new Random();
int n = random.nextInt(list.size());
return list.get(n);
}
}
代理类拿数据的时候从注册中心拿,需要优化
public class ProxyFactory {
public static T getProxy(final Class interfaceClass){
//读取用户的配置,看用户用什么,这里用jdk
//得到代理对象
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class[]{interfaceClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),
method.getParameterTypes(), args);
//根据interfaceClass.getName 来判断该访问哪个ip 端口 可能是集群有多个ip
List list = RemoteRegister.get(interfaceClass.getName());
//拿到多个URL 选择其中一个 需要负载均衡 读取配置 第一次从注册中心拿 拿了后可以进行本地注册放到本地
//zk的监听机制 redis的发布订阅 可以共享数据的同时保证本地的数据实时更新
// 如果就这样代码 会拿到空,因为服务端和消费端是两个进程
URL url = LoadBalance.random(list);
String result = new HttpClient().send(url.getHostname(), url.getPort(), invocation);
return result;
}
});
}
}
同时注册及获取对应的数据这里将数据存入了文件,读取的时候也是读取的文件获取的配置的url信息,服务端可动态修改ip 端口
public class Provider {
public static void main(String[] args) {
//1.接口 实现类 2.通过网络来访问我这个服务 需要有服务来接收网络请求 例如:jetty/tomcat/netty 接收http 这里服务端需要有相应的服务
//本地注册 暴露接口出去
LoclRegister.regist(HelloService.class.getName(), HelloServerImpl.class);
//正常需要在注册中心注册 这样client根据interface类既可以找到对应的 ip 端口 Map<接口名,List>
//注册中心注册
URL url = new URL("localhost",8081);
RemoteRegister.regist(HelloService.class.getName(),url);
//可进行操作,如果不符合的接口请求 返回对应提示信息
//启动http服务 才能接收http请求
Httpserver httpserver = new Httpserver();
httpserver.start(url.getHostname(),url.getPort());
//3.需要双方协商好 交互的 内容 请求服务需要给 ->接口名字、方法名、方法参数类型列表(可能重载)、参数值列表、version 可加版本实现多版本功能
//这里不考虑多实现类多版本问题
//请求内容可封装为一个对象 双方统一序列化方式都用JDK
//请求过来后 会传接口名过来,那么到底对应的是哪个具体实现类,只有provider知道 provider需要有一个本地注册,将自己的实现类记录下来
}
}
消费端直接调用
public class Consumer {
public static void main(String[] args) {
//获取返回结果:hello:lily
// HelloService helloService 如果直接得到这个对象 就可以直接调用方法 如果要得到就需要将provider整体打jar包放进去,就成了
//单体应用了 所以可以用代理对象 得到helloservice的代理对象
HelloService helloService = ProxyFactory.getProxy(HelloService.class);
//执行的时候会进入invoke方法中去
String res2 = helloService.sayHello("lalala8081");
System.out.println("res2用了代理:"+res2);
}
}
结果
可以在代理类里面捕获异常,如果连接失败服务失败,返回对应数据
1.如何将服务方的接口暴露给客户端?可单独将服务端的接口及实现类 都放到一个maven项目中,打jar包,让客户端引入
2.引入注册中心,将接口和url存入