
1.4 数据传输各个角色的开发思路
在笔者通常所做的商用数据传输系统中,虽然工程千变万化,但是其中还是有规律可循的。任何一个网络工程总离不开服务器和客户端这两个角色,两个角色的开发各有侧重点。这里提出来,供大家参考。
一般说来,服务器开发以稳定为主导思想,开发思路偏保守,尽量使用成熟技术,回避新技术的使用。而客户端的开发,由于和UI相关,使用技术偏新,且对程序的稳定性要求有一定放宽,毕竟客户端即使挂死,一般也不会有太大问题。
但近年来嵌入式系统不断发展,嵌入式开发越来越成为重点。笔者粗略了解了一下,发现嵌入式设备的开发难点反而较高,这里也提出来,单独做一个讨论。
1.4.1 服务器的设计原则
网络服务器的开发一般都有运营的要求,即放到电信机房长期运行。因此,服务器的设计对系统稳定性、故障自恢复性、错误追溯性提出了较高要求。
网络服务器的设计开发以Linux平台开发居多,Windows平台也占相当比例。
服务器设计必须对所有资源的使用做专门的设计,及时做好资源回收,避免泄露,这些资源主要包括内存资源、socket资源、线程资源等。
服务器针对内存必须有特殊的设计,对于使用完的内存块必须及时回收重用,避免内存碎片的产生。服务器应尽量减少对动态内存的申请和使用。
服务器对线程的使用,不允许每个任务独立一个线程,应该设计时间片概念,以任务方式组织业务动作,达到线程重用的目的。鉴于Linux服务器的特性,一个服务器的并发线程建议不超过300。
服务器内部建立完善的资源跟踪机制,保证资源的申请和释放,可跟踪、可追溯,并利用服务器的Log系统做好记录。
服务器必须自带Log系统,Log系统须充分考虑磁盘写满、文件超限等情况,自身设计非常安全。Log系统执行静默原则,即有错误则执行记录,没有错误不做任何事。如无特殊要求,原则上,服务器的Log系统保留最少72小时的记录,甚至更长,以方便故障分析。
在网络协议的动作规约中,原则上服务器永远是被动方,即每次动作总是由客户端发起,这样一来避免由于网络错误导致动作发起方的挂死,二来,由于客户端通常处于内网,外网的服务器很难访问内网的设备。但这个原则在多服务器集群中并非绝对。
服务器对请求的响应原则上执行“从严”的策略,层级校验,一旦发现对方身份不对,则立即终止服务以预防DoS攻击。
在服务策略的选择上,服务器执行“自私”原则,即优先保证自己的安全,再谈客户服务,对于服务器而言,单个客户的某一次服务失败,不叫bug,应及时终止本笔交易,释放资源为下一交易服务。
服务器需要自动定时显示基本的负载信息,供操作员观察服务器负载情况,及时做出负载均衡调整。
由于很多服务器处于电信托管机房,原则上服务器有两套退出系统,控制台的键盘输入和远程的退出机制,以便于远程管控。退出时应尽量温和退出,即按顺序释放所有资源后退出,避免磁盘文件等残留物影响下次运行。
服务器原则上工作于后台,除了退出信号,一般为哑元设备,不需要客户交互,避免不必要的bug。
由于很多服务器实际上工作于Linux控制台模式,原则上服务器的开发,所有输出信息应使用英文,避免使用中文,以免乱码。
由于服务器运行的平台,通常不止一路服务,服务器对CPU和内存等本机资源的使用,应该有礼貌,原则上使用则申请,不使用则及时释放,避免影响其他服务的正常运行。
必要时可以考虑为服务器开发专用的“看门狗”程序,监控服务器的运行状态,以便故障时重启服务器,快速恢复服务。
1.4.2 PC客户端的开发思路
PC平台大概是大家最熟悉的微型计算机平台了。近年来,PC平台随着科技的进步,性能也越来越强大,很多家用机的性能已经超过了以前服务器的性能。
PC平台定位在家用机和办公用机,因此,在PC平台上运行的绝大多数是商用数据传输中的客户端软件。因此,这里我们主要讨论客户端的开发思路。
由于目前Windows平台绝对的市场占有率,一般说来客户端基于Windows平台开发。
客户端开发以UI为主,以用户体验为主,可以适当考虑使用新的技术,追求一些特效,即使由此带来可能的不稳定也可以接受。
客户端原则上不考虑长期运行能力,因此可以使用动态内存分配,当然,必须及时释放资源,不允许读写出界,但可以不考虑内存碎片的影响。
客户端由于并发任务较少,因此一般直接开线程工作,不考虑使用池技术。
客户端原则上不考虑Log系统,由于拥有UI界面,可以很方便地将信息直接与用户交互。
客户端不关心负载平衡性,对于资源的使用可以适当放宽。
客户端在不考虑上述特殊设计的前提下,应该尽量灵活多变,满足快速开发目标,以响应不断变化的用户需求。
理想状态下,客户端显式分为内核和UI层,二者以API交互,这样可以实现快速换肤之类的UI特效。
在与服务器交互的过程中,客户端对于服务器资源的申请,应该主动发起连接,并且,客户端负责所有失败后连接恢复的事务,且请求失败后,应该能自动恢复现场,不至于崩溃。
1.4.3 嵌入式设备的开发思路
嵌入式设备在网络中的角色一般很难说,基本上既有服务器应用,也有客户端应用,甚至二者兼有。比如一个家庭无线网关,就很难说他的多角色身份定位。
但嵌入式设备有一个天生的局限,就是系统资源偏少,比如一台ARM9的设备,通常情况下,64MB RAM,64MB Flash(模拟磁盘),相对于PC机动辄1GB的内存来说,显得非常少。
这就要求嵌入式设备的开发,应该比服务器的要求更加严格,对于资源的使用,更加谨慎才行。而且,嵌入式设备的操作系统也非常复杂,有Palm、Symbian、Windows Mobile、Linux等,颇有点当时Windows出来之前,PC平台上操作系统百花争艳的感觉。
上述种种因素都给嵌入式开发带来了很多困难。
不过万事万物皆有规律可循,不管哪个操作系统,在C和C++语言这个层面上总是差不多的。商用工程开发的规律也是差不多的。因此,从开发思路上讲,各种嵌入式平台差别并不如想象得那么大。
笔者在本书之中,主要使用ARM9平台的Linux 2.4内核实现开发试验,但代码的通用性均较好,有兴趣的读者可以在本书代码的基础上略作修改,即可适应其他操作系统开发。
嵌入式设备的开发,首先要确定比服务器更加严格的边界界定,甚至要分模块设定边界。这是因为嵌入式设备的内存、CPU资源远低于普通的PC机和服务器,对资源溢出或泄漏更加敏感。
原则上,嵌入式软件对内存的使用,应使用服务器级的内存池管理,且设立更加严格的边界检查,一旦内存申请失败即放弃服务,以牺牲服务品质确保本机安全。
嵌入式设备的内存容量较小,直接导致C和C++程序运行空间较小,因此,原则上不允许申请超过4MB以上的内存(特殊需求除外),且对内存的及时回收和重用提出了更高的要求,这个原则必须贯穿于软件开发的始终。
由于前述原因,且由于线程启动时,需要分配一定的栈空间(Linux下为1MB),嵌入式系统原则上并发的线程数一般不允许超过50条(一般建议小于30条),且每个C函数内部,不允许使用超过1MB的静态数组。
嵌入式设备运行的应用相对比较单一,很多只跑有限的几路服务,且CPU和内存较低,因此在设计时,对于CPU、内存等资源,可以考虑在不挂死的前提下尽量征用,以保证较高的服务品质。
嵌入式设备基本上属于哑元运行,且没有自己独立的输入、输出设备,一般都是远程登录管理,因此,所有信息必须由Log系统保留到虚拟磁盘上(Flash ROM),但由于磁盘容量较小,原则上保留最后1小时的信息,供事故分析。
嵌入式设备通常作为设备长期运行,虽然处于用户家中或办公室,但实际上是7×24小时工作居多,因此,嵌入式软件产品的稳定性要求应按照服务器的标准设定。
嵌入式设备对请求的响应原则上执行“从严”的策略,层级校验,一旦发现对方身份不对,则立即终止服务。
在服务策略的选择上,嵌入式设备执行“自私”原则,即优先保证自己的安全,再谈客户服务,对于嵌入式设备而言,单笔服务失败不叫bug,应及时终止退出,释放资源为下一客户服务。
嵌入式设备通常也没有服务器的专业管理手段,用户关机一般是直接关闭电源,没有安全退出的可能性。因此嵌入式设备对于需要长期保存的数据,不允许使用任何Cache机制,以牺牲性能为代价,直接写入Flash Rom等虚拟磁盘文件中。
嵌入式设备至少要提供一种远程管理手段,有条件的话,应该提供小型的http服务,供用户使用浏览器登录管理。且要充分考虑到嵌入式设备很多都工作在内网,因此,尽量提供外网到内网的浏览器直接登录管理,这需要额外配套公网中继Relay手段。
嵌入式设备的业务逻辑尽量以响应请求,予以回应的“傻瓜化”操作为主,尽量非智能,系统逻辑越简单越好,条件允许时,如有中心服务器的系统,可以考虑将嵌入式设备的一些计算压力放到中心服务器代为解决。
嵌入式设备的用户相关业务,UI层原则上和内部逻辑层分离设计,二者尽量以异步方式通信,一来可以实现换肤等操作,二来可以尽量减少程序潜在的bug。
嵌入式平台通常自带看门狗硬件,应加以利用,必要时系统自动重启,恢复服务。
1.4.4 跨平台软件模块的开发思路
很多时候,商用数据传输系统要考虑各种不同的客户需求,并且还要综合成本与利润的考虑。基本上一个系统内部既有服务器、PC客户端,也包括嵌入式设备,一般都比较复杂。
而对商用程序员来说,为每个平台开发一个底层传输内核,显然是一种不经济的行为。程序员也要考虑自己的产能。
这说明,商用程序员首先要有比较强的系统分析能力,能将每个系统与平台特性相关密切的部分(如UI)以及通用性较好的部分(如传输内核),显式地分为不同的模块,分别予以维护。其次,商用程序员有较强的跨平台开发功力,在通用性较好的部分尽量维护同一个模块,通过条件编译等手段,实现一套库模块多平台通用。
笔者在网上和一些朋友沟通的时候,很多朋友都在询问,如果一套代码,跨平台通用,则这个代码必然是通用性较高的代码,那么通用代码效率低是众所周知的,如何保证效率?
笔者却认为这不是一个问题,跨平台开发本身已经决定了程序不可能调用太多平台相关特性,很多算法和数据结构需要自己内部实现。而在实现过程中,必然会针对我们的需求做特定优化,其效率很多时候反而高于调用平台提供的通用性系统调用。这在后面的模块说明中有详细介绍。
由于服务器的标准高于客户端,而嵌入式在服务器基础上做了更加严格的界定,因此,一般跨平台模块的开发要求参照嵌入式设备和服务器的标准执行。
所有的模块,内部不允许使用大型静态数组,尽量动态内存静态管理,以保证有限的栈资源调用,以及保证边界可调,方便各个平台条件编译。
根据商业化系统工程常见的需求分析,跨平台传输库至少包含内存池、线程池、log日志系统、基本队列、基本传输等关键模块。
设计原则:数据传输内核提供通路,但不保证传输,传输内核不负责校验数据的正确性,传输品质保证由上层应用模块负责。