谈谈RPC中的异步调用设计
RPC(远过程调用)在分布式系统中是很常用的基础通讯手段,核心思想是将不同进程之间的通讯抽象为函数调用,基本的过程是调用端通过将参数序列化到流中并发送给服务端,服务端从流中反序列化出参数并完成实际的处理,然后将结果序列化后返回给调用端。通常的RPC由接口形式来定义,接口定义服务的名字,接口方法定义每个请求的输入参数和返回结果。RPC内部的序列化、网络通讯等实现细节则由框架来完成,对用户来说是完全透明的。之前我使用.net开发过一套轻量级的分布式框架(PPT看这里,视频看这里),这套框架经过2年多的持续开发和改进已经运用到数款产品中(包括网络游戏和分布式应用),取得了不错的效果,等未来框架成熟后会考虑开源,本文讨论的RPC基于这套框架展开。
通常我们的函数调用都是同步的,也就是调用方在发起请求后就能得到结果(成功返回结果失败则抛出异常),中间不能去干其他事情,与这种模式对应的RPC称之为请求-响应式模式。请求-响应式的优点在于时序清晰,逻辑简单,和普通的函数调用完全等价。比如我们可以这样定义RPC接口:
[Protocol(ID=)]
public interface ICalculate
{
[DispId()]
int Add(int p1, int p2);
}
客户端就可以像这样使用接口:
var calculate = new ICalculateProxy();//ICalculateProxy为框架生成的接口代理类
calculate.Connect(url);
var result = calculate.Add(, );
但是在分布式中这种模式的缺点也是非常的明显,第一个问题是网络通讯的延迟会严重的制约请求-响应式RPC的响应速度,使得请求吞吐量无法满足性能需要,大量的CPU时间会阻塞在等待请求的响应上;第二个问题是请求-响应式只有由客户端向服务端发起请求,服务端不能主动向客户端发送事件通知,也就是缺乏一种callback机制。
针对请求-响应式的缺点我们可以用双向通讯机制来改进,首先去掉请求的返回值,所有的方法请求都定义为无返回结果,这样调用方在发出请求之后就可以继续干后面的事情了,而不需要再等待服务返回结果。同时针对服务接口定义一个Callback接口用于服务端向客户端发送请求结果和事件通知,这样服务器就可以主动向客户端发送消息了。这种RPC模式可以称之为双向会话式,接口可以这样定义:
[Protocol(ID=), Callback(typeof(ICalculateCallback))]
public interface ICalculate
{
[DispId()]
void Add(int p1, int p2);
} public interface ICalculateCallback : IServiceCallback
{
[DispId()]
void OnAdd(int result);
}
服务端可以这样实现服务接口:
public class CaculateService : ICaculateImpl //这里ICaculateImpl为框架生成的服务实现接口
{
ICaculateImpl.Add(Session session, int p1, int p2)
{
var result = p1 + p2;
session.Cllback.OnAdd(result);
}
}
双向会话式解决了请求的异步处理以及服务器的双向通讯问题,但是也给调用者带来了一些不便,例如上例中如果调用方发起多个Add请求,在收到OnAdd消息后如何将结果与请求关联起来呢?一种解决方案是在Add请求中多加一个request id参数,服务器在处理完Add之后将request id放到OnAdd方法中和结果一起传给客户端,客户端根据request id来关联请求与结果。这种手工处理的方式代码写起来很麻烦,那么有没有一种更好的RPC模式来解决这个问题呢?这就是下面给大家介绍的支持异步调用的RPC设计。
异步调用的主要设计思想是在双向会话式的基础上让调用方通过一个回调函数来获得请求的结果,而不再通过Callback接口来获得结果。采用回调函数的好处在于我们可以使用闭包来隐式的关联请求和响应之间的上下文,这样就不需要显式的传递request id来手工关联上下文了。并且服务器仍然可以通过Callback接口向客户端主动发送消息,保留了原来双向通讯的优点。但是需要注意的是由于请求在服务器上可能是异步执行的,所以服务器不保证请求的响应是按顺序返回的,这可能造成一些隐含的乱序问题,需要客户端在调用时特别注意。如果响应需要严格的按照请求顺序返回客户端,那么服务端需要同步处理请求,或者引入队列机制对异步的响应进行排队有序返回响应。
之前的ICalculate就可以这样定义:
[Protocol(ID=), Callback(typeof(ICalculateCallback))]
public interface ICalculate
{
[DispId(), Async]
void Add(int p1, int p2, Action<int> OnResult, Action<int,string> OnError = null);
}
用Async这个标签表示这个请求为异步请求,调用者用OnResult回调函数来接收请求的结果,OnError则为返回错误的回调函数,如果调用者不关心错误返回,那么可以不传递OnError,而在IServiceCallback的OnError方法中接收错误信息。
调用者可以很方便的使用闭包来处理结果,同时隐藏异步的实现细节,像这样:
void TestAdd(ICalculateProxy calculate, int p1, int p2)
{
calculate.Add(p1, p2, result => MessageBox.Show(string.Format("{0} + {1} = {2}", p1, p2, result), (errCode, errMsg) => MessageBox.Show("Add failed:" + errMsg));
}
服务器端的实现是这样的:
public class CaculateService : ICaculateImpl
{
ICaculateImpl.Add(Session session, int p1, int p2, ICaculate_AddAsyncReply reply)
{
try
{
var result = p1 + p2;
reply.OnResult(result);
}
catch(OverflowException e)
{
reply.OnError(-, e.Message);
}
}
}
ICaculate_AddAsyncReply为框架生成的返回异步结果的对象,有一个OnResult和一个OnError方法。有了这个reply对象之后,服务器的请求处理也可以实现异步处理,客户端请求不需要在请求函数里一次完成,而是可以放到其他线程或者异步方法中处理,稍后在通过reply向客户端返回结果。
下面我们来看看框架在背后为我们做的一些实现细节,首先是客户端的Proxy:
//在Proxy中使用一个RequestContext结构保存请求的上下文信息,上下文中记录某个请求的唯一id,在调用时一起发送到服务器:
struct RequestContext
{
public int reqId;
public Delegate OnResult;
public Action<int, string> OnError; public RequestContext(int id, Delegate onResult, Action<int, string> onError)
{
reqId = id;
OnResult = onResult;
OnError = onError;
}
} //服务器返回响应之后proxy就找出reqId对应的请求上下文,然后调用对应的回调函数传递结果
void OnAddReply(BinaryStreamReader __reader)
{
int reqId;
int ret;
__reader.Read(out reqId);
__reader.Read(out ret);
if(ret == )
{
int p0;
__reader.Read(out p0);
RequestContext ctx = PopAsyncRequest(reqId);
var __onResult = ctx.OnResult as Action<int>;
__onResult(p0);
}
else
{
RequestContext ctx = PopAsyncRequest(reqId);
string msg;
__reader.Read(out msg);
if(ctx.OnError != null)
ctx.OnError(ret, msg);
else
_handler.OnError(ret, msg);
}
}
服务端的一些实现细节:
//框架生成请求对应的异步响应类
public class ICaculate_AddAsyncReply : AsyncReply
{
public ICaculate_AddAsyncReply(int reqId, Connection conn)
{
_reqId = reqId;
_connection = conn;
} public void OnError(int error, string msg)
{
var stream = new BinaryStreamWriter();
stream.Write();
stream.Write(_reqId);
stream.Write(error);
stream.Write(msg);
_connection.Write(stream.BuildSendBuffer());
}
public void OnResult(int result)
{
var stream = new BinaryStreamWriter();
stream.Write();
stream.Write(_reqId);
stream.Write();
stream.Write(result);
_connection.Write(stream.BuildSendBuffer());
}
}
框架生成的Stub类将收到的请求数据进行解析然后调用具体服务类来处理请求:
void AddInvoke(ICaculateImpl __service, Session __client, BinaryStreamReader __reader)
{
int p1;
int p2;
int __reqId;
__reader.Read(out __reqId);
__reader.Read(out p1);
__reader.Read(out p2);
var reply = new ICaculate_AddAsyncReply(__reqId, __client.Connection);
try
{
__service.Add(__client, p1, p2, reply);
}
catch(ServiceException e)
{
reply.OnError(e.ErrCode, e.Message);
Log.Info("Service Invoke Failed. clientId:{0} error message:{1}", __client.ID, e.Message);
}
catch(Exception e)
{
reply.OnError((int)ServiceErrorCode.Generic, "generic service error.");
Log.Error("Generic Service Invoke Failed, clientId:{0} error message:{1}\nCall Stack: {2}", __client.ID, e.Message, e.StackTrace);
}
}
由于完整的框架代码比较庞大,所以上面只贴了关键部分的实现细节。从实现细节我们可以看到,框架实际上也是通过request id来关联请求和响应函数之间的上下文的,但是通过代码生成机制隐藏了实现的细节,给使用者提供了一种优雅的抽象。
总结:在双向会话式的RPC基础上,引入了一种新的异步请求调用模式,让调用者可以通过闭包来方便的异步处理请求的响应结果,同时服务器端的请求处理也可以实现异步处理。
谈谈RPC中的异步调用设计的更多相关文章
- PHP中实现异步调用多线程程序代码
本文章详细的介绍了关于PHP中实现异步调用多线程方法,下面我们以给1000个用户发送一封推荐邮件,用户输入或者导入邮件账号了提交服务器执行发送来讲述. 比如现在有一个场景,给1000个用户发送一封推荐 ...
- C#中的异步调用及异步设计模式(一)
近期项目中使用了不少异步操作,关于“异步”做个总结.总结的内容大部分都来自于MSDN,还有一些自己的心得. 关于“异步”的使用可分为:使用层面和类库设计层面,细分如下: 一.使用异步方式调用同步方法( ...
- C#中的异步调用及异步设计模式(三)——基于事件的异步模式
四.基于事件的异步模式(设计层面) 基于事件的C#异步编程模式是比IAsyncResult模式更高级的一种异步编程模式,也被用在更多的场合.该异步模式具有以下优点: · ...
- C#中的异步调用及异步设计模式(二)——基于 IAsyncResult 的异步设计模式
三.基于 IAsyncResult 的异步设计模式(设计层面) IAsyncResult 异步设计模式通过名为 BeginOperationName 和 EndOperationName 的两个方法来 ...
- jquery中ajax异步调用接口
之前写过一个原始的.无封装的页面,没有引入任何外部js,直接实例化Ajax的XmlRequest对象去异步调用接口,参见Ajax异步调用http接口后刷新页面,可对比一下. 现在我们用jquery包装 ...
- Spring Boot中实现异步调用之@Async
一.什么是异步调用 “异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行:异步调用指程序在顺序执行时,不等待异步调用 的语句返回结果 ...
- python 协程(单线程中的异步调用)(转廖雪峰老师python教程)
协程,又称微线程,纤程.英文名Coroutine. 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用. 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在 ...
- Dubbo中CompletableFuture异步调用
使用Future实现异步调用,对于无需获取返回值的操作来说不存在问题,但消费者若需要获取到最终的异步执行结果,则会出现问题:消费者在使用Future的get()方法获取返回值时被阻塞.为了解决这个问题 ...
- 钉钉开发中post异步调用问题
最近项目上在做钉钉开发中,经常会遇到使用post方式调用钉钉内部的方法(微信也有一样),这里涉及到跨域的post调用,但跨域一般都是用jsonp格式,而这个格式只支持get方式.尝试了挺多方法都没有返 ...
随机推荐
- js--找字符串中出现最多的字符
在一个字符串中,如 'zhaochucichuzuiduodezifu',我们要找出出现最多的字符.本文章将详细说明方法思路. 先介绍两个string对象中的两个方法:indexOf()和charAt ...
- ECharts外部调用保存为图片操作及工作流接线mouseenter和mouseleave由于鼠标移动速度过快导致问题解决办法
记录两个项目开发中遇到的问题,一个是ECharts外部调用保存为图片操作,一个是workflow工作流连接曲线onmouseenter和onmouseleave事件由于鼠标移动过快触发问题. 一.外部 ...
- AngularJS中的指令全面解析(转载)
说到AngularJS,我们首先想到的大概也就是双向数据绑定和指令系统了,这两者也是AngularJS中最为吸引人的地方.双向数据绑定呢,感觉没什么好说的,那么今天我们就来简单的讨论下AngularJ ...
- jeecg环境搭建20160707
1.首页修改位置:src/main/webapp/webpage/main 2.tomcat45秒超时启动修改,open打开servers项目,在右上角处的timeouts参数修改: 3.eclips ...
- iOS之There was an internal API error错误
There was an internal API error. 错误原因:把Product Name作为程序名称,程序名称错乱 解决方法:检查Product Name, 不要包含中文以及特殊字符.在 ...
- Android中通过线程实现更新ProgressDialog(对话进度条)
作为开发者我们需要经常站在用户角度考虑问题,比如在应用商城下载软件时,当用户点击下载按钮,则会有下载进度提示页面出现,现在我们通过线程休眠的方式模拟下载进度更新的演示,如图(这里为了截图方便设置对话进 ...
- iOS 疑难杂症 — — Swift debugger 无法在控制台 po 变量值的问题
前言 这个问题出现有好几个月了,一直没弄,以为是 Xcode 的问题后面版本升级应该就能好所以就不管了,今天心情好顺便查了一下. 声明 欢迎转载,但请保留文章原始出处:) 博客园:http://w ...
- [转]iOS开发中的火星坐标系及各种坐标系转换算法
iOS开发中的火星坐标系及各种坐标系转换算法 源:https://my.oschina.net/u/2607703/blog/619183 其原理是这样的:保密局开发了一个系统,能将实际的坐标转 ...
- 使用DOTNETZIP过滤并压缩相对目录
业务要求: 压缩某个文件夹及其子目录 压缩时只压缩指定的文件类型,如cshtml 压缩后保持相对目录 找了很久,没有直接的DEMO,最后尝试通过以下代码完成 示例演示了只压缩cshtml和js,同 ...
- 深入解析SQL Server并行执行原理及实践(上)
在成熟领先的企业级数据库系统中,并行查询可以说是一大利器,在某些场景下他可以显著的提升查询的相应时间,提升用户体验.如SQL Server, Oracle等, Mysql目前还未实现,而Postgre ...