RPC (Remote Procedure Call) 遠程過程調用,遠端程序呼叫

https://www.zhihu.com/question/25536695

https://zh.wikipedia.org/wiki/%E9%81%A0%E7%A8%8B%E9%81%8E%E7%A8%8B%E8%AA%BF%E7%94%A8


RPC, 遠程過程調用直觀說法就是A通過網絡調用B的過程方法。

通信中的協議是你自己規定的,比如你可以規定說當A向B發送數字1, B就打印hello world, 並返回數字1給A, 如果發送數字2,B就打印hello, guy.並發送數字2給A。

這就是一個簡單的RPC示例,實際上natty就是乾這個的,只不過它提供一套框架給你,讓你可以定義自己的規則,實現B裡面的函數。

我們來想一想,當A要把數字1發送給B要怎麼辦呢,你需要用socket,B是server, A是client, B執行完以後再把1通過socket寫回即可。你可以用AUPE書中的例子很容易就構建出來。

問題到這裡,應該就結束了,事實上就是這麼簡單,那為什麼需要RPC框架呢?

當你給B發送1或者2就可以區分函數方法,但是你不可能寫100個函數打印100個字符,你需要在發送1的時候,再接著發送一個字符串,這樣B只要實現一個通用的print函數就夠了。

我們繼續思考,當你想發送一個數字比如65534給B的時候,我們知道socket是按照字節接受數據的,一個字節最大為255也就是0xff, 如果要發送65534你有兩種方法,第一把65534當作5個字符6,5,5,3,4然後讓socket接受5個字符。第二,你可以把65534變成二進制0xfffe, 這樣你可以先發送一個0xff再發送一個0xfe,也就是先發一個255再發一個254再拼裝。

另外在拼裝這255和254的時候,我們知道不同體系結構的CPU字節序是不一樣的,因此,你需要解決大端小端字節序的問題。否則對於一個32位機器上,65534作為一個int型可能會是0xfffe0000,或者0x0000feff,這取決於你如何發送和打包它。

對於相對複雜的RPC, 我們發送的一個數據包往往既包含字符串又包含數字,這樣我們就需要把他們切割開來然後分別解碼編碼。

這個部分就叫做序列化反序列化,在高級一點的RPC框架中,甚至可以做到把一個類從A扔給B.用到的還是這個方法,只是把類這個類型也放進去了。

對於腳本語言比如Lua,你甚至可以把編譯過後的字節碼發給遠程,然後在B用Lua虛擬機執行。

所以對於一個完整的RPC框架底層往往是socket搭配序列化反序列化的工作。

問題到這裡,一個RPC分析應該也結束了, 實際上要想把一個RPC做穩定了,還有幾個問題。

首先,要想讓一個RPC能夠更高效,往往做到讓A可以連續向B發送請求,這就要求在協議解析的時候區分兩個包,在TCP分包的時候這裡有個問題叫做半包和粘包。

另外B也無需在返回完函數1以後再返回函數2,這就要求在返回的時候,A能夠區分開來,這就要求在協議裡面加入session.

高效的寫法不但在編解碼和協議設計理念,還需要在編寫socket server時候,盡可能降低阻塞調用,用異步來做,這有一大推開源方案,比如libev.netty就是一個這樣異步的RPC通信框架。

當然,在實際項目中,我們只是需要解決實際問題,完全沒必要從頭構建,你可以選一個通信協議比如protobuf/JSON/msgpack/thrift等等再找他相關的RPC庫使用即可。





RPC:远程调用。通过RPC框架,使得我们可以像调用本地方法一样地调用远程机器上的方法:

1、本地调用某个函数方法

2、本地机器的RPC框架把这个调用信息封装起来(调用的函数、入参等),序列化(json、xml等)后,通过网络传输发送给远程服务器

3、远程服务器收到调用请求后,远程机器的RPC框架反序列化获得调用信息,并根据调用信息定位到实际要执行的方法,执行完这个方法后,序列化执行结果,通过网络传输把执行结果发送回本地机器

4、本地机器的RPC框架反序列化出执行结果,函数return这个结果

服務調用端 (本地機器)

服務提供端 (遠程機器)

單一 RPC 無法實現 push,即推送服務。理由是,RPC 是client 調用 server獲取數據,是一個完整的過程,實現不了server調用client。解決方案:讓client 既可以調用server上的RPC服務,反之client本身也成為一個RPC服務讓Server來調用。


比如說 Java Netty 是在TCP(Socket)層對nio進行封裝的框架,在RPC框架中可用於解決網絡傳輸問題。

現在流行的微服務框架(dubbo、spring cloud等),實際上就是各種各樣的RPC框架

1. Netty只是網絡通信框架,目的是讓你用最少的代碼構建出足夠支撐網絡通信的功能。

2.完成RPC 需要兩個協議: 對象序列化協議 和 調用控制協議



==>
早期單機時代,一台電腦上運行多個進程,大家各干各的,老死不相往來。假如A進程需要一個畫圖的功能,B進程也需要一個畫圖的功能,程序員就必須為兩個進程都寫一個畫圖的功能。這不是整人麼?於是就出現了IPC(Inter-process communication,單機中運行的進程之間的相互通信)。 OK,現在A既然有了畫圖的功能,B就調用A進程上的畫圖功能好了,程序員終於可以偷下懶了。

到了網絡時代,大家的電腦都連起來了。以前程序只能調用自己電腦上的進程,能不能調用其他機器上的進程呢?於是就程序員就把IPC擴展到網絡上,這就是RPC(遠程過程調用)了。現在不僅單機上的進程可以相互通信,多機器中的進程也可以相互通信了。

要知道實現RPC很麻煩呀,什麼多線程、什麼Socket、什麼I/O,都是讓咱們普通程序員很頭疼的事情。於是就有牛人開發出RPC框架(比如,CORBA、RMI、Web Services、RESTful Web Services等等)。

OK,現在可以定義RPC框架的概念了。簡單點講,RPC框架就是可以讓程序員來調用遠程進程上的代碼一套工具。有了RPC框架,咱程序員就輕鬆很多了,終於可以逃離多線程、Socket、I/O的苦海了。

Netty、Mina是遊戲行業做服務器開發的Java程序員用的比較多的PRC框架。據說互聯網公司用的也比較多。這兩行業都有高並發量的、長連接、分佈式、異步通訊、大數據量等特點。 Netty這種RPC框架封裝和優化了Java NIO和異步網絡編程的一些繁瑣的細節,一方面可以讓開發者專注於業務邏輯的實現,一方面只需要調用Netty封裝的API就可以很快編寫出高性能的服務器。



==>
先说说原理。

本地过程调用
RPC就是要像调用本地的函数一样去调远程函数。在研究RPC前,我们先看看本地调用是怎么调的。假设我们要调用函数Multiply来计算lvalue * rvalue的结果:
1 int Multiply(int l, int r) {
2    int y = l * r;
3    return y;
4 }
5 
6 int lvalue = 10;
7 int rvalue = 20;
8 int l_times_r = Multiply(lvalue, rvalue);
那么在第8行时,我们实际上执行了以下操作:
  1. 将 lvalue 和 rvalue 的值压栈
  2. 进入Multiply函数,取出栈中的值10 和 20,将其赋予 l 和 r
  3. 执行第2行代码,计算 l * r ,并将结果存在 y
  4. 将 y 的值压栈,然后从Multiply返回
  5. 第8行,从栈中取出返回值 200 ,并赋值给 l_times_r
以上5步就是执行本地调用的过程。(20190116注:以上步骤只是为了说明原理。事实上编译器经常会做优化,对于参数和返回值少的情况会直接将其存放在寄存器,而不需要压栈弹栈的过程,甚至都不需要调用call,而直接做inline操作。仅就原理来说,这5步是没有问题的。)

远程过程调用带来的新问题
在远程调用时,我们需要执行的函数体是在远程的机器上的,也就是说,Multiply是在另一个进程中执行的。这就带来了几个新问题:
  1. Call ID映射。我们怎么告诉远程机器我们要调用Multiply,而不是Add或者FooBar呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用Multiply,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <--> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
  2. 序列化和反序列化。客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
  3. 网络传输。远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。
所以,要实现一个RPC框架,其实只需要把以上三点实现了就基本完成了。
Call ID映射可以直接使用函数字符串,也可以使用整数ID。映射表一般就是一个哈希表。
序列化反序列化可以自己写,也可以使用Protobuf或者FlatBuffers之类的。
网络传输库可以自己写socket,或者用asio,ZeroMQ,Netty之类。

最后,有兴趣的可以看一个小而精的RPC库 tinyrpc(hjk41/tinyrpc),对于理解RPC如何工作很有好处。



==>
我们在做一个访问量不大的项目的时候,一台服务器部署上一个应用+数据库也就够了.

那么访问量稍微大一点之后呢,为了解决用户反馈的卡,反应慢的情况,我们就上集群.架设nginx,部署多个服务,由nginx负责把请求转发到其他服务上,这样就解决了用户说的卡慢问题.

过了一段时间之后呢,我们发现数据库已经扛不住了,应用服务完好,数据库有时候宕机. 那这个时候呢,我们就上数据库读写分离,再架设几台数据库服务器,做主从,做分库分表. 然后数据库也不宕机了,服务又恢复了流畅.

又过了一段时间,公司事业增增日上,服务访问量越来越高,且大部分都是查询, 吸取之前宕机且为了办证数据库的健壮性,我们这个时候又加上了缓存, 把用户高频次访问的数据放到缓存里.

后来,项目功能越来越多,整个项目也愈发庞大,修改一个类就需要全盘上传,切换nginx重启,这样的发布流程越来越长,越来越繁杂.然后我们开始把模块拆分,用户信息分个项目,订单系统分一个项目.这样就达到了,用户模块代码修改的时候,只需要更新用户信息服务就好了.但是还是需要切换顶层的nginx.把要重启的服务的流量切到可用服务上. 这个时候我们就想到了RPC

那么RPC解决了什么呢? 所有的服务在启动的时候注册到一个注册机里面,然后顶层处理在接收到nginx的请求时,去注册机找一个可用的服务,并调用接口. 这样子呢,在不加新功能的时候,顶层处理服务我们就不需要动了? 那修改了用户信息项目的时候,我们只需要一个个更新用户信息项目的服务群就好了?

这样的话,无论是扩展还是服务的健壮性都妥妥的了?


==> 你妈妈电脑系统出问题了,你使用qq远程助手,帮她修复系统,这中间,你做的每一步都可以理解为一个rpc调用,本质上就是想操作你本机的代码一样调用远程机器上的代码


程式語言編年史

程式語言編年史原文 下面這張圖片描繪了整個程式語言的歷史。包括各種程式語言的發明人、程式語言的特點和適用領域、被什麼網站或公司使用等 (檢視 完整高清圖 )。 之所以會有那麼多不同的程式語言是因為設計程式語言的初衷不同、對語言學習曲線的追求不同、不同程式之間的執行成本差異...