RPC是什么

Posted 踩踩踩从踩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RPC是什么相关的知识,希望对你有一定的参考价值。

前言

本篇文章主要介绍 RPC 理论,以及RPC常见得协议 ,工作原理,功能要点,以及 我们常见得RPC框架, 我们在公司开发开发rpc框架都是按照这样得思路设计出来得。因此了解这些,有助于我们快速上手框架,很有作用。

RPC概述

RPC,全称Remote Procedure Call, 即远程过程调用。

主要作用是屏蔽网络编程细节,实现调用远程方法就像调用本地方法(同一个进程中的方法)一样的体验。同时屏蔽底层网络通信的复杂性,让我们更加专注业务逻辑的开发。在osi网络通信模型中,RPC跨越了传输层和应用层,RPC使得开发 包括网络分布式在内得应用程序更加容易

 RPC的概念与技术早在1981年由Nelson提出。1984年,Birrell和Nelson把其用于支持异构型分布式系统间的通讯。Birrell的RPC 模型引入存根进程( stub) 作为远程的本地代理,调用RPC运行时库来传输网络中的调用。Stub和RPC runtime屏蔽了网络调用所涉及的许多细节,特别是,参数的编码/译码及网络通讯是由stub和RPC runtime完成的,因此这一模式被各类RPC所采用。由于分布式系统的异构性及分布式计算模式与计算任务的多样性,RPC作为网络通讯与委托计算的实现机制,在方法、协议、语义、实现上不断发展,种类繁多,其中SUN公司和开放软件基金会在其分布式产品中所建立和实用的RPC较为典型。

现在比较热门得框架 很多思想都是很早就提出来,但是当时不热门,现在网络达到条件 才热起来,包括DDD  领域驱动设计,nosql等等。

而过程是什么,业务处理。计算任务  等 就像本地方法一样调用远程得过程。

而远程过程调用需要通过网络,那么 肯定存在下面得问题,响应慢,并且不可靠。

RPC采用得 C/S模式 采用 client-service 结构 , 通过 request-response消息方式实现。

因此存在 有一定得耦合性。

 RPC的三个过程

通讯协议,寻址  联系地址, 数据 进行序列化。

 通过这三个过程,才能找到具体的RPC的服务端。

为什么要用rpc

业务发展到一个阶段,要将服务逻辑划分出来, 独立成服务,需要进行通信,新增的应用,要与service进行交互, 需要高效的通信方式, 这些场景 就是 服务化,微服务 

分布式系统架构、服务可重用、系统之间进行交互调用。

RPC协议

而这里就会想到的RMI   

而RMI 远程方法调用是OOP领域中RPC的一种具体实现,我们熟悉webservice、restful调用都是RPC 只是组织的方式 、消息协议不同。

 RPC的特性和使用场景

与MQ对比, 关键点在与异步  调用上面的差异,主要是这方面,做缓冲等等。    message queue把请求的压力保存下来,逐渐释放处理,按处理者按照自己的节奏来处理,message queue 引入一下新的节点,系统的可靠性会受message queue 结点的影响。 message queue 是异步单像的消息,发送者设计是不需要等待消息处理完成,所以对于有同步的请求, 用message queue 则变得麻烦。

1.希望同步得到结构 场合。rpc合适

2.希望使用简单。则rpc  rpc 操作基于接口。使用简单。 使用方式模拟本地调用。异步得方式编程比较复杂。

3.不希望发送端受限于处理端得速度,使用 message queue  

4.随着业务发展 有得处理端得处理量会成为瓶颈,会同步调用到异步消息改造。

 RPC的流程和协议

RPC都需要要一个存根

 1.客户端处理过程中调用Client stub 就像本地调用方法一样。

2.Client stub 将参数编组为消息,然后通过系统调用服务端进行发送消息、

3.客户端本地操作系统将消息从客户端机器发送服务端机器。

4.服务端操作系统作为收到消息传递给  service stub  

5.Server stub 解组消息为参数

6.Sever Stub 再调用服务端的过程,过程执行结果以反方向的相同步骤响应给客户端。

Stub (存根):分布式计算中的存根是一段代码,它转换在远程过程调用( RpC )期间 Clien 呀口 serve 泛间传递的参数。

需要处理的问题

 流程过程中

1 . client stub 、 Server stub 的开发;
2 .参数如何编组为消息,以及解组消息;
3 .消息如何发送;
4 .过程结果如何表示、异常情况如何处理;
5 .如何实现安全的访问控制。

这都是 在流程中遇到的问题。

然后会涉及到术语

  • Client 、 Server ' calls 、 replies 、 serverices , programs , procedures , version , marshalling (编组)、 unmarshalling (解组)
  • 一个网络服务由一个或多个远程程序集构成
  • 一个远程程序实现一个或多个远程过程过程
  • 过程的参数、结果在程序协议说明书中定义说明
  • ​​​二为兼容程序协议变更、一个服务端可能支持多个版本的远程程序

RPC协议

RPC 调用过程中需要将参数编组为消息进行发送,接收方需要解组消息为参数,过程处理结果同样需要经编组、解组。消息由哪些部分构成及消息的表示形式就构成了消息协议。 RpC 调用过程中采用的消息协议称为 RPC 协议。

RpC 是要做的事,RpC 协议规定请求、响应消息的格式。在TCP (网络传输控制协议)之上我们可以选用或自定义消息协议来完成我们的 RpC 消息交互。

我们可以选用通用的标准协议,如: http 、 https 也可根据自身的需要定义自己的消息协议!

RPC框架

封装好了参数编组、消息解组、底层网络通信的 RPC 程序开发框架,让我们可以直接在其基础上只需专注于我们的过程代码编写。

常用 RpC 框架 iava 领域:传统的 webservice 框架: Apache CXF 、 Apache AxisZ 、 lava 自带的 JAX 一 WS 等等。 webservice 框架大多基于标准的 SOAP 协议。新兴的微服务框架: Dubbo 、 springcloud 、 ApacheThrift 、 ICE 、 GRPC 等等

暴露服务

远程提供者需要以某种形式提供服务调用相关的信息,包括但不限于服务接口定义、数据结构、或者中间态的服务定义文件。例如 Facebook 的 Thrift的IDL 文件, Web service 的 WSDL 文件;服务的调用者需要通过一定的途径获取远程服务调用相关的信息。

 代理处理技术

服务调用者用的服务实际是远程服务的本地代理。说白了就是通过动态代理来实现。
 JaVa 里至少提供了两种技术来提供动态代码生成,一种是 Jdk 动态代理,另外一种是字节码生成。
动态代理相比字节码生成使用起来更方便,但动态代理方式在性能上是要逊色于直接的字节码生成的,而字节码生成在代码可读性上要差很多。
两者权衡起来,牺牲一些性能来获得代码可读性和可维护性显得更重要。

RPC通信

RPC框架通信与具体的协议无关。RPC可基于HTTP或TCP协议, Web Service 就是基于HTTP 协议的RPC,它具有良好的跨平台性,但其性能却不如基于TCP协议的RPC。

这里说一点,NIO并不一定比BIO更加高效,在不同的场景下有不同的使用方式。

RPC的序列化

两方面会直接影响RPC的性能,一是传输方式、二是序列化

这是最直接影响rpc框架的效率。

RPC框架的实现

 

1.客户端处理过程中调用Client stub(就像调用本地方法一样),传人参数;

2.Client stub将参数编组为消息,然后通过系统调用向服务端发送消息;

3.客户端本地操作系统将消息从客户端机器发送到服务端机器;

4.服务端操作系统将接收到的数据包传递给Server stub;

5.Server stub 解组消息为参数;

6.Sever stub再调用服务端的过程,过程执行结果以反方向的相同步骤响应给客户端。

用户在使用RPC框架开发过程需要做什么

1.定义过程定义接口

2.服务端实现过程

3.客户端使用生成的stub代理对象

具体的实现  由invoke实现。

Rpc客户端

里面方法包含

RegistCenter regist = new RegistCenter();
	private static final Client instance = new Client();

    public  static Client getInstance()
                return instance ;

	
	public List<ServiceResource> importService(String serviceName) 
		List<ServiceResource> resouces = regist.loadServiceResouces(serviceName);
		return resouces;
	
	
	public Object invoke(RpcRequest request) throws Throwable 
		List<ServiceResource> resources = Client.getInstance().importService(request.getClassName());
		int index = new Random().nextInt(resources.size());
		ServiceResource resource = resources.get(index);
		if(resource.getMethods().indexOf(request.getMethodName()) < 0) 
			throw new IllegalAccessException("服务方 ["+request.getClassName()+"] 没有提供"+request.getMethodName()+"方法");
		
		return invoke(request, resource.getHost(), resource.getPort());
	
	

通过网络IO,打开远端服务连接,将请求数据写入网络中,并获得响应结果。

/**
	 * 
	 * 
	 */
	public Object invoke(RpcRequest request, String host, int port) throws Throwable 
		// 打开远端服务连接
		Socket server = new Socket(host, port);
		
		ObjectInputStream oin = null;
		ObjectOutputStream oout = null;
		
		try 
			// 1. 服务端输出流,写入请求数据,发送请求数据
			oout = new ObjectOutputStream(server.getOutputStream());
			oout.writeObject(request);
			oout.flush();
			
			// 2. 服务端输入流,获取返回数据,转换参数类型
			// 类似于反序列化的过程
			oin = new ObjectInputStream(server.getInputStream());
			Object res = oin.readObject();
			RpcResponse response = null;
			if(!(res instanceof RpcResponse))
				throw new InvalidClassException("返回参数不正确,应当为:"+RpcResponse.class+" 类型");
			else
				response = (RpcResponse) res;
			
			
			// 3. 返回服务端响应结果
			if(response.getError() != null) // 服务器产生异常
				throw response.getError();
			
			return response.getResult();
		finally
			try 	// 清理资源,关闭流
				if(oin != null) oin.close();
				if(oout != null) oout.close();
				if(server != null) server.close();
			 catch (IOException e) 
				e.printStackTrace();
			
		
	

RegistCenter 

然后 下面的  类似dubbo的情况进行链接 注册中心找到 注册中心  注册中心代码

ZkClient client = new ZkClient("localhost:2181");
	
	private String centerRootPath = "/rpc";
	
	public RegistCenter() 
		client.setZkSerializer(new MyZkSerializer());
	

 注册的代码

public void regist(ServiceResource serviceResource) 
		String serviceName = serviceResource.getServiceName();
		String uri = JsonMapper.toJsonString(serviceResource);
		try 
			uri = URLEncoder.encode(uri, "UTF-8");
		 catch (UnsupportedEncodingException e) 
			e.printStackTrace();
		
		String servicePath = centerRootPath + "/"+serviceName+"/service";
		if(! client.exists(servicePath)) 
			client.createPersistent(servicePath, true);
		
		String uriPath = servicePath+"/"+uri;
		client.createEphemeral(uriPath);
	

  加载配置中心中服务资源信息

/**
	 *
	 * @param serviceName
	 * @return
	 */
	public List<ServiceResource> loadServiceResouces(String serviceName) 
		String servicePath = centerRootPath + "/"+serviceName+"/service";
		List<String> children = client.getChildren(servicePath);
		List<ServiceResource> resources = new ArrayList<ServiceResource>();
		for(String ch : children) 
			try 
				String deCh = URLDecoder.decode(ch, "UTF-8");
				ServiceResource r = JsonMapper.fromJsonString(deCh, ServiceResource.class);
				resources.add(r);
			 catch (UnsupportedEncodingException e) 
				e.printStackTrace();
			
		
		return resources;
	

绑定service的数据

private void sub(String serviceName, ChangeHandler handler) 
		
		String path = centerRootPath + "/"+serviceName+"/service";
		client.subscribeChildChanges(path, new IZkChildListener() 
			@Override
			public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception 
				handler();
			
		);
		client.subscribeDataChanges(path, new IZkDataListener() 
			@Override
			public void handleDataDeleted(String dataPath) throws Exception 
				handler();
			
			
			@Override
			public void handleDataChange(String dataPath, Object data) throws Exception 
				handler();
			
		);
	
	
	interface ChangeHandler 
		/**
		 * 发生变化后给一个完整的属性对象
		 * @param resource
		 */
		void itemChange(ServiceResource resource);
	

RpcServer 

这里面的服务端    中代码

  包含了  属性 服务的数据  
 

    RegistCenter registCenter = new RegistCenter();
    
    Map<String, Object> services;

start方法

启动指定的网络端口号服务,并监听端口上的请求数据。获得请求数据以后将请求信息委派给服务处理器,放入线程池中执行。

	/**
	 * @param port 监听端口
	 * @param clazz 服务类所在包名,多个用英文逗号隔开
	 */
	public void start(int port, String clazz) 
		ServerSocket server = null;
		try 
			// 1. 创建服务端指定端口的socket连接
			server = new ServerSocket(port);
			// 2. 获取所有rpc服务类
			services = getService(clazz);
			String host = InetAddress.getLocalHost().getHostAddress();
			exportService(host, port);
			
			// 3. 创建线程池
			Executor executor = new ThreadPoolExecutor(5, 15, 15, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
			while(true)
				// 4. 获取客户端连接
				Socket client = server.accept();
				// 5. 放入线程池中执行
				RpcServerHandler service = new RpcServerHandler(client, services);
				executor.execute(service);
			
		 catch (IOException e) 
			e.printStackTrace();
		finally
			//关闭监听
			if(server != null)
				try 
					server.close();
				 catch (IOException e) 
					e.printStackTrace();
				
		
	

这里导出service

public void exportService(String host, int port) 
		services.forEach((k, v)->
			ServiceResource resource = new ServiceResource();
			resource.setHost(host);
			resource.setPort(port);
			Class<?>[] interfaces = v.getClass().getInterfaces();
			resource.setServiceName(k);
			String methods = "";
			for(Class<?> face : interfaces) 
				if(k.equals(face.getName())) 
					Method[] ms = face.getDeclaredMethods();
					for(Method m : ms) 
						if(!"".equals(methods)) 
							methods += ",";
						
						methods += m.getName();
					
				
			
			resource.setMethods(methods);
			registCenter.regist(resource);
		);
		
	

  实例化所有rpc服务类,也可用于暴露服务信息到注册中心

/**。
	 * @param clazz 服务类所在包名,多个用英文逗号隔开
	 * @return
	 */
	public Map<String,Object> getService(String clazz) 
		try 
			Map<String, Object> services = new HashMap<String, Object>();
			// 获取所有服务类
			String[] clazzes = clazz.split(",");
			List<Class<?>> classes = new ArrayList<Class<?>>();
			for(String cl : clazzes)
				List<Class<?>> classList = getClasses(cl);
				classes.addAll(classList);
			
			// 循环实例化
			for(Class<?> cla : classes) 
				Object obj = cla.newInstance();
				services.put(cla.getAnnotation(Service.class).value().getName(), obj);
			
			return services;
		 catch (Exception e) 
			throw new RuntimeException(e);
		
	
	

 获取包的的service类

/**
	 * 获取包下所有有@Sercive注解的类
	 * @param pckgname
	 * @return
	 * @throws ClassNotFoundException
	 */
	public static List<Class<?>> getClasses(String pckgname) throws ClassNotFoundException 
		List<Class<?>> classes = new ArrayList<Class<?>>();
		// 找到指定的包目录
		File directory = null;
		try 
			ClassLoader cld = Thread.currentThread().getContextClassLoader();
			if (cld == null)
				throw new ClassNotFoundException("无法获取到ClassLoader");
			String path = pckgname.replace('.', '/');
			URL resource = cld.getResource(path);
			if (resource == null)
				throw new ClassNotFoundException("没有这样的资源:" + path);
			directory = new File(resource.getFile());
		 catch (NullPointerException x) 
			throw new ClassNotFoundException(pckgname + " (" + directory + ") 不是一个有效的资源");
		
		if (directory.exists()) 
			// 获取包目录下的所有文件
			String[] files = directory.list();
			File[] fileList = directory.listFiles();
			// 获取包目录下的所有文件
			for (int i = 0; fileList != null && i < fileList.length; i++) 
				File file = fileList[i];
				//判断是否是Class文件
				if (file.isFile() && file.getName().endsWith(".class")) 
					Class<?> clazz = Class.forName(pckgname + '.' + files[i].substring(0, files[i].length() - 6));
					if(clazz.getAnnotation(Service.class) != null)
						classes.add(clazz);
					
				else if(file.isDirectory()) //如果是目录,递归查找
					List<Class<?>> result = getClasses(pckgname+"."+file.getName());
					if(result != null && result.size() != 0)
						classes.addAll(result);
					
				
			
		 else
			throw new ClassNotFoundException(pckgname + "不是一个有效的包名");
		
		return classes;
	

 

用来解决 灵活的多种协议的方式。

 将协议方法扩展

对于网络层,知道服务地址 ,进行发送数据

 比较完整的客户端类图架构

以上是关于RPC是什么的主要内容,如果未能解决你的问题,请参考以下文章

rpc机制和实现过程

7.Go语言高并发与微服务实战 --- 远程过程调用 RPC

为啥在 REST 中没有存根?

Windows 异步 RPC C++ MIDL

存根的 gRPC 并发

rpc原理,和httpclient