RPC是远程调用系统简称,它允许程序调用运行在另一台计算机上的过程,就像调用本地的过程一样。RPC 实现了网络编程的“过程调用”模型,让程序员可以像调用本地函数一样调用远程函数。最近在做的也是远程调用过程,所以通过重新梳理RPC来整理总结一下。
项目来源:
GitHub - qicosmos/rest_rpc: modern C++(C++11), simple, easy to use rpc framework
目录
RPC指的是计算机A的进程调用另外一台计算机B的进程,A上的进程被挂起,B上被调用的进程开始执行,当B执行完毕后将执行结果返回给A,A的进程继续执行。调用方可以通过使用参数将信息传送给被调用方,然后通过传回的结果得到信息。这些传递的信息都是被加密过或者其他方式处理。这个过程对开发人员是透明的,因此RPC可以看作是本地过程调用的一种扩展,使被调用过程不必与调用过程位于同一物理机中。

RPC可以用于构建基于B/S模式的分布式应用程序:请求服务是一个客户端、而服务提供程序是一台服务器。和常规和本地的调用过程一样,远程过程调用是同步操作,在结果返回之前,需要暂时中止请求程序。
RPC的优点:
以ARM环境为例,我们拆解本地调用的过程,以下面代码为例:
int selfIncrement(int a)
{
return a + 1;
}
int a = 10;
selfIncrement(a);
当执行到selfIncrement(a)时,首先把a存入寄存器R0,之后转到函数地址selfIncrement,执行函数内的指令 ADD R0,#1。跳转到函数的地址偏移量在编译时确定。
但是如果这是一个远程调用,selfIncrement函数存在于其他机器,为了实现远程调用,请求方和服务方需要提供需要解决以下问题:
1. 网络传输。
本地调用的参数存放在寄存器或栈中,在同一块内存中,可以直接访问到。远程过程调用需要借助网络来传递参数和需要调用的函数 ID。
2. 编解码
请求方需要将参数转化为字节流,服务提供方需要将字节流转化为参数。
3. 函数映射表
服务提供方的函数需要有唯一的 ID 标识,请求方通过 ID 标识告知服务提供方需要调用哪个函数。
以上三个功能即为 RPC 的基本框架所必须包含的功能。
一次 RPC 调用的运行流程大致分为如下七步,具体如下图所示。

服务端存根和客户端存根可以看做是被封装起来的细节,这些细节对于开发人员来说是透明的,但是在客户端层面看到的是 “本地” 调用了 selfIncrement() 方法,在服务端层面,则需要封装、网络传输、解封装等等操作。因此 RPC 可以看作是传统本地过程调用的一种扩展,其使得被调用过程不必与调用过程位于同一物理机中。
RPC 的目标是做到在远程机器上调用函数与本地调用函数一样的体验。 为了达到这个目的,需要实现网络传输、序列化与反序列化、函数映射表等功能,其中网络传输可以使用socket或其他,序列化和反序列化可以使用protobuf,函数映射表可以使用std::function。
lambda与std::function内容可以看:
lambda 表达式和 std::function 的功能是类似的,lambda 表达式可以转换为 std::function,一般情况下,更多使用 lambda 表达式,只有在需要回调函数的情况下才会使用 std::function。
#include <iostream>
#include <memory>
#include <thread>
#include <functional>
#include <cstring>
class RPCClient
{
public:
using RPCCallback = std::function<void(const std::string&)>;
RPCClient(const std::string& server_address) : server_address_(server_address) {}
~RPCClient() {}
void Call(const std::string& method, const std::string& request, RPCCallback callback)
{
// 序列化请求数据
std::string data = Serialize(method, request);
// 发送请求
SendRequest(data);
// 开启线程接收响应
std::thread t([this, callback]() {
std::string response = RecvResponse();
// 反序列化响应数据
std::string result = Deserialize(response);
callback(result);
});
t.detach();
}
private:
std::string Serialize(const std::string& method, const std::string& request)
{
// 省略序列化实现
}
void SendRequest(const std::string& data)
{
// 省略网络发送实现
}
std::string RecvResponse()
{
// 省略网络接收实现
}
std::string Deserialize(const std::string& response)
{
// 省略反序列化实现
}
private:
std::string server_address_;
};
int main()
{
std::shared_ptr<RPCClient> client(new RPCClient("127.0.0.1:8000"));
client->Call("Add", "1,2", [](const std::string& result) {
std::cout << "Result: " << result << std::endl;
});
return 0;
}
这段代码定义了RPCClient类来处理客户端的请求任务,用到了lambda和std::function来处理函数调用,在Call中使用多线程技术。main中使用智能指针管理Rpcclient类,并调用了客户端的Add函数。
127.0.0.1为本地地址,对开发来说需要使用本地地址自测,端口号为8000,需要选择一个空闲端口来通信。
下面是服务端的实现
#include <iostream>
#include <map>
#include <functional>
#include <memory>
#include <thread>
#include <mutex>
// 使用第三方库实现序列化和反序列化
#include <boost/serialization/serialization.hpp>
#include <boost/serialization/map.hpp>
using namespace std;
// 定义RPC函数类型
using RPCCallback = std::function<std::string(const std::string&)>;
class RPCHandler {
public:
void registerCallback(const std::string& name, RPCCallback callback) {
std::unique_lock<std::mutex> lock(mtx_);
callbacks_[name] = callback;
}
std::string handleRequest(const std::string& request) {
// 反序列化请求
std::map<std::string, std::string> requestMap;
std::istringstream is(request);
boost::archive::text_iarchive ia(is);
ia >> requestMap;
// 查找并调用对应的回调函数
std::string name = requestMap["name"];
std::string args = requestMap["args"];
std::unique_lock<std::mutex> lock(mtx_);
auto it = callbacks_.find(name);
if (it == callbacks_.end()) {
return "Error: Unknown function";
}
RPCCallback callback = it->second;
return callback(args);
}
private:
std::map<std::string, RPCCallback> callbacks_;
std::mutex mtx_;
};
int main() {
RPCHandler rpcHandler;
// 注册回调函数
rpcHandler.registerCallback("add", [](const std::string& args) {
std::istringstream is(args);
int a, b;
is >> a >> b;
int result = a + b;
std::ostringstream os;
os << result;
return os.str();
});
rpcHandler.registerCallback("sub", [](const std::string& args) {
std::istringstream is(args);
int a, b;
is >> a >> b;
int result = a - b;
std::ostringstream os;
os << result;
return os.str
});
// 创建处理请求的线程
std::thread requestThread([&]() {
while (true) {
std::string request;
std::cin >> request;
std::string response = rpcHandler.handleRequest(request);
std::cout << response << std::endl;
}
});
requestThread.join();
return 0;
}
上面的代码实现了一个简单的C++ RPC服务端。主要实现了以下功能:

注意,这套代码是最简单的RPC机制,只能调用本地的资源,他还存在以下缺点:
下面我们一步一步完善它。
下面是 RPCHandler 类中加入错误处理的代码示例:
class RPCHandler {
public:
// 其他代码...
std::string handleRequest(const std::string& request) {
// 反序列化请求
std::map<std::string, std::string> requestMap;
std::istringstream is(request);
boost::archive::text_iarchive ia(is);
ia >> requestMap;
// 查找并调用对应的回调函数
std::string name = requestMap["name"];
std::string args = requestMap["args"];
std::unique_lock<std::mutex> lock(mtx_);
auto it = callbacks_.find(name);
if (it == callbacks_.end()) {
return "Error: Unknown function";
}
RPCCallback callback = it->second;
try {
return callback(args);
} catch (const std::exception& e) {
return "Error: Exception occurred: " + std::string(e.what());
} catch (...) {
return "Error: Unknown exception occurred";
}
}
};
上面的代码在 RPCHandler 类的 handleRequest 函数中加入了错误处理的代码,它使用了 try-catch 语句来捕获可能发生的异常。如果找不到对应的函数或发生了异常,会返回错误信息。这样,如果请求格式不正确或函数不存在,服务端将会返回相应的错误信息。
加入网络连接不需要动服务端的实现,只需要在main里创造套接字去链接就好:
int main()
{
io_context ioc;
ip::tcp::acceptor acceptor(ioc, ip::tcp::endpoint(ip::tcp::v4(), 8080));
RPCHandler rpcHandler;
// 注册函数
rpcHandler.registerCallback("add", [](const std::string& args) {
std::istringstream is(args);
int a, b;
is >> a >> b;
int result = a + b;
std::ostringstream os;
os << result;
return os.str();
});
rpcHandler.registerCallback("sub", [](const std::string& args) {
std::istringstream is(args);
int a, b;
is >> a >> b;
int result = a - b;
std::ostringstream os;
os << result;
return os.str();
});
// 等待连接
while (true) {
ip::tcp::socket socket(ioc);
acceptor.accept(socket);
// 创建线程处理请求
std::thread requestThread([&](ip::tcp::socket socket) {
while (true) {
// 读取请求
boost::asio::streambuf buf;
read_until(socket, buf, '\n');
std::string request = boost::asio::buffer_cast<const char*>(buf.data());
request.pop_back();
// 处理请求
std::string response = rpcHandler.handleRequest(request);
// 发送响应
write(socket, buffer(response + '\n'));
}
}, std::move(socket));
requestThread.detach();
}
return 0;
}
这是一个使用Boost.Asio库实现的RPC服务端代码示例。它使用了TCP协议监听8080端口,等待客户端的连接。当有客户端连接时,创建一个新线程来处理请求。请求和响应通过网络传输。
使用并发和异步机制,忽略重复代码,实现如下:
class RPCHandler {
public:
// ...
void handleConnection(ip::tcp::socket socket) {
while (true) {
// 读取请求
boost::asio::streambuf buf;
read_until(socket, buf, '\n');
std::string request = boost::asio::buffer_cast<const char*>(buf.data());
request.pop_back();
// 使用并行执行处理请求
std::vector<std::future<std::string>> futures;
for (int i = 0; i < request.size(); i++) {
futures.emplace_back(std::async(std::launch::async, &RPCHandler::handleRequest, this, request[i]));
}
// 等待所有请求处理完成并发送响应
for (auto& f : futures) {
std::string response = f.get();
write(socket, buffer(response + '\n'));
}
}
}
};
这样,请求会被分成多个部分并行处理,可以利用多核 CPU 的优势提高服务端的并发性能。
main():
int main() {
io_context ioc;
ip::tcp::acceptor acceptor(ioc, ip::tcp::endpoint(ip::tcp::v4(), 8080));
RPCHandler rpcHandler;
// 注册函数
rpcHandler.registerCallback("add", [](const std::string& args) {
std::istringstream is(args);
int a, b;
is >> a >> b;
int result = a + b;
std::ostringstream os;
os << result;
return os.str();
});
rpcHandler.registerCallback("sub", [](const std::string& args) {
std::istringstream is(args);
int a, b;
is >> a >> b;
int result = a - b;
std::ostringstream os;
os << result;
return os.str();
});
// 创建线程池
boost::thread_pool::executor pool(10);
// 等待连接
while (true) {
ip::tcp::socket socket(ioc);
acceptor.accept(socket);
// 将请求添加到线程池中处理
pool.submit(boost::bind(&RPCHandler::handleConnection, &rpcHandler, std::move(socket)));
}
return 0;
}
在 main 函数中可以使用 boost::thread_pool::executor 来管理线程池,在线程池中提交任务来处理请求。这里的线程池大小设置为10,可以根据实际情况调整。
在其中使用了重试机制来保证客户端能够重新连接服务端:
class RPCClient {
public:
RPCClient(const std::string& address, int port) : address_(address), port_(port), socket_(io_context_) {
connect();
}
std::string call(const std::string& name, const std::string& args) {
// 序列化请求
std::ostringstream os;
boost::archive::text_oarchive oa(os);
std::map<std::string, std::string> request;
request["name"] = name;
request["args"] = args;
oa << request;
std::string requestStr = os.str();
// 发送请求
write(socket_, buffer(requestStr + '\n'));
// 读取响应
boost::asio::streambuf buf;
read_until(socket_, buf, '\n');
std::string response = boost::asio::buffer_cast<const char*>(buf.data());
response.pop_back();
return response;
}
private:
void connect() {
bool connected = false;
while (!connected) {
try {
socket_.connect(ip::tcp::endpoint(ip::address::from_string(address_), port_));
connected = true;
} catch (const std::exception& e) {
std::cerr << "Error connecting to server: " << e.what() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
}
std::string address_;
int port_;
io_context io_context_;
ip::tcp::socket socket_;
};
在这个示例中,当连接服务端失败时,客户端会在一定的时间间隔后重试连接,直到成功连接上服务端为止。
服务端需要处理大量的请求,这部分的实现是可以独立拎出来长篇大论的,在此贴出其他大神的帖子吧。
至此,我们逐步完善了RPC,在最简单的RPC基础上加入了网络连接、加入错误处理、增强了并发访问的功能、并加入了容错机制,但是对于一个可以让客户正常使用的RPC来说,这还远远不够,我本人也是实力有限,仅仅能读懂或者解析部分RPC的设计动机及原理,要详细介绍RPC光写这些是远远不够的。工作中一套RPC附加其他功能需要一个团队忙活差不多两个月,我仅仅在其中负责测试工具开发和代码生成,所以有不妥的地方请读者谅解,有错的地方请指出必将改正。好梦!!!
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
我的工作要求我为某些测试自动生成电子邮件。我一直在四处寻找,但未能找到可以快速实现的合理解决方案。它需要在outlook而不是其他邮件服务器中,因为我们有一些奇怪的身份验证规则,我们需要保存草稿而不是仅仅发送邮件的选项。显然win32ole可以做到这一点,但我找不到任何相当简单的例子。 最佳答案 假设存储了Outlook凭据并且您设置为自动登录到Outlook,WIN32OLE可以很好地完成此操作:require'win32ole'outlook=WIN32OLE.new('Outlook.Application')message=
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识
//1.验证返回状态码是否是200pm.test("Statuscodeis200",function(){pm.response.to.have.status(200);});//2.验证返回body内是否含有某个值pm.test("Bodymatchesstring",function(){pm.expect(pm.response.text()).to.include("string_you_want_to_search");});//3.验证某个返回值是否是100pm.test("Yourtestname",function(){varjsonData=pm.response.json
在前面两节的例子中,主界面窗口的尺寸和标签控件显示的矩形区域等,都是用C++代码编写的。窗口和控件的尺寸都是预估的,控件如果多起来,那就不好估计每个控件合适的位置和大小了。用C++代码编写图形界面的问题就是不直观,因此Qt项目开发了专门的可视化图形界面编辑器——QtDesigner(Qt设计师)。通过QtDesigner就可以很方便地创建图形界面文件*.ui,然后将ui文件应用到源代码里面,做到“所见即所得”,大大方便了图形界面的设计。本节就演示一下QtDesigner的简单使用,学习拖拽控件和设置控件属性,并将ui文件应用到Qt程序代码里。使用QtDesigner设计界面在开始菜单中找到「Q