【CSDN 编者按】对于开发人员来说,调用远程服务就像是调用本地服务一样便捷。尤其是在微服务盛行的今天,了解RPC的原理过程是十分有必要的。

作者 | Alex Ellis       译者 | 弯月

出品 | CSDN(ID:CSDNnews)

以下为译文:

计算机之间的通信方式多种多样,其中最常用的一种方法是远程过程调用(Remote Procedure Call,即RPC)。该协议允许一台计算机调用另一个计算机上的程序,就像调用本地程序一样,并负责所有传输和通信。

假设我们需要在一台计算机上编写一些数学程序,并且有一个判断数字是否为质数的程序或函数。在使用这个函数的时候,我们只需传递数字进去,就可以获得答案。这个函数保存在我们的计算机上。

很多时候,程序保存在本地非常方便调用,而且由于这些程序与我们其余的代码在一起,因此调用的时候几乎不会产生延迟。

但是,在有些情况下,将这些程序保留在本地也不见得是好事。有时,我们需要在拥有大量核心和内存的计算机上运行这些程序,这样它就可以检查非常大的数字。但这也不是什么难事,我们可以将主程序也放到大型计算机上运行,即使其余的程序可能并没有这种需求,质数查找函数也可以自由利用计算机上的资源。如果我们想让其他程序重用质数查找函数,该怎么办?我们可以将其转换成一个库,然后在各个程序之间共享,但是每一台运行质数查找库的计算机,都需要大量的内存资源。

如果我们将质数查找函数单独放在一台计算机上,然后在需要检查数字时与该计算机对话,怎么样呢?如此一来,我们就只需提高质数查找函数所在的计算机的性能,而且其他计算机上程序也可以共享这个函数。

这种方式的缺点是更加复杂。计算机可能会出现故障,网络也有可能出问题,而且我们还需要担心数据的来回传递。如果你只想编写一个简单的数学程序,那么可能无需担心网络状况,也不用考虑如何重新发送丢失的数据包,甚至不用担心如何查找运行质数查找函数的计算机。如果你的工作是编写最佳质数查找程序,那么你可能并不关心如何监听请求或检查已关闭的套接字。

这时就可以考虑远程过程调用。我们可以将计算机间通信的复杂性包装起来,然后在通信的任意一侧建立一个简单的接口(stub)。对于编写数学程序的人来说,看上去就像在调用同一台计算机上的函数;而对于编写质数查找程序的人来说,看上去就像是自己的函数被调用了。如果我们将中间部分抽象化,那么两侧都可以专心做好自己的细节,同时仍然可以享受将计算拆分到多台计算机的优势。

RPC调用的主要工作就是处理中间部分。它的一部分必须存在数学程序的计算机上,负责接受并打包参数,然后发送到另一台计算机。此外,在收到响应后,还需要解析响应,并传递回去。而质数查找函数计算机则必须等待请求,解析参数,然后将其传递给函数,此外,还需要获取结果,将其打包,然后再返回结果。这里的关键之处是数学程序和质数查找程序间,以及它们的stub之间都有一个清晰的接口。

更多详细信息,请参见 Andrew D. Birrell和Bruce Jay Nelson1 于1981年发表的论文《Implementing Remote Procedure Calls》。

从头编写RPC

下面,我们来试试看能不能编写一个RPC。

首先,我们来编写基本的数学程序。为了简单起见,我们编写一个命令行工具,接受输入,然后检查是否为质数。它有一个单独的方法is_prime,处理实际的检查。

// basic_math_program.c
#include <stdio.h>
#include <stdbool.h>// Basic prime checker. This uses the 6k+-1 optimization
// (see https://en.wikipedia.org/wiki/Primality_test)
bool is_prime(int number) {// Check first for 2 or 3if (number == 2 || number == 3) {return true;}// Check for 1 or easy modulosif (number == 1 || number % 2 == 0 || number % 3 == 0) {return false;}// Now check all the numbers up to sqrt(number)int i = 5;while (i * i <= number) {// If we've found something (or something + 2) that divides it evenly, it's not// prime.if (number % i == 0 || number % (i + 2) == 0) {return false;}i += 6;}return true;
}int main(void) {// Prompt the user to enter a number.printf("Please enter a number: ");// Read the user's number. Assume they're entering a valid number.int input_number;scanf("%d", &input_number);// Check if it's primeif (is_prime(input_number)) {printf("%d is prime\n", input_number);} else {printf("%d is not prime\n", input_number);}return 0;
}

这段代码有一些潜在的问题,我们没有处理极端情况。但这里只是为了说明,无伤大雅。

目前一切顺利。下面,我们将代码拆分成多个文件,is_prime 可供同一台计算机上的程序重用。首先,我们为 is_prime 创建一个单独的库:

// is_prime.h
#ifndef IS_PRIME_H
#define IS_PRIME_H#include <stdbool.h>bool is_prime(int number);#endif
// is_prime.c
#include "is_prime.h"// Basic prime checker. This uses the 6k+-1 optimization
// (see https://en.wikipedia.org/wiki/Primality_test)
bool is_prime(int number) {// Check first for 2 or 3if (number == 2 || number == 3) {return true;}// Check for 1 or easy modulosif (number == 1 || number % 2 == 0 || number % 3 == 0) {return false;}// Now check all the numbers up to sqrt(number)int i = 5;while (i * i <= number) {// If we've found something (or something + 2) that divides it evenly, it's not// prime.if (number % i == 0 || number % (i + 2) == 0) {return false;}i += 6;}return true;
}

下面,从主程序中调用:

// basic_math_program_refactored.c
#include <stdio.h>
#include <stdbool.h>#include "is_prime.h"int main(void) {// Prompt the user to enter a number.printf("Please enter a number: ");// Read the user's number. Assume they're entering a valid number.int input_number;scanf("%d", &input_number);// Check if it's primeif (is_prime(input_number)) {printf("%d is prime\n", input_number);} else {printf("%d is not prime\n", input_number);}return 0;
}

再试试,运行正常!当然,你也可以加一些测试:

下面,我们需要将这个函数放到其他计算机上。我们需要编写的功能包括:

  • 调用程序的 stub:

  • 打包参数

  • 传输参数

  • 接受结果

  • 解析结果

  • 被调用的 stub:

  • 接受参数

  • 解析参数

  • 调用函数

  • 打包结果

  • 传输结果

我们的示例非常简单,因为我们只需要打包并发送一个 int 参数,然后接收一个字节的结果。对于调用程序的库,我们需要打包数据、创建套接字、连接到主机(暂定 localhost)、发送数据、等待结果、解析,然后返回。调用程序库的头文件如下所示:

// client/is_prime_rpc_client.h
#ifndef IS_PRIME_RPC_CLIENT_H
#define IS_PRIME_RPC_CLIENT_H#include <stdbool.h>bool is_prime_rpc(int number);#endif

可能有些读者已经发现了,实际上这个接口与上面的函数库一模一样,但关键就在于此!因为调用程序只需要关注业务逻辑,无需关心其他一切。但实现就稍复杂:

// client/is_prime_rpc_client.c#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>#define SERVERPORT "5005"  // The port the server will be listening on.
#define SERVER "localhost"  // Assume localhost for now#include "is_prime_rpc_client.h"// Packs an int. We need to convert it from host order to network order.
int pack(int input) {return htons(input);
}// Gets the IPv4 or IPv6 sockaddr.
void *get_in_addr(struct sockaddr *sa) {if (sa->sa_family == AF_INET) {return &(((struct sockaddr_in*)sa)->sin_addr);} else {return &(((struct sockaddr_in6*)sa)->sin6_addr);}
}// Gets a socket to connect with.
int get_socket() {int sockfd;struct addrinfo hints, *server_info, *p;int number_of_bytes;memset(&hints, 0, sizeof hints);hints.ai_family = AF_UNSPEC;hints.ai_socktype = SOCK_STREAM;  // We want to use TCP to ensure it gets thereint return_value = getaddrinfo(SERVER, SERVERPORT, &hints, &server_info);if (return_value != 0) {fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(return_value));exit(1);}// We end up with a linked-list of addresses, and we want to connect to the// first one we canfor (p = server_info; p != NULL; p = p->ai_next) {// Try to make a socket with this one.if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {// Something went wrong getting this socket, so we can try the next one.perror("client: socket");continue;}// Try to connect to that socket.if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {// If something went wrong connecting to this socket, we can close it and// move on to the next one.close(sockfd);perror("client: connect");continue;}// If we've made it this far, we have a valid socket and can stop iterating// through.break;}// If we haven't gotten a valid sockaddr here, that means we can't connect.if (p == NULL) {fprintf(stderr, "client: failed to connect\n");exit(2);}// Otherwise, we're good.return sockfd;
}// Client side library for the is_prime RPC.
bool is_prime_rpc(int number) {// First, we need to pack the data, ensuring that it's sent across the// network in the right format.int packed_number = pack(number);// Now, we can grab a socket we can use to connect see how we can connectint sockfd = get_socket();// Send just the packed number.if (send(sockfd, &packed_number, sizeof packed_number, 0) == -1) {perror("send");close(sockfd);exit(0);}// Now, wait to receive the answer.int buf[1];  // Just receiving a single byte back that represents a boolean.int bytes_received = recv(sockfd, &buf, 1, 0);if (bytes_received == -1) {perror("recv");exit(1);}// Since we just have the one byte, we don't really need to do anything while// unpacking it, since one byte in reverse order is still just a byte.bool result = buf[0];// All done! Close the socket and return the result.close(sockfd);return result;
}

如前所述,这段代码需要打包参数、连接到服务器、发送数据、接收数据、解析,并返回结果。我们的示例相对很简单,因为我们只需要确保数字的字节顺序符合网络字节顺序。

接下来,我们需要在服务器上运行被调用的库。它需要调用我们前面编写的 is_prime 库:

// server/is_prime_rpc_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "is_prime.h"
#define SERVERPORT "5005"  // The port the server will be listening on.
// Gets the IPv4 or IPv6 sockaddr.
void *get_in_addr(struct sockaddr *sa) {
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);} else {
return &(((struct sockaddr_in6*)sa)->sin6_addr);}
}
// Unpacks an int. We need to convert it from network order to our host order.
int unpack(int packed_input) {
return ntohs(packed_input);
}
// Gets a socket to listen with.
int get_and_bind_socket() {
int sockfd;
struct addrinfo hints, *server_info, *p;
int number_of_bytes;
memset(&hints, 0, sizeof hints);hints.ai_family = AF_UNSPEC;hints.ai_socktype = SOCK_STREAM;  // We want to use TCP to ensure it gets therehints.ai_flags = AI_PASSIVE;  // Just use the server's IP.
int return_value = getaddrinfo(NULL, SERVERPORT, &hints, &server_info);
if (return_value != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(return_value));
exit(1);}
// We end up with a linked-list of addresses, and we want to connect to the
// first one we can
for (p = server_info; p != NULL; p = p->ai_next) {
// Try to make a socket with this one.
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
// Something went wrong getting this socket, so we can try the next one.perror("server: socket");
continue;}
// We want to be able to reuse this, so we can set the socket option.
int yes = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {perror("setsockopt");
exit(1);}
// Try to bind that socket.
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
// If something went wrong binding this socket, we can close it and
// move on to the next one.close(sockfd);perror("server: bind");
continue;}
// If we've made it this far, we have a valid socket and can stop iterating
// through.
break;}
// If we haven't gotten a valid sockaddr here, that means we can't connect.
if (p == NULL) {
fprintf(stderr, "server: failed to bind\n");
exit(2);}
// Otherwise, we're good.
return sockfd;
}
int main(void) {
int sockfd = get_and_bind_socket();
// We want to listen forever on this socket
if (listen(sockfd, /*backlog=*/1) == -1) {perror("listen");
exit(1);}
printf("Server waiting for connections.\n");
struct sockaddr their_addr;  // Address information of the client
socklen_t sin_size;
int new_fd;
while(1) {sin_size = sizeof their_addr;new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
if (new_fd == -1) {perror("accept");
continue;}
// Once we've accepted an incoming request, we can read from it into a buffer.
int buffer;
int bytes_received = recv(new_fd, &buffer, sizeof buffer, 0);
if (bytes_received == -1) {perror("recv");
continue;}
// We need to unpack the received data.
int number = unpack(buffer);
printf("Received a request: is %d prime?\n", number);
// Now, we can finally call the is_prime library!
bool number_is_prime = is_prime(number);
printf("Sending response: %s\n", number_is_prime ? "true" : "false");
// Note that we don't have to pack a single byte.
// We can now send it back.
if (send(new_fd, &number_is_prime, sizeof number_is_prime, 0) == -1) {perror("send");}close(new_fd);}
}

最后,我们更新一下我们的主函数,使用新的RPC库调用:

// client/basic_math_program_distributed.c
#include <stdio.h>
#include <stdbool.h>
#include "is_prime_rpc_client.h"
int main(void) {
// Prompt the user to enter a number.
printf("Please enter a number: ");
// Read the user's number. Assume they're entering a valid number.
int input_number;
scanf("%d", &input_number);
// Check if it's prime, but now via the RPC library
if (is_prime_rpc(input_number)) {
printf("%d is prime\n", input_number);} else {
printf("%d is not prime\n", input_number);}
return 0;
}

这个 RPC 实际的运行情况如下:

现在运行服务器,就可以运行客户端将质数检查的工作分布到其他计算机上运行!现在,程序调用 is_prime_rpc 时,所有网络业务都在后台进行。我们已经成功分发了计算,客户端实际上是在远程调用程序。

示例有待改进的方面

本文中的实现只是一个示例,虽然实现了一些功能,但只是一个玩具。真正的框架(例如 gRPC3)要复杂得多。我们的实现需要改进的方面包括:

  • 可发现性:在上述示例中,我们我们假定服务器在 localhost 上运行。RPC 库怎么知道将 RPC 发送到哪里呢?我们需要通过某种方式来发现可以处理此 RPC 调用的服务器在哪里。

  • RPC 的类型:我们的的服务器非常简单,只需处理一个 RPC 调用。如果我们希望服务器提供两个不同的RPC服务,比如 is_prime 和get_factors,那么该怎么办?我们需要一种方法来区分发送到服务器的两种请求。

  • 打包:打包整数很容易,打包一个字节更容易。如果我们需要发送一个复杂的数据结构,该怎么办?如果我们需要为了节省带宽而压缩数据,又该怎么办?

  • 自动生成代码:我们肯定不希望每次编写新的 RPC,都需要手动编写所有的打包和网络处理代码。理想情况下,我们只需定义一个接口,然后其余的接口都由计算机自动完成,并自动提供 stub。这里,我们需要考虑协议缓冲区等。

  • 多种语言:按照上面的思路,如果我们能够自动生成 stub,那么就可以考虑支持多种语言,如此一来,跨服务和跨语言的通信也只需调用一个函数。

  • 错误和超时处理:如果 RPC 失败怎么办?如果网络出现故障,服务器停止运行,wifi 掉线,该怎么办?我们需要考虑超时处理。

  • 版本控制:假设上述所有功能已全部实现,但你想修改某个正在多台计算机上运行的 RPC,那么该怎么办?

  • 其他有关服务器的注意事项:线程、阻塞、多路复用、安全性、加密、授权等等。

计算机科学就是要站在巨人的肩膀上,很多库已经为我们完成了大量工作。

原文链接:https://alexanderell.is/posts/rpc-from-scratch/

声明:本文由CSDN翻译,转载请注明来源。

60+专家,13个技术领域,CSDN 《IT 人才成长路线图》重磅来袭!

直接扫码或微信搜索「CSDN」公众号,后台回复关键词「路线图」,即可获取完整路线图!

从头开发一个 RPC 是种怎样的体验?相关推荐

  1. flutter 获取定位_从头开发一个Flutter插件(二)高德地图定位插件

    在上一篇文章从头开发一个Flutter插件(一)开发流程里具体介绍了flutter插件的具体开发流程,从创建项目到发布.接下来将会为Flutter天气项目开发一个基于高德定位sdk的flutter定位 ...

  2. 从头开发一个BurpSuite数据收集插件

    一段时间没写公众号了,最近写了个 burpsuite 数据收集的插件,于是想出一篇从头编写一个 burpsuite 插件的教程. ​ 这个插件的目的收集 burpsuite 请求中的数据,如请求中的子 ...

  3. 从零开发一个灰太狼游戏是什么样的体验?(建议收藏)

    极客江南: 一个对开发技术特别执着的程序员,对移动开发有着独到的见解和深入的研究,有着多年的iOS.Android.HTML5开发经验,对NativeApp.HybridApp.WebApp开发有着独 ...

  4. 企业是如何从头开发一个商业项目的?

    对于还没有参与过项目的同学,大都与企业项目开发的流程都感到特别的好奇!项目对于程序员来说像是自己的孩子,自己看着一步一步成熟,完善!最后到独立的运行!然后大多数程序员都如含泪老母亲一样,看这自己的项目 ...

  5. Android小玩意儿-- 从头开发一个正经的MusicPlayer(三)

    MusicService已经能够接收广播,通过广播接收的内容来做出相应的MediaPlayer对象的处理,包括播放,暂停,停止等,并当MediaPlayer对象的生命周期发生变化的时候,同样通过发送广 ...

  6. 使用 feapder 开发爬虫是一种怎样的体验

    这是「进击的Coder」的第 370 篇技术分享 作者:Boris1260 来源:程序员技术宝典 " 阅读本文大概需要 12 分钟. " 之前,我们写爬虫,用的最多的框架莫过于 s ...

  7. 在阿里巴巴做中后台开发,是一种怎样的体验?

    作者 | 牧瞳 本文经授权转载自阿里巴巴中间件(ID:Aliware_2018) 「开发全流程在线化」近些年来热度不断攀升,比如 AWS 在 C9 的实践.开源届比较出名的 TheiaJS,到后起之秀 ...

  8. 在霍格沃兹测试开发学社学习是种怎样的体验?

    霍格沃兹我怎么了解到的 我是河北某二本院校软工专业的学生,大三开始学校来了很多宣讲和实训的公司,都是为我们以后的职业发展做参考.学校有软件测试课程,有一次老师无意提到了霍格沃兹测试开发学社举办的高校& ...

  9. java atm模拟系统_Java RPC模式开发一个银行atm模拟系统

    采用rpc模式开发一个银行atm模拟系统. 系统主要提供一个服务Card,该服务接口可以提供登录.查询.取钱.存钱等功能.服务接口的设计和实现自定义. Atm客户端功能需求: 1.ATM可以实现用户登 ...

最新文章

  1. [转载] 人类智能PK人工智能——06 计算智能
  2. SAP 库存关联表信息
  3. 直白介绍卷积神经网络(CNN)
  4. Tensorflow 循环神经网络 文本情感分析概述02
  5. mysql基础5-数据的操作
  6. Ajax中async与cache参数
  7. 软件需求分析课堂讨论一
  8. linux 编译glibc
  9. 常用软件问题四则希望对大家有帮助
  10. CSR烧录工具csr单个蓝牙烧录小工具qcc300x烧录软件/CSR86xx烧写工具
  11. 【python中级】linux系统获得计算机网卡流量
  12. KRC跨境商城系 拍卖系统 竞拍系统 商城系统 虚拟支付源码
  13. Rust vs. Go:为什么他们在一起更好
  14. 打开计算机出现酷我音乐删不掉,删除 “我的电脑” 里的 “酷我音乐” 快捷方式...
  15. 深入浅出TensorFlow2函数——tf.data.Dataset.padded_batch
  16. 海洋地球科学开放数据库
  17. HCL Domino/Notes专业课程和认证体系介绍
  18. 大一学生一周十万字爆肝版C语言总结笔记
  19. 在背景色和背景图片同时存在的情况下,为什么还要设置背景色?
  20. 萨摩耶数科林建明:坚守“终局思维” 让金融科技发展行稳致远

热门文章

  1. 遍历hashmap 的四种方法
  2. struts2.0+spring intercepter 不能注入属性
  3. 敏捷开发智慧敏捷系列之四:每日立会开多久?
  4. @property 的属性class
  5. Java String类的intern()方法
  6. 前端使用工具sublime text 3下载
  7. C语言中全局变量、局部变量、静态全局变量、静态局部变量的区别 (转)
  8. ubuntu各大学更新源(教育网速度都很快)
  9. Linux用树形结构显示目录结构
  10. XX银行 机器学习平台使用情况访谈总结