RPC作品开发记录

代码请访问:https://github.com/dalerkd/KD_RPC

历时9天,规划跨度3周。代码量4k+。C++语言编写。带自动测试的多线程RPC框架->的开发记录。

前言

RPC即Remote Procedure Calls,一种中间件,提供一种类似于调用本地服务的方式来调用远程服务。
已经不少这样的框架,再实现的意义有限。但一则我考虑到自己需要一个作品来锤炼一下各项技能,二者还想到了一些好主意,我选择的突破方向是

  1. 提高RPC调用和本地函数调用的近似度。
    不需要初始化之类的过程。接口要尽量贴近C语言服务。
  2. 动态更新接口
    API和RPC框架分离,不需要事先知道。

有了以上两个目标,我也就不再迟疑了。

以下是开发记录:

第一次规划

客户端机制

同步调用

轮循 事件栈,触发事件

1. 发送数据
2. push事件栈
3. 进入等待状态
4. 取回结果

这么一想还真不如异步。
效率上。

此外要注意的是:同步回调的参数.
?是否兼容char &i指针?
因为这两者意味着数据可能会被修改到外面。
我们可以做限制,也就是服务器端的程序会做一种限制,只能使用我们的接口。
如果我们不做限制,就需要检测到这种形式,即数据是否有变动,有的话更新数据。当然这是C++的一种形式。

异步调用

轮循

一种特殊情况就是不管结果的异步,一种方法是用空指针做异步。
FindFirstFile(BYTE* strpX,bool async,callback*);
其中async决定是否采用异步调用。

异步调用流程是:

即使用户不关心结果也照样 push 异步回调栈,这样有助于调试?这也取决于服务端是否会发送不关心结果的数据过来:

struct {
char* 回调函数
char* Buffer//存放取回数据
size_t BufferSize
}

此外我们得关心一下,如果返回内容超过Buffer的事情。如果需要多次存取怎么办?这和Recv一样。

方案有:
1. 异常
2. 返回一个长度表示存储空间不足。(系统Recv的实现)
3. 先返回长度,再申请Buffer。(PeFileEdit中数据库接口实现)

想想就像本地使用一样,那么本地使用这些函数怎么用呢?

多参数

对于多参数一个主意是,通过结构体指针传入。这样极大地方便了我们生成动态dll。
因为参数都是一个普通指针。

struct ArgvS{
unsigned char Argc Number;


}

FindFirstFile( strpX);

分别细节

客户端

原理

根据网络或者本地需求 动态生成dll接口,让用户使用api的名字变得容易,和本地调用类似:

  1. 调用初始化
  2. LoadLibrary()
  3. GetProcAddress()

形式

能否自动生成头文件?

难点在于参数的获取和不出错
如何精准获取参数?我觉得依靠编写DLL时撰写的头文件是合适的。
关键点在于第一个参数结构体的获取。

其他

用三参数形式调用,只是名字不同罢了。
functionName(BYTE* argv,async =true ,callback* = nullptr);

我们看第一个参数BYTE* argv
这就是参数列表,它指向一个结构体:

struct argc{
    char a;
    int    b;
_IN_    BYTE* x;
    int    counteof(x);
_OUT_   BYTE* y;
    int counteof(y);
}

每个指针放数据量的多少是为什么呢?
因为我们要传输数据过去,不放长度发过去不知道多少。

指针在远程的改变还能不能算?算-把它看做本地调用。

也就是指针能不能回传东西给我们?

在同步的情况下,如果废弃指针这种回传方式,我认为这是不OK的。
另外const char*这种形式我就不管了。反正服务器接受到的数据,如果检测到其被修改了,我们就回传这种修改。

难点-本地要做指针解析操作

本地要按照参数列表进行指针的内容的复制和发送。

另外一种已经解决的问题是:&a
我们不支持哈哈。因为我们使用的是参数指针,所以直接规避了这种变态形式,666。

回调与指针参数

指针的回改作用只在同步调用下有效

只有异步的时候指针参数的回改才是有效的。因为此时调用函数会立即返回。而回改本身是一个同步操作。
而如果它作为一个异步操作将会带来太多的问题,所以这样是合理的。

优点

  • 动态性
    我们的接口数据可以随时从服务器更新。

  • 兼容性
    和本地调用没有任何差别。

回调的数据谁来销毁

thread()
{
    p = new(len);
    strcpy;
    被调用者(p,len);
    delete()

}

服务端

调用格式

可以与客户端一致的格式,建议如此才能保证测试方便哈:
functionName(BYTE* argv/*async =true这个参数不暴露出来*/ ,callback* = nullptr);
也可以使用普通的编译格式:
functionName(argv1,argv2 ...,/*async =true*/ ,callback* = nullptr);

两者各有益处。但没有根本差别。

依然是通过LoadLibrary()
GetProc形式获得函数调用地址。

同步调用

需要
- 检查指针内容是否被修改 (非必须)
- 返回值的接收(必须)

异步调用

需要
- 异步调用服务端指定函数使用某值
该值回被接收相当于返回值了。但是该值可以是指针

回调函数原形 :

void Function(char* p,int len);

再总结

  • 同步调用返回结果的途径:
    • 指针
    • 直接返回值
  • 异步调用返回结果的途径:
    • 指针

我有点感觉这比较困难,但是我的设计很棒:)我有能力实现它。
我得估算一下任务量和优先任务级。

一个建议是使用尽量相同的机制,在开始的时候不要处理特殊情况,比如:
不要优化掉空的回调。
特殊情况后处理。

任务优先级

基本关键词

自动测试,错误处理,基本框架搭建,功能扩展。

错误处理:“最早暴露”策略。

基本框架搭建

本地客户端基本功能分为:

  • 初始化器
    • 建立通讯
    • 修改DLL
  • DLL
    • 接收调用信息,打包调用信息,传输给通讯机制。
    • 按照返回信息,按照不同的调用方式返回给他们。

服务端

  • 启动
    • 搜集接口信息
    • 建立连接
    • 发送接口信息
  • 按照任务安排参数,调用任务相关函数
  • 获取返回信息并回传
    • 获取返回值 检查 指针是否改动,如果改动,回传数据
    • 获取回调信息

框架层次

类分:
- 网络层:处理网络连接
- 协议层:自定义协议解析与生成
- 参数处理层:参数获取相关
- 等待机制:。

测试

分网络测试和本地测试

我们要做到远程调用和本地调用无区别。即 加入网络的测试,和直接 使用远程DLL在本地执行的过程无根本区别。

实际RPC的流程:
1. 初始化
2. LoadLibrary(),GetProcAddress()

本地直接调用服务DLL的流程:
1. LoadLibrary(),GetProcAddress()

所以要做到远程DLL服务接口和本地调用接口一致

注意

必须分类。不想看到一堆垃圾。

一些额外的情况

版本号不匹配的情况
握手信息
服务中断的服务转移
服务器地址的动态获取
心跳包及其过滤
……

任务列表

具体设计:
    实施顺序是:
        1. 不带网络层的客户端直接使用服务端的DLL测试 - 定结构体格式    √ 
        2. 假定已经有存在.h文件并能解析且导出函数已经被初始化完毕:
            用代码尝试打通 “异步和同步区分与分别注册”过程封装类。
            **写测试函数。如果不想一直重看这里的话。
        3. “数据格式打包”“数据格式解包”过程封装类。要客户端和服务端都能用的。
            **写测试函数。
共114. 格式解析
            **写测试函数。
        4. 回调参数捕获-服务端
        4. 参数修改侦测-服务端
        4. 参数回写-客户端
        4. 服务端初始化程序
        4. 客户端初始化程序-导出函数修改
        5. 完成除网络外的所有内容 并能够在单机运行。
        6. 网络层

部分难题集

FreeLibrary不能退出的问题

引出了:静态全局指针的问题。发现

静态类指针在全局初始化后;
在dllmain()中做添加操作,在其他导出函数中再次获取,却发现其使用了新的指针,与全局指针不同的对象。

新探索-支持本地函数零成本迁移

对方会压多少参数进来?
对方压入的参数是指针还是变量?

DLLmain能干嘛

能够在dllmain里做所有的初始化的事情?
Yes:就放一个dll.最好是这个.
No:就放一个另一个dll中

struct中指针到底偏移几何?

你传输struct,是不是得知道它多大呀?
难道得靠服务器传?还是自己解析?
自己解析很不靠谱啊。

其中的指针,用户怎么给我们标记它们是指针呀。
即使直接解析.h,那么我们也非常难知道参数结构体中指针的偏移。

解决之道

如果是动态语言就解决问题了。
我们一则不想限制用户必须将指针放在指定偏移,二则不想麻烦用户告知我们偏移在哪里,其实麻烦一下也没有关系的。

这应该在服务器编译时搞定。

一种方法是通过指针找回去?
能否通过指针宏把SizeOf算出来?

看一下别人是怎么做的吧

我们上面遭遇的问题是由于抽象层级低导致的。

基于偏移问题。
两种解决方案:
1. 预处理
先执行一遍获取struct偏移
2. 指针宏
在定义结构体的指针处做手脚来计算偏移。

struct
{
    int a;
    int b;
    char* p;
    int p_len;

}

struct first
{
    int    x;
    char    y;
}
就是这么吊
struct second

{
}

结果

还是限制吧,限制并标记字符串。这样就能把问题解决在服务端。
还是动态语言好呀。

解决方案

反正就是得限制一下下啦,不然没有办法推测啦。指定字符串数量:
eg:数量为:3
那么就是

struct xx{
    char* a;
    int a_len;
    char* b;
    int b_len;
    char* c;
    int c_len;

    char y;
    int z;
    ......
}

即前面必须是三个指针参数和对应的实际长度。这样我们通过一个3即可了解所有偏移信息。
容忍少量限制,才能更多自由。

心得者 新得也

经历了设计项目到实施,在实施中重构到最终完成的过程。

单元测试理念

依照单元测试设计的理念,本项目在设计时就完成了验证最终结果的模块。
而相关模块的确定,明确了何时结束。并且在初期引入了较为方便调试的支持,在中期加强了相关调试输出机制。在后期在提出更多需求时,对调试输出做了改善。

同样依照是单元测试的理念,本项目最终以通过全部测试为阶段性终结。
这带给我一些不一样的开发经历。

类的深入使用带来的全面益处

而类的使用,也变得更加随心所欲。
本项目是类的集合,通过设计自动管理类。

类的三大特点:继承,多态,封装。
用类的封装来隔离代码和数据。
用类的继承来提炼公共模块。
用类的多态来设计方便的共用代码。
服务端和客户端公用模块达到8个。
为了解决多线程服务的问题,对栈和队列都进行了多线程封装。
将转流代码在设计中期重构为公用代码,也是为了重用和集中管理的目的。

以上举动大幅度提升了代码的重用。

如设计指针的统一管理方案,解决了不定量指针需要释放的问题。这样的例子还有很多,经此之战后我对类的运用越来越熟练了。

对抗疲劳-引入新设计替代不好的设计的策略

人总是会累和疲劳的,我在此项目开发过程中遵照“将难题留给明天,而不是今天决定不做”的方法。来解决以上问题。

通过单独的设计验证工程,使得新技术在稳定后才进入主项目。

对待BUG的态度

我知道做为一个中型项目,BUG的出现是难以避免的,那么以下成为我在设计程序时首要考虑的问题:
1. 如何从机制上更少的出现BUG
2. 如何在出现BUG后更好定位
3. 如何在代码调整时避免引入新BUG

涉及到了流格式结构设计

经验+2;

只依赖接口的网络层

不在开始时设计网络层,因为我设计过网络层。对设计一个能用的网络层有足够信心。

而网络层和应用层不应当有关系,两者应当只依赖接口,通过设计让网络层和其他层分离彻底。

使得我可以在中后期用一个下午时间设计了基于MailSlot的网络层。

来自版本管理工具的保障

当然Github的使用,是以上成功的保证。
跨度3周,纯写代码用了8天。总共4k+行。

可改善之处

  • 依赖于Windows平台。由于大量Windows函数的使用。
    而我不清楚C代码库中的替代函数。
  • RPC不支持类
    或者说没有提供类的流化程序。而我并不明确清楚这一过程如何实施。
  • 没有提供高效的网络模块和协商机制等
    网络模块是RPC的核心,我偏重于本地用户体验,但是缺乏高性能网络模块的实际设计经验。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值