贝贝花花包包店,精品555皮具,钱夹,皮夹

字体: | 推荐给好友 上一篇 | 下一篇

ACE技术论文集-第3章 高效可移植和灵活的网络编程的C++包装

发布: 2008-6-13 14:57 | 作者: Prashant Jain and D | 来源: 转载 | 查看: 498次

3IPC SAP:用于高效、可移植和灵活的网络编程C++包装
 
Douglas C. Schmidt
 
本论文的一个扩展版本[1](含有在以太网和ATM网络上的性能评测)可在http://www.cs.wustl.edu/schmidt/COOTS-95.ps.Z处获取。
 
3.1介绍
 
本论文描述采用C++包装类来封装OS进程间通信(IPC)机制的面向对象(OO)技术,并聚焦于ACE构架[2]中的IPC SAP组件所提供的C++包装。ACE是一组可复用C++类库和OO构架组件,它们简化了可移植、高性能和实时通信软件的开发。IPC SAP是ACE中的一种组件,它提供了一个OO网络编程接口族来封装socket接口[3]、系统V传输层接口(TLI)[4]、SVR4 STREAM管道[5]、UNIX FIFO[6]和Windows NT命名管道[7]。
IPC SAP中的C++包装将开发者及应用与OS的本地和远地IPC机制的不可移植的细节屏蔽开。IPC SAP封装的IPC机制包括标准的面向连接的和无连接的协议,比如在UNIX/POSIX、Win32和实时操作系统中可用的TCP、UDP和IPX/SPX。IPC SAP利用OO技术和C++特性来提供一组丰富的组件,简化了高效、可移植和灵活的通信软件的开发。
本论文被组织如下:3.2概述用于编写通信软件的抽象层级;3.3描述现有的网络编程接口;3.4概述它们的局限;3.5介绍IPC SAP的OO设计和实现,并解释它是怎样克服现有网络编程接口的局限的;3.6详细检查socket、TLI、STREAM管道和FIFO的C++包装;3.7演示若干例子,使用IPC SAP来实现客户/服务器流式应用;3.8讨论指导IPC SAP的设计的原则;3.9则总结使用C++来为本地OS接口开发OO包装的优点和缺点。
 
3.2网络编程接口综述
 
 
图3-1 网络编程的抽象层次
 
编写健壮、可扩展和高效的通信软件是困难的。开发者必须掌握许多复杂的OS和通信概念,比如:
 
-网络寻址和服务标识。
-表示转换(比如加密、压缩和有可选处理器字节序的异种终端系统间的网络字节序转换)。
进程和线程创建及同步
-本地和远地进程间通信(IPC)机制的系统调用和库函数接口。
 
许多编程工具和接口已被创建用来帮助简化通信软件的开发。图3-1演示在当代的OS平台(比如UNIX和Win32)上可用的IPC接口。如图所示,应用可以访问本地和远地IPC的网络编程接口的若干层次。这一部分的余下部分概述每一抽象层,范围从高级分布式对象计算(DOC)中间件到用户级网络编程接口,再到低级的内核编程接口。
 
3.2.1 DOC中间件
 
以一种“请求—响应”方式来与客户交换数据的应用常常使用分布式对象计算(DOC)中间件来开发。DOC中间件的宽泛定义包括对象请求代理(ORB),像CORBA[8]和Microsoft的DCOM[9];以及面向消息的中间件,像Mqseries。DOC中间件使分布式应用开发的许多麻烦而易错的方面得以自动完成,包括:
 
-认证、授权和数据安全;
-服务定位和绑定;
-服务登记和启用;
-事件多路分离和分派;
-在像TCP这样的面向字节流的通信协议之上实现消息帧;
-涉及网络字节序和参数整编(marshaling)的表示转换问题。
 
此外,DOC中间件还提供一组高级工具,比如IDL编译器和名字服务,将开发者与较低级的OS系统调用(它们在网络上传输和接收包)的复杂性屏蔽开。
 
3.2.2用户级网络编程接口
 
DOC中间件通常建构在网络编程接口之上,比如socket[3]、TLI[4],或Windows NT命名管道。与较高级的DOC中间件相比,通过用户级网络编程接口来开发应用有若干优点:
 
最小化不必要功能的时间和空间开销:应用可以忽略不必要的功能,比如ASCII数据或内存区域的表示层转换。
允许对行为进行细粒度控制:网络编程接口使得对行为的控制粒度更为精细,比如允许多点传输和信号驱动的异步I/O。
增强可移植性:像socket这样的网络编程接口可用于广泛的OS平台,而DOC中间件则不是这样。
 
对于一类特定的被称为“流式应用”[10]的应用,DOC中间件提供的请求-响应和“单路”通信机制并不特别适用。流式应用的特征是高带宽、无类型字节流或相对简单的数据类型的长持续时间的通信,对通信性能有着严格的要求。交互式电话会议、医学成像和视频点播是流式应用的范例。
流式应用服务质量需求(QoS)常常不能忍受DOC中间件所带来的性能开销[11]。这样的开销源于未优化的表示格式转换、未优化的内存管理、低效的接收者端多路分离、停-等流控制、同步的发送端方法请求,以及非自适配的重发定时器方案。传统上,满足流式应用的需求涉及到对像socket[3]或TLI[4]这样的网络编程接口的直接访问。
 
3.2.3内核级网络编程接口
 
在OS内核的通信子系统中有较低级的网络编程接口。例如,SVR4 putmsg和getmsg系统调用可用于直接访问系统V STREAMS[14]中的传输供应者接口(TPI)[12]和数据链路供应者接口(DLPI)[13]。
还有可能开发像路由器或网络文件系统这样的网络服务,它们整个地驻留在OS内核中[5]。但是,在这一级进行编程通常不能在不同的OS平台间移植。而且,甚至也不能在同一OS的不同版本间移植。
 
3.2.4评估
 
使用用户级或内核级网络编程接口、而不是DOC中间件,通常要更难进行编程。像socket和TLI这样的传统网络编程库缺少类型安全、可移植、可重入和可扩展的接口。例如,socket端点通过弱类型的描述符实现,从而增加了在运行时发生微妙错误的潜在可能性[15]。
本文中描述的IPC SAP组件通过封装网络编程接口的大量复杂性,在设计空间中提供了一个“中点”。IPC SAP的目标是提高通信软件的正确性易用性可移植性/可复用性,而又不损害它的性能。IPC SAP与ACE构架[2]一起发布,并被用于许多公司的商业项目中,包括Bellcore、波音、朗讯、摩托罗拉、Nortel、SAIC和西门子,等等。
 
3.3 网络编程接口考察
 
这一部分考察像socket和TLI这样的传统网络编程接口的行为和局限。
 
3.3.1背景
 
在许多操作系统中,比如UNIX和Win32,通信协议栈驻留在OS内核的保护地址空间中。运行在用户地址空间中的应用程序通过像socket、TLI或Win32命名管道这样的接口来访问驻留内核的协议栈。这些接口对本地和远地的通信端点这样来进行管理:允许应用打开到远地主机的连接、磋商和启用/禁用特定的选项、交换数据,以及在传输完成时关闭全部或部分连接。
socket和TLI松散地建模在UNIX文件I/O接口之上,后者定义了open、read、write、close、ioctl、lseek和select函数[14]。但是,socket和TLI还提供了额外的功能,没有直接被标准的UNIX文件I/O接口所支持。这些额外的功能源于文件I/O和网络I/O之间语法和语义的差异。例如,在分布式环境中,UNIX系统用于标识文件的路径名并非是全局唯一的。因此,采用了一种不同的命名方案(比如IP主机地址)来唯一地标识网络应用。
socket和TLI接口提供类似的功能。它们支持一种多通信域[3]的通用接口。域指定协议族和地址族。每个协议族都含有一个协议栈,实现域中特定的通信类型。常用的协议栈提供可*、双向、面向连接的消息和流的服务(例如,像TCP、TP4和SPX这样的协议),以及不可*、无连接的数据报服务(例如,像UDP、CLNP和IPX这样的协议)。
地址族定义地址格式(例如,地址的字节长度、字段的数目和类型和字段顺序)以及一组驻留内核的对地址格式进行解释的函数(例如,决定一个IP数据报要发到哪个子网)。
3.3.2给出了socket综述,3.3.3简要描述了TLI,3.3.4讨论STREAM管道,而3.3.5讨论UNIX FIFO。对这些接口的完整讨论超出了本论文的范围(更多详情参见[5, 3, 7, 6, 16])。
 
3.3.2 socket接口
 
socket接口最初是在BSD UNIX中开发的,用以提供TCP/IP协议组[3]的接口。从应用的视点来看,socket是本地的通信端点,与驻留在本地或远地的地址绑定在一起。socket可通过句柄(也称为描述符)来访问。
在UNIX中,socket句柄与其他句柄共享同一个名字空间,例如,文件、管道和终端设备句柄。句柄提供一种封装机制,将应用与内部的OS数据结构的知识屏蔽开。句柄标识特定的由OS维护的通信端点。
 
 
图3-2 socket接口中的函数
 
socket接口如图3-2所示。该接口含有大约两打的系统调用,可分为以下类型:
 
本地管理:socket接口为管理本地上下文信息提供以下函数:
 
-socket:分配最小的未用socket句柄;
-bind:将socket句柄与本地或远地地址相关联;
-getsockname和getpeername:分别确定socket所连接的本地或远地地址;
-close:释放socket句柄,使它可用于后面的复用。
 
连接建立和连接终止:socket接口为建立和终止连接提供以下函数:
 
-connect:客户通常使用connect来主动地与服务器建立连接;
-listen:服务器使用listen来指示它想要被动地侦听进入的客户连接请求;
-accept:服务器使用accept来创建新的通信端点,以为客户服务;
-shutdown:有选择地终止一个双向连接的读端和/或写端流。
 
数据传输机制:socket接口提供以下函数来发送和接收数据:
 
-read/write:通过特定句柄接收和传输数据缓冲区;
-send/recv:与read/write类似,但它们提供一个额外的参数来控制特定的socket特有操作(比如交换“紧急”数据,或“偷看”接收队列中的数据,而又不把它从队列中移除);
-sendto/recvfrom:交换无连接数据报;
-readv/writev:分别支持“分散读”和“集中写”语义(这些操作优化用户/内核模式切换并简化内存管理);
-sendmsg/recvmsg:通用函数,包含了所有其他数据传输函数的行为。对于UNIX域的socket,sendmsg和recvmsg函数还提供在同一主机的任意进程间传递“访问权限”(比如打开文件句柄)的能力
 
注意这些接口也可被用于其他类型的I/O,比如文件和终端。
 
选项(option)管理:socket接口定义以下函数,允许用户改变socket行为的缺省语义:
 
-setsockopt和getsockopt:修改或查询在协议栈不同层次中的选项。选项包括多点传送、广播,以及设置/获取发送和接收传输缓冲区的大小;
-fcntl和ioctl:是UNIX系统调用,使在socket上能够进行异步I/O、非阻塞I/O,以及紧急消息递送。
 
除了上面描述的socket函数,通信软件还可使用以下标准库函数和系统调用:
 
-gethostbyname和gethostbyaddr:处理网络寻址的多种情况,比如映射主机名到IP地址;
-getservbyname:通过服务的端口号或人类可读的名字来对它们进行标识;
-ntohl、ntohs、htonl、htons:执行网络字节序转换;
-select:在成组的打开的句柄上执行基于I/O和基于定时器的事件多路分离。
 
3.3.3 TLI接口
 
TLI是访问通信协议栈的一种可选接口。基本上,TLI提供一组和socket一样的服务。但是,它更加强调使应用与底层传输供应者的细节屏蔽开来。[5]详细地讨论TLI。
 
3.3.4 STREAM管道
 
STREAM管道是对原始的UNIX管道机制的增强。早先的UNIX管道提供单一的从作者端点到读者端点的单向字节流。STREAM管道支持在执行在同一主机上的进程和/或线程间进行双向的字节流和按优先级排序的消息的递送[16]。尽管pipe系统调用接口保持不变,STREAM管道还提供了额外的功能,大致等价于UNIX域的SOCK_STREAM socket。但是它们比UNIX域的socket要更灵活一些,因为它们使STREAM模块可被“压入”或是“弹出”管道端点。
缺省地,流管道仅在它的两个端点间提供单一数据通道。因此,如果多个发送者向管道写入,所有的消息都被放置到同一个通信通道中。这常常太过受限,因为多路分离单个通道上来自多个客户的数据必须进行人工编程。例如,每个消息都必须包含一个标识符,使接收者能够确定是哪一个发送者传输的消息。通过使用已安装的(mounted)STREAM管道和connld模块[17],应用可以将一个单独的非多路复用的I/O通道专用于服务器和客户的每一实例之间。
STREAM管道和connld的工作方式如下:服务器调用pipe系统调用,创建双向通信端点。Fattach系统调用可以将管道句柄安装(mount)到UNIX文件系统中的指定位置。通过将connld STREAM模块压入STREAM管道的已安装的一端,就可以创建服务器应用。在运行服务器的同一主机上运行的客户应用随即打开与已安装管道相关联的文件。在这一点,connld模块确保客户和服务器分别收到一个唯一的I/O句柄,标识一个非多路复用、双向的通信信道。
 
3.3.5 FIFO接口
 
UNIX FIFO(也称为命名管道[6])是STREAM管道的受限形式。不像STREAM管道,FIFO仅提供单向的、从一或多个发送者到单个接收者的数据通道。而且,来自不同发送者的消息都被放入同一个通信通道中。因此,必须在每个消息中明确地包括某种类型的多路分离标识符,以使接收者能够确定是哪一个发送者传输的消息。
SVR4 UNIX中基于STREAM的FIFO实现同时提供消息和字节流递送语义。相反,一些早期版本的UNIX(比如SVR3和SunOS 4.x)仅提供面向字节流的FIFO。因此,除非总是使用定长消息,每个经由FIFO发送的消息必须通过某种形式的字节计数或特殊结束符来进行区分,从而使接收者能够从FIFO字节流中提取消息。FIFO在[5, 6, 16]中进一步描述。
 
3.4 问题:现有IPC接口的局限
 
socket、TLI、STREAM管道和FIFO为访问本地和远地IPC机制提供了广泛的接口。但是这些接口都有若干局限。下面的讨论聚焦于socket接口的局限,但是其中的大多数也适用于其他网络编程接口。
 
高错误可能性:在UNIX和Win32中,socket、文件、管道、终端和其他设备的句柄是用“弱类型”的整数或指针值来标识的。这样的弱类型检查会导致微妙的运行时错误。例如,socket接口无法确保用于不同通信角色(比如主动 vs. 被动连接建立,或数据报 vs. 流通信)的socket函数的正确使用。而且,编译器无法检测或阻止句柄的错误使用,因为句柄是弱类型的。因而,可能会不正确地对句柄进行操作,例如,在为建立连接而设置的句柄上调用数据传输操作。
 
#include
#include
 
const int PORT_NUM = 10000;
 
int buggy_echo_server (void)
{
sockaddr s_addr;
int length; // (1) uninitialized variable.
char buf[BUFSIZ];
int s_fd, n_fd;
 
// Create a local endpoint of communication.
if (s_fd = socket (PF_UNIX, SOCK_DGRAM, 0) == -1)
return -1;
// Set up the address information to become a server.
// (2) forgot to "zero out" structure first...
s_addr.sin_family = AF_INET;
// (3) used the wrong address family ...
s_addr.sin_port = PORT_NUM;
// (4) forgot to use htons() on PORT_NUM...
s_addr.sin_addr.s_addr = INADDR_ANY;
if (bind (s_fd, (sockaddr *) &s_addr, sizeof s_addr) == -1)
perror ("bind"), exit (1);
// (5) forgot to call listen()
 
// Create a new endpoint of communication.
// (6) doesn’t make sense to accept a SOCK_DGRAM!
if (n_fd = accept (s_fd, &s_addr, &length) == -1)
{
// (7) Omitted a crucial set of parens...
int n;
// (8) doesn’t make sense to read from the s_fd!
while ((n = read (s_fd, buf, sizeof buf)) > 0)
// (9) forgot to check for "short-writes"
write (n_fd, buf, n);
// Remainder omitted...
}
}
图3-3 臭虫成灾的Echo服务器
 
图3-3描述下列在使用socket接口时发生的微妙和“过于常见”的错误:
 
  1. 忘记将accept的len参数初始化为struct sockaddr_in的大小;
  2. 忘记将socket地址结构中的所有字节初始化为“0”;
  3. 使用了与socket的协议族相矛盾的地址族类型;
  4. 忽略了使用htons库函数来将端口号从主机字节序转换到网络字节序,反之亦然。
  5. 创建被动模式的SOCK_STREAM socket时遗漏了listen系统调用;
  6. 对SOCK_DGRAM socket使用了accept函数;
  7. 在赋值表达式中错误地遗漏了一组关键的括号;
  8. 试图从被动模式socket中读,而这样的socket只能用于接受连接;
  9. 没有能适当地检测和处理由于缓冲而发生的“短写”(short-writes)。
 
上面所列问题中的一些是C的经典问题。例如,如果遗漏了下面这个表达式中的括号
 
if (n_fd = accept (s_fd, &s_addr, &length) == -1)
 
n_fd的值将总是被设为0或者1(取决于accept()是否等于-1)。
一个更深的问题是C数据结构缺乏足够的抽象。例如,通用的sockaddr地址结构使得开发者必须使用强制类型转换来提供Internet域和UNIX域地址的一种继承形式。这些“子类”地址结构,sockaddr_in和sockaddr_un,分别对sockaddr“基类”进行重定义。
一般而言,强制类型转换的使用,与弱类型的、基于句柄的socket接口一起,使得编译器很难在编译时检测错误。相反,错误检查被推延到运行时,这使得错误处理变得更为复杂,并且降低了应用的健壮性。
 
复杂的接口:socket提供了单一接口来支持多种协议族,像TCP/IP、IPX/SPX、ISO OSI和UNIX域的socket。socket接口含有许多函数,支持不同的通信角色(比如主动 vs. 被动连接建立)、通信优化(比如在单个系统调用中发送多个缓冲区的writev),以及用于不常使用的操作的选项,比如广播、多点传送、异步I/O和紧急数据递送。
尽管socket将这些功能组合进一个通用的接口,所得到的机制仍然是复杂而又难以掌握的。这样的复杂性源于socket接口过于宽泛的和一维的(one-dimensional)设计。例如,如图3-2所示,所有函数都出现在单一的抽象层中。这样的设计增加了正确学习和使用socket所需的努力。这样,程序员必须理解整个socket接口,即使他们只使用其中一部分。
但是,如果仔细地检查socket,很清楚该接口可以被分解为下面三个函数簇:
 
  1. 通信服务类型:也就是,流 vs. 数据报 vs. 有连接的数据报;
  2. 通信角色:也就是,主动的 vs. 被动的(客户通常是主动的,而服务器通常是被动的)
  3. 通信域:也就是,本地 vs. 本地/远地。
 
图3-4根据这三个标准来对相关的socket函数进行分类:
 
 
图3-4 socket的各个维度
 
但是,因为接口是一维的,这样自然的分类被弄得含混不清。3.6演示了怎样将此分类重新构造为一个类层次,以简化socket接口并增强通信软件的类型安全性。
 
不统一:socket接口的另一问题是它的若干打函数缺乏统一的命名习惯。不统一的命名使得开发者很难确定socket接口的范围。例如,socket、bind、accept和connect之间的相关并不显而易见。其他网络编程接口通过在每个函数前面添加公共前缀来解决这一问题。例如,在TLI库的每个函数前都有t_前缀。
但是,TLI接口也含有有着过于复杂的语义的操作。例如,不像socket,TLI选项处理接口没有以一种标准的方式来规定。这使得开发者很难编写可移植的应用来访问标准的TCP/IP选项。同样地,在qlen > 1的并发服务器中,需要使用微妙的应用级代码来处理t_listen和t_accept的非直观和易错的行为[5]。

TAG: IPC SAP 高效

51/512345>
 

评分:0

我来说两句

seccode