文章目录
RPC介绍
RPC是远程过程调用(Remote Procedure Call)的缩写形式。
个人理解:相关框架会封装相应的http请求或者其他类似socker的请求,解析数据,得到结果。让调用远程方法就像本地方法一样easy~
相关框架 :dubbo,fegin…
Fegin原理
Fegin是Ribbon的扩展,封装实现远程调用接口
@FeignClient
参数
- name:使用Ribbon去服务注册中心查询相关服务的ip,进行调用
- url:适合本地调试,直接使用http去调用接口
至于原理,看到上面两个重要参数自然了解Fegin原理啦,这里不继续讲下去。
Dubbo理解
也是一个远程调用封装的框架,异步拿到结果,可以进行版本控制so on.
@Reference
这个是dubbo重要注解,标记在消费端。服务端会将服务暴露到zk,客户端进行消费。
个人理解
由于本人平时使用dubbo框架比较少,简单说下,也是使用动态代理的模式,采用异步框架进行相关的请求返回到结果集进行解析。
手写rpc框架
参考文章
首先观摩下大佬烟的文章https://www.cnblogs.com/rjzheng/p/8798556.html
以及一位大佬的文章(使用zk进行注册)https://blog.csdn.net/zhanglei082319/article/details/88930872
个人观后感
大佬烟那一篇使用的是socker,以及动态代理来实现的。(dubbo的影子)
另一篇文章则是类似http请求去获取结果(Fegin的影子)
时序图
分为两部分,服务端,注册中心zk,消费者。。。
个人的实战
参考了上面两篇文章,主要实现还是第二篇文章,从而进一步了解微服务的远程接口调用过程!
启动zk
这个就不详细说了,(windows)双击zkServer
maven
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.12</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
编写rpc-provider
注册到zk(模拟微服务的注册服务)
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author M
*/
@Component
public class Register {
@Value("${server.port}")
private String port;
//类似服务名
private static final String SERVER_PATH = "/provide";
private static final String ZK_ADDRESS = "127.0.0.1:2181";
private static final int ZK_TIMEOUT = 20000;
private static final String LOCAL_IP = "127.0.0.1";
/**
* 注册
*/
@PostConstruct
public void register() {
try {
ZooKeeper zooKeeper = new ZooKeeper(ZK_ADDRESS, ZK_TIMEOUT, (WatchEvent) -> {
});
Stat stat = zooKeeper.exists(SERVER_PATH, false);
if (stat == null) {
zooKeeper.create(SERVER_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
String path = LOCAL_IP + ":" + port;
System.out.println("注册的端口:"+port);
//创建短暂的可排序的子节点
zooKeeper.create(SERVER_PATH + "/instance", path.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
} catch (Exception e) {
e.printStackTrace();
}
}
}
写死了本地的ip,以及相关的配置,比如路径/provide
请求类
import com.example.demo.entity.Student;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProvideController {
@RequestMapping(value = "/provide",method = RequestMethod.GET)
public Student get(){
return new Student("大鸡腿",2);
}
}
编写rpc-consumer
读取注册服务
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
@Component
public class ZookListener {
private static final String SERVER_PATH = "/provide";
private static final String ZK_ADDRESS = "127.0.0.1:2181";
private static final int ZK_TIMEOUT = 20000;
private ZooKeeper zooKeeper;
private List<String> paths = new LinkedList<>();
@PostConstruct
public void init(){
try {
zooKeeper = new ZooKeeper(ZK_ADDRESS, ZK_TIMEOUT, (WatchEvent) -> {
//监听该节点的变化,如果节点出现变化,则重新获取节点下的ip和端口
if(WatchEvent.getType() == Watcher.Event.EventType.NodeChildrenChanged &&
WatchEvent.getPath().equals(SERVER_PATH)){
getChilds();
}
});
getChilds();
}catch (Exception e){
e.printStackTrace();
}
}
private void getChilds(){
List<String> ips =new LinkedList<>();
try {
//添加监听
List<String> childs = this.zooKeeper.getChildren(SERVER_PATH, true);
for(String child : childs){
byte[] obj = zooKeeper.getData(SERVER_PATH+"/"+child,false,null);
String path = new String(obj,"utf-8");
ips.add(path);
}
this.paths = ips;
}catch (Exception e){
e.printStackTrace();
}
}
public String getPath(){
if(paths.isEmpty()){
return null;
}
//这里我们随机获取一个ip端口使用
int index = new Random().nextInt(paths.size());
return paths.get(index);
}
}
读取之后保持到List,而且进行变化事件的监听。
getPath可以实现负载均衡
调用远程接口
import com.example.demo.entity.Student;
import com.example.demo.util.ZookListener;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
public class ConsumerController {
@Resource
private RestTemplate restTemplate;
@Resource
private ZookListener zookListener;
@RequestMapping(value = "/provide", method = RequestMethod.GET)
public Student get() throws Exception {
//从zookeeper中获取调用的ip
String path = zookListener.getPath();
if (StringUtils.isEmpty(path)) {
throw new Exception("对不起,产品暂时停止服务!");
}
System.out.println(path);
return restTemplate.getForObject("http://" + zookListener.getPath() + "/provide", Student.class);
}
}
这里使用RestTemplate进行远程调用,有没有Fegin的影子,西西~
手写框架相关改造
我们可以使用动态代理进行改造,可以让使用者不关心相关的调用,而直接使用。
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
动态代理
import com.example.demo.entity.Student;
import com.example.demo.util.ZookListener;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* @author M
*/
@Component
public class ProxyHandler implements InvocationHandler {
@Resource
private ZookListener zookListener;
@Resource
private RestTemplate restTemplate;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String path = zookListener.getPath();
if (StringUtils.isEmpty(path)) {
throw new Exception("对不起,产品暂时停止服务!");
}
System.out.println(path);
return restTemplate.getForObject("http://" + zookListener.getPath() + "/provide", Student.class);
}
}
当调用代理后会进入invoke方法,请求后将结果返回。
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Proxy;
@Component
public class RpcConsumer {
@Resource
ProxyHandler proxyHandler;
public <T> T getService(Class<T> clazz) {
return (T)Proxy.newProxyInstance(RpcConsumer.class.getClassLoader(), new Class<?>[] {clazz}, proxyHandler);
}
}
调用动态代理。
调用远程接口
import com.example.demo.entity.Student;
import com.example.demo.rpc.RpcConsumer;
import com.example.demo.service.DoSomeThingService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
RpcConsumer rpcConsumer;
@Test
public void contextLoads() {
DoSomeThingService doSomeThingService= rpcConsumer.getService(DoSomeThingService.class);
Student result=doSomeThingService.sayHello();
System.out.println(result);
}
}
Debug模式进去看看
当调用到方法的时候进入了代理类,进行相关http请求。
返回结果
{“name”:“大鸡腿”,“age”:2}
@Component理解
中性注解,为了让Spring扫描到该类,类似@Bean进行容器注入。