客户端架构设计的简单总结
(原文连接:
http://www.cppblog.com/weiym/archive/2014/07/26/207819.html) 我们知道,客户端是相对服务端而言的,客户端程序相对普通应用程序,主要是增加了网络通讯功能。在这个移动和云存储的年代,大部分终端应用程序都有网络通讯功能, 所以都可以称为客户端。常见的客户端如浏览器,IM客户端, 网络会议客户端,邮件客户端,微博和微信客户端等...通过观察,我们会发现所有的客户端基本是大同小异,都会包括一些相同的功能组件, 下面简单例举下:通讯协议层
既然客户端都有网络功能,就会涉及到通讯方式和数据格式以及协议, 这三者不是完全独立,而是有机统一的。
首先说通讯方式,常见的通讯方式包括TCP,UDP, P2P和http(s), 很多时候我们不会用单一的通讯方式,而是多种通讯方式的结合。比如说TCP端口被封,走不通时,我们会转成尝试http(s)。IM中聊天文本走的是TCP, 由服务器转发,但是2个客户端之间的文件传输我们可能走的又是P2P了, 多个人之间的语音聊天, 我们走的又是UDP了。
其次说数据格式,常见的数据格式包括二进制编码,开源序列化协议和文本格式。
二进制一般是自定义的私有格式,通常对数值,我们会转成大头端,对字符串我们会用UTF8 编码,因为没有冗余数据,它的优点是不会浪费带宽;主要缺点是有硬编码的味道,不好扩充。
开源序列化协议这里主要是指google的protocal buffer, 现在很多公司都在用, 很多人基于它开发了自己的RPC框架。主要优点是数据小,使用简单而高效。
文本格式主要是指xml和json. 相对来说xml比较清晰和容易扩充,但是冗余数据比较多。json借助javascript对它语言层次的支持,感觉主要是前端人员使用的比较多。
最后再说协议, 协议和我们的应用相关联。比如邮件客户端,当然是走SMTP和POP3了; IM客户端的话,一般走XMPP了; 网络会议的话,可以走ITU的T.120协议, 也可以RFC 6501定义的XCON, 信令走SIP, 数据走RTP等。
通信协议层是整个客户端网络事件驱动的引擎,它可能会比较简单,也可能会很复杂。如果是基于XMPP的IM, 它可能会比较简单,因为基本上只需要一层文本协议的封包和解包就可以了。 当如果是基于T.120网络会议客户端,就会比较复杂,它数据包走的自定义的二机制格式,按照T.120协议的建议, 在通讯协议层又分了3层:TP, MCS和GCC。TP层主要封装数据传输的方式, 可以让上层无差别的区分TCP和http(s)。 MCS层主要提供多点传输功能, 它抽象出通道(channel)这个概念, 让不同session的数据进行逻辑隔离, 上层用户可以同时加入不同的通道来进行一对一和一对多的数据收发,并且通道中的数据有不同的优先级, 还有令牌这个机制。我们也可以在MCS层对数据进行加密和压缩, 还可以对上层的大数据包进行切包等。 GCC层主要封装会议的最基本逻辑,比如创建会议和加入session数据包的格式封装等, 让上层可以通过API调用而不用关心协议要求的数据包格式。不同的数据包会工作在不同的层次, 比如心跳包可能在底层TP层就被拦截了,它不要再往上层发,因为上面不用关心这个; 而有些数据包,则需要从底层往上层按照整个协议栈层层转发,当然每层都会剥离掉自己的协议头, 直至上层用户数据到达它的最终用户。
总之,通讯协议层封装了客户端和服务端的通讯方式及协议格式, 让上层用户不用关心底层的通信机制, 而只关注应用的接口事件。理论上我们可以在上层应用不做大调整的前提下,直接将网络会议客户端中的T.120协议成基于SIP的XCON。
功能组件
一个客户端程序通常是由很多功能模块组成,模块按功能来说可以分为基础组件和应用组件。
基础组件为应用组件提供的基础设施,基础组件是可以在不同的项目中重复使用的(比如界面控件库,2D渲染引擎Skia, 跨平台的网络和线程库等)。
应用组件通常和我们当前的特定应用程序相关,比如我们的网络会议客户端包含的桌面共享模块, 文档共享模块,视频音频模块,文本聊天模块等。
应用模块本身分为带界面和无界面两种情况, 带界面的情况下我们通常会给组件提供一个容器窗口的句柄, 让组件自己在内部组织自己的界面。这种带界面的组件通常会为逻辑和界面的分离带来麻烦,在Window上实现一些半透明和动画效果也很难。 比如我们想提供跨平台的SDK来封装逻辑,这时我们会更倾向让应用组件采用无界面的模式,组件在跨平台层只封装逻辑和提供数据, 而把数据发到最上层界面层后再统一处理。
对功能组件我们的设计原则是尽量保持独立和可复用,最好能以仿COM方式动态升级而不用重新编译, 另外组件之间要保持层次性,避免双向或是循环依赖。
数据存储
客户端本身是处理和收发网络数据, 这里就涉及到对这些数据如何组织和存储的问题。这个通常会根据客户端的类型采用不同的处处理方式:
对于永久存储的数据,当然是保存成文件或是存入数据库,文件如xml, 数据库如ACCESS, SQL server, mysql等。
临时数据当然是存入内存了,根据需要采用不同的数据结构, 组织格式如array,list, map, hashmap等。
还有一种是常见的数据保存方式是内存数据库,最常见是SQLite了, 内存数据库既能高效的分类保存大量数据, 又可以直接用基于SQL语句进行查询和处理, 比如foxmail客户端就是用SQLite存储的邮件信息。
还有一种是跨进程共享的数据,我们一般当然是内存映射文件了。比如我们有一个实时显示log的工具, 我们通常会在对方应用程序里分配共享内存的内存映射文件,所有的log都写到里面,然后在log工具程序里读取和显示。
当然还有一些数据可能存到网上去的, 常见的比如我们的云笔记, 这时数据分服务端数据和本地cache。
对于数据存储我们的设计原则是根据需要,选择简单高效的方式。比如我们在设计网络会议客户端时曾讨论要不要引入SQLite, 理想情况是引入内存数据库后各个组件和上层应用的数据都可以统一存储,采用数据驱动的方式,可以很方便的跟踪整个客户端的运行情况。但后来发现这种设计会把本来各自独立的组件通过数据库耦合在了一起,而且各组件的数据格式本身都很不要一样, 很难统一存储, 另外很多数据实际也只是临时数据, 没必要把简单的时间做复杂了。
客户端框架
客户端框架一般有两个作用:一是把所有的功能组件组织起来,进行统一的管理和展现; 二是实现整个客户端的主界面。客户端框架在协调各个组件时, 要注意避免让组件之间产生双向依赖, 而是应该让组件把事件通知给框架后由框架统一协调和处理, 所以客户端框架通常是整个客户端逻辑最复杂的部分。 一个好的客户端框架,通常会采用插件方式设计,可以动态插拔需要的组件。最好是可以根据服务端的配置, 动态下载和更新所需要的插件。
对于客户端框架, 我们的设计原则是低耦合和可扩展。基本上所有下层组件的改动都会影响到我们的客户端框架,客户的很多新需求和新组件也会导致框架产生坏味道, 这里我们设计时就要考虑如何让客户端框架能及时的适应这些变化。
界面库
客户端肯定会有界面,在Windows平台上,现在的界面大致分为以下几类:
基于Windows原始窗口控件句柄的C++ native 客户端, 基于.net的winform客户端,基于.net的WPF客户端,基于C++的DirectUI客户端, 以嵌入浏览器(如webkit)方式实现的客户端, 还有一类是基于Xaml的Metro客户端。
对于企业级大型安装应用,主要考虑的是开发效率,所以客户端还是以.net为主; 但是对于互联网企业, 更多的是要求客户端精简而高效, 所以还是以C++为主。 这里就设及到C++的两种界面库, 一种是基于windows窗口句柄和控件自绘机制的界面库,还有一中是基于DirectUI的界面库, 现在大一点的公司基本上都有自己的DirectUI界面库。基于修改Webkit方式实现的界面库可以通过Canvas和SVG动画等方式实现一些很炫的效果, 但是它和标准程序的用户体验还是有一定差距:一来web实现的控件跟Windows标准控件有很大不同, 二来web的流式布局和应用程序的网格布局也不一样。
对于界面,个人觉得这个东西变化实在太快了,所以我觉得应该尽量用界面和逻辑相分离的方式组织代码。
总结
对于客户端架构设计,个人觉得最大的原则就分层设计, 每层都封装一个概念并保持独立, 同时根据依赖倒置的原则, 站在上层客户的角度提供接口。软件工程里面的一条黄金定律:“任何问题都可以通过增加一个间接层来解决。