Chapter 3. ACE Socket 封装外观

Chapter 3. ACE Socket 封装外观

3.1 概览

ACE 定义一组 C++ 类以解决 Socket API 如 Section 2.3 所述的限制。这些类按照 Wrapper Facade 设计模式设计。它们因此封装由现存非面向对象的 APIs 提供的函数和数据,使用更加简明,稳定,可移植,可维护,和更集中的面向对象的类接口。出现在本章中的 ACE Socket 封装外观类包括:

ACE 类描述
ACE_AddrACE 网络地址层级的 root
ACE_INET_Addr封装 Internet-domain 地址族
ACE_IPC_SAPACE IPC 封装外观层级的 root
ACE_SOCKACE Socket 封装外观层级的 root
ACE_SOCK_Connector工厂用于连接对端 acceptor,然后初始化一个在 ACE_SOCK_Stream 对象中的新的通讯 endpoint
ACE_SOCK_IO ACE_SOCK_Stream封装由 data-mode socket 支持的数据传输机制
ACE_SOCK_Acceptor工厂用于初始化一个 ACE_SOCK_Stream 对象中的通讯 endpoint,作为来自对端 connector 的连接请求的响应

图 3.1 解释这些类之中的关键关系。ACE Socket 封装外观提供下列好处:

  • Enhance type-safety 通过快速地检测很多不易察觉的应用类型错误以提升类型安全;例如,主动和被动连接建立工厂不提供方法用于发送或接受数据,所以类型错误在编译期被捕获而非运行时。
  • Ensure portability 通过平台独立的 C++ 类提高可移植性。
  • Simplify common use cases 通过减少对底层网络编程细节的应用代码的数量和开发努力,使开发者能专注于更高层级,以应用为中心的关注点。
    在这里插入图片描述

进一步来说,ACE Socket 封装外观通过使用内联函数来保持效率以提升上述列出的软件质量而不牺牲效率。ACE 的效率相关的设计原则在 Section A.6 中讨论。

ACE Socket 封装外观的结构与图 3.2 中出现的通讯服务,连接/通讯角色,和通讯域的分类相对应。图中的类提供以下功能:

  • ACE_SOCK_* 类封装 Internet-domain Socket API 功能。
  • ACE_LSOCK_* 类封装 UNIX-domain Socket API 功能。
    在这里插入图片描述

C Socket API 的函数在面向连接的协议中 (例如 TCP) 能被分成三种不同的角色:

  1. 主动连接角色 (connector) 由一端应用扮演,初始化一个对远程对端的连接。
  2. 被动连接角色 (acceptor) 由一端应用扮演,接收来自远程对端的连接。
  3. 通讯角色 (stream) 双端应用都扮演,在连接后交换数据。
    在这里插入图片描述

3.2 ACE_Addr 和 ACE_INET_Addr 类

动机

Socket API 网络地址机制使用 C 结构和类型转换,这对编程来说冗长且易于出错。地址族是通常的地址结构 (sockaddr) 的第一个成员。其他地址结构,例如用于 Internet-domain 地址的 sockaddr_in 和用于 UNIX-domain 地址的 sockaddr_un ,也有一个地址族成员,占据与 sockaddr struct 中同样的位置和大小。应用使用这些指定的地址族结构通过:

  1. 为期望的地址族分配一个结构体,例如,struct sockaddr_in
  2. 填充地址族成员 (例如,AF_INET) 以指示 Socket API 它的真实类型是什么
  3. 提供地址。例如 IP 和端口号,和
  4. 转换类型为 sockaddr*,传递给 Socket API 函数。

为了最小化这些底层细节的复杂度,ACE 定义了一个类的继承层级以为所有的 ACE 网络地址对象提供统一的接口。

类能力

ACE_Addr 类是 ACE 网络地址层级的 root。该类的接口如图 3.4 所示,它的关键方法提供下述能力,它们对所有的 ACE 网络地址类都是通用的。

方法描述
operator==()比较地址是否相等
operator!=()比较地址是否不等
hash()计算地址的 hash 值

在这里插入图片描述
ACE_Addr 也定义了一个 sap_any 静态数据成员,客户端和服务器可以把它当作一个 “通配符” 来使用,如果不关心它们分配的地址的话。例如:

  • 客户端使用 sap_any 来创建临时的 OS 分配的端口号,被称为 “临时端口”,在连接关闭后它会被循环使用。
  • 服务器应用使用 sap_any 来选择它们的的端口号,只要能通过某种类型的位置发现机制向客户端导出分配的端口号,例如 naming 或 trading 服务。

每个 IPC 机制的具体地址类,例如 Internet-domain sockets 和 UNIX-domain sockets,都派生自 ACE_Addr 并添加它们所需的地址。例如,TCP/IP 和 UDP/IP 地址信息由 ACE_INET_Addr 类表示,如图 3.4 所示。除了实现 ACE_Addr 基类接口,ACE_INET_Addr 提供下述关键方法:

方法描述
ACE_INET_Addr() set()使用主机名,IP 地址,和/或 端口号初始化 ACE_INET_Addr
string_to_addr()将字符串转换为 ACE_INET_Addr
addr_to_string()将 ACE_INET_Addr 转换为字符串
get_port_number()以主机字节序返回端口号
get_host_name()返回主机名

为网络地址使用 ACE Socket 封装外观避免了在使用 C sockaddr 数据结构族时常见的陷阱和缺陷。例如,使用图 3.4 所示的 ACE_INET_Addr 构造函数从端口号和主机名创建一个 ACE_INET_Addr,因此消除常见的编程错误:

  • 初始化所有 sockaddr_in 中的字节为 0
  • 转换端口号IP 地址网络字节序

3.3 ACE_IPC_SAP 类

动机

Section 2.3.1 描述了由原生 OS IPC APIs 使用的 I/O handle 所引起的兼容性问题。ACE 解决 I/O handle 的兼容性通过:

  • 定义 ACE_HANDLE 类型定义,在每个 OS 平台上指定合适的 handle 类型,和
  • 定义可移植的 ACE_INVALID_HANDLE 宏,应用可以使用它来测试错误。

这些简单的 ACE 定义的类型和值抽象帮助提升应用的可移植性。

然而,熟练的软件开发者会注意到,即使是一个可移植的 handle 对于面向对象的网络应用来说也不是一个合适等级的抽象。相反,更合适的编程抽象是某种类型的 I/O handle 类,这就是为什么 ACE 提供 ACE_IPC_SAP 类的原因。

类功能

ACE_IPC_SAP 类是 ACE 的 IPC 封装外观层级的 root,为其他 ACE 封装外观提供基础的 I/O handle 操作功能。它的接口如图 3.5 所示 (伴随着 Section 3.4 描述的 ACE_SOCK 类),它的方法如下表所示:

方法描述
enable() disable()启用或禁用各种的 I/O handle 选项,例如启用/禁用 non-blocking I/O
set_handle() get_handle()设置或获取底层的 I/O handle

在这里插入图片描述
虽然 ACE_IPC_SAP 定义了有用的方法和数据,但是并不推荐应用直接使用它。相反,它由其子类使用,例如 ACE 对文件,STREAM pipes,Named Pipes,和 System V Transport Layer Interface(TLI) 的封装外观。为了施加这一设计约束,避免 ACE_IPC_SAP 被直接实例化,ACE_IPC_SAP 被声明为一个抽象类,其构造函数被声明在类的 protected 部分。我们没有为其声明一个纯虚函数,这样避免了子类对虚指针的需求。

3.4 ACE_SOCK 类

动机

正如 Section 2.3 所讨论的,意外复杂度的关键原因源于编译器无法在编译器检测到 socket handle 的错误使用。Section 3.3 描述的 ACE_IPC_SAP 类是解决该问题的第一步。本章的剩余部分描述用于解决其他问题的其他 ACE 封装外观。从图 3.1 所示的继承层级向下移动,并讨论每一层的有意义的行为,从 ACE_SOCK 类开始。

类功能

ACE_SOCK 类是 ACE Socket 封装外观层级的 root。除了导入了继承自 ACE_IPC_SAP 类的方法外,ACE_SOCK 提供了与其他 ACE Socket 封装外观相同的功能,包括随后在本章讨论的类。这些功能包括:

  • 创建和销毁 socket handles
  • 获取本地和远程端的地址,和
  • 设置和获取 socket 选项,例如 socket 队列大小,启用 broadcast/multicast 通讯,和经用 Nagle‘s 算法

ACE_SOCK 接口和它与 ACE_IPC_SAP 基类间的关系如图 3.5 所示,它的关键方法如下表所示:

方法描述
open() close()创建和销毁通讯的 socket endpoint
get_local_addr() get_remote_addr()分别返回本地和远程端的地址
set_option() get_option()设置和获取 socket 选项

为了避免意外的错误使用,ACE_SOCK 被定义为抽象类;即它的构造函数被声明在类的 protected 访问控制部分。因此与 ACE_IPC_SAP 一样,ACE_SOCK 对象不能被直接实例化,因为它们仅能被子类访问,例如 Section 3.5 到 Section 3.7 所述的面向连接的封装外观。

ACE_SOCK 类提供 close() 方法因为它在其析构函数中不关闭 socket handle。正如 Section A.6.1 中所述,这一设计意在当 ACE_SOCK_Stream 按值传递或拷贝到不同的对象时,避免一些错误的发生。有经验的 ACE 开发者使用更高层级的类来自动关闭底层的 socket handle。例如,Section 4.4.2 所述的 Logging_Handler 类在更高层级的 handler 对象关闭时关闭它的 socket。

A.6.1 设计高效的封装外观
ACE 使用下述技术保证封装外观的效率:

  • 很多 ACE 封装外观是具体的类型;即,它们的方法是非虚的。该设计避免了分配动态绑定方法的开销,增加了方法内联的机会,并使对象能被放入共享内存中。
  • 所有的 ACE IPC 封装外观都包含显式的 open()close() 方法,且它们的析构函数不关闭 handles。这一设计避免了当从一个对象到另一个对象按值传递时,过早地关闭 I/O handles 而造成不易察觉的错误。ACE 封装外观有意地避免使用 Bridge 模式,因为这会增加动态内存分配并降低效率,而非提升它。相反,我们应用于 ACE 的原则是对类层级的 root 不执行某种类型的隐式操作。高层级的抽象会在需要时提供这些功能。例如,Section 4.4.1 中的 Logging_Server 类在它的析构函数中关闭 acceptor 的 socket handle,这是高层级抽象中的常见使用案例之一。

3.5 ACE_SOCK_Connector 类

动机

虽然在面向连接的协议中有三种不同的角色,Socket API 只支持下述两种 socket 模式:

  1. data-mode socket 由 peer 应用在它们的通讯角色中使用,以在连接端之间交换数据
  2. passive-mode socket,peer 应用在被动连接角色中使用的工厂,返回一个 handle 来连接 data-mode socket。

没有 socket 模式专门用于主动连接角色,习惯上 data-mode socket 扮演这一角色。应用因此不支持在 data-mode socket 上调用 recv()send() 直到调用 connect() 函数成功建立连接后。

Socket API 在连接角色和 socket 模式间的不对称性令人迷惑且易于出错。例如,应用可能意外地在 data-mode socket 连接前调用 recv()send()。不幸地是,这一问题直到运行时才能检测到,因为 socket handles 是弱类型地,data-mode socket 的双重角色仅能通过编程习惯约束,而非通过编译器类型检查特性。ACE 因此定义了 ACE_SOCK_Connector 类,它通过仅暴露用于主动建立连接的方法来避免意外的错误使用。

类功能

ACE_SOCK_Connector 类是一个工厂,它主动建立新的通讯 endpoint。它提供下述功能:

  • 初始化对对端 acceptor 的连接,在连接建立后初始化一个 ACE_SOCK_Stream 对象。
  • 连接可以以阻塞,非阻塞,或超时的方式进行初始化。
  • 使用 C++ 特性以支持泛型编程技术,通过 C++ 参数化类型支持整体替换的功能,如 Sidebar 5 所述。

Sidebar 5:为 ACE 封装外观使用特性
为了简化 IPC 类和与它们关联的地址类的整体替换,ACE Socket 封装外观定义了 traits。Traits 是 C++ 泛型编程习语,用于定义和结合一组用于改变模板类行为的特性。ACE Socket 封装外观使用特性来定义下述类关联:

  • PEER_ADDR 这一特性定义 ACE_INET_Addr 寻址类,它与 ACE Socket 封装外观相关联。
  • PEER_STREAM 这一特性定义 ACE_SOCK_Stream 数据传输类,它与 ACE_SOCK_AcceptorACE_SOCK_Connector 工厂相关联。

ACE 如同 C++ 类型定义般实现这些特性,在本章的 UML 图中也有体现。Section A.5.3 说明了如何使用这些特性编写简明的通用函数和类。

A.5.3 通过参数化类型处理可变性
问题: 网络应用和中间件通常必须运行于一系列的平台上,平台上的 OS 功能的可用性和效率各不相同。例如,某些 OS 平台可能处理不同的底层网络 APIs,例如 Sockets 而非 TLI 或者反过来。同样的,不同的 OS 平台实现的 APIs 的执行效率或多或少也有不同。当在多样化的平台上编写可重用的软件时,下列的关注点必须被解决:

  • 不同的应用可能需要不同的中间件策略配置,例如不同的同步机制或 IPC 机制。添加新的或提升策略应该是直观的。理想情况下,每个应用函数或类应该被限制为单个副本,以避免版本偏斜。
  • 为变化所选的机制不应该过度的影响运行时的性能。特别是,继承和动态绑定会增加额外的运行时开销,因为虚方法的间接性 [HLS97]。

解决方法: 通过参数化类型处理可变性而非继承和动态绑定。参数化类型使应用不再依赖于特定的策略,例如同步或 IPC APIs,不会增加运行时的开销。虽然参数化类型会增加编译时和链接时的开销,它们通常被编译为高效的代码 [Bja00]。

例如,使用 C++ 类封装 Socket API (而非标准的 C 函数) 有助于提高可移植性,使用参数化类型能让网络编程机制被整个替换。下面的代码说明了这一原理,它通过可生成和泛型的编程技术修改 echo_server(),使它成为一个函数模板:

template <class ACCEPTOR>
int echo_server(const typename ACCEPTOR::PEER_ADDR &addr) {
	// Connection factory
	ACCEPTOR acceptor;
	// Data transfer object
	typename ACCEPTOR::PEER_STREAM peer_stream;
	// Peer address object
	typename ACCEPTOR::PEER_ADDR peer_addr;
	int result = 0;
	
	// Initialize passive mode server 
	// and accept new connection
	if(acceptor.open(addr) != -1
		&& acceptor.accept(peer_stream, &peer_addr) != -1) {
		char buf[BUFSIZ];
		
		for(size_t n; 
			(n = peer_stream.recv(buf, sizeof buf)) > 0;) {
			if(peer_stream.send_n(buf, n) != n) {
				result = -1;
				break;
			}
		}
		peer_stream.close();
	}
	return result;
}

通过使用 ACE 和 C++ 模板,应用可以使用 C++ Socket 或 TLI 封装外观透明地参数化,取决于底层 OS 平台的属性。

// Conditionally select IPC mechanism.
#if defined (USE_SOCKETS)
typedef ACE_SOCK_Acceptor ACCEPTOR;
#elif defined (USE_TLI)
typedef ACE_TLI_Acceptor ACCEPTOR;
#endif /* USE_SOCKETS */

int driver_function(u_short port_num) {
	// ...
	
	// Invoke the <echo_server()> with appropriate network
	// programming APIs=. Note use of template traits for 
	// <addr>.
	typename ACCEPTOR::PEER_ADDR addr(port_num);
	echo_server<ACCEPTOR>(addr);
}

这一技术的工作原理如下:

  • ACE C++ Socket 和 TLI 封装外观类暴露具有公共签名的面向对象的接口。在接口在最初的设计中并不一致的情况下,能应用 Adapter 模式来保证一致性。
  • C++ 模板支持基于签名的类型一致性,不需要类型参数包含所有潜在的功能。相反,模板将应用代码参数化,这些应用代码被设计为仅调用各种网络编程方法的通用方法中的子集,例如 open()close()send()recv()

一般来说,参数化类型较替代类型具有更少的侵入性和更多的可拓展性,例如实现多个版本的 echo_server() 函数或在整个应用源码中丢弃条件编译指令。

ACE_SOCK_Connetor 的接口如图 3.6 所示。ACE_SOCK_Connector 中的两个关键的方法如下表所示:

方法描述
connect()主动连接一个位于特定网络地址上的 ACE_SOCK_Stream,使用阻塞,非阻塞,或超时模式
compete()尝试完成一个非阻塞的连接,并初始化一个 ACE_SOCK_Stream

在这里插入图片描述
ACE_SOCK_Connector 支持阻塞,非阻塞,和定时的连接,阻塞是默认行为。当在高延迟的链接上建立连接时,使用单线程的应用时,或初始化很多端它们能以任意的顺序被连接时,非阻塞和定时连接十分有用。三种类型的 ACE_Time_Value 能被传入 connet() 函数来控制这一行为:

行为
NULL ACE_Time_Value 指针表示 connet() 应该永久等待,阻塞直到连接被建立或 OS 认为服务器不可达
Non-NULL ACE_Time_Value 指针,其sec()usec() 方法返回 0表示 connect() 应当执行一个非阻塞连接,即,如果连接没有立刻建立,返回 1 并将 errno 设为 EWOULDBLOCK
Non-NULL ACE_Time_Value 指针,其sec()usec() 方法返回值大于 0表示 connect() 应当仅等待相对数量的时间来建立连接,返回 1,errno 被设为 ETIME 如果在该时间点还没有成功建立连接

ACE_SOCK_Connector 连接超时支持在实践中十分有用,因为 Socket API 实现连接超时的方式在不同的 OS 平台上很不一样。

因为底层的 socket API 不使用工厂 socket 来连接 data-mode socket,ACE_SOCK_Connector 不需要继承自 ACE_SOCK 类。因此它也没有自己的 socket handle。相反,ACE_SOCK_ConnectorACE_SOCK_Stream 中获得 handle 传入 connet() 方法中,并使用它主动建立连接。结果,ACE_SOCK_Connector 实例不存储任何状态,在多线程程序中它们可以重入使用,不需要额外的锁。

示例

#include "ace/INET_Addr.h"
#include "ace/SOCK_Connector.h"
#include "ace/SOCK_Stream.h"

int main(int agrc, char* argv[]) {
	const char* pathname =
		argc > 1 ? argv[1] : "index.html";
	const char*server_hostname =
		argc > 2 ? argv[2] : "ace.ece.uci.edu";
	ACE_SOCK_Connector connector;
	ACE_SOCK_Stream peer;
	ACE_INET_Addr peer_addr;
	
	if(peer_addr.set(80, server_hostname) == -1)
		return -1;
	else if(connector.connect(peer, peer_addr) == -1)
		return -1;
	// ...
} 

非阻塞的connect()

// Designate a nonblcoking connect
if(connector.connect (peer, 
					  peer_addr, 
					  &ACE_Time_Value::zero) == -1) {
	if(errno == EWOULDBLOCK) {
		// Do some other work...
		
		// Now, try to complete the connection establishment,
		// but don't block if it isn't complete yet.
		if(connector.complete(peer,
							  0,
							  &ACE_Time_Value::zero) == -1) {
			//...
		}
	}			  
}

类似的,定时的 connet() 执行如下:

ACE_Time_Value timeout(10); // Set time-out to 10 seconds
if(connector.connect(peer, 
					 peer_addr, 
					 &timeout) == -1) {
	if(errno == ETIME) {
		// Time-out, do something else...
	}
}

3.6 ACE_SOCK_IO 和 ACE_SOCK_STREAM 类

动机

Section 2.3 确定的意外复杂度域是不能检测到 socket 的错误使用。如 Section 3.1 所述,连接管理涉及三个角色:active connection rolepassive connection role,和 communication role。然而 Socket API 仅定义了两个角色:data modepassive mode。开发者可能会因此错误的使用 sockets,这在编译期无法被检测到。ACE_SOCK_Connector 类向解决这一复杂度域迈出了第一步;ACE_SOCK_Stream 类迈出了下一步。ACE_SOCK_Stream 定义了 data-mode “transfer-only” 的对象。一个 ACE_SOCK_Stream 对象不能被用于其他角色除了数据传输,这样就不能故意地违反其接口。

类功能

ACE_SOCK_Stream 封装了由 data-mode socket 支持的数据传输机制。该类提供下述功能:

  • 支持发送或接收最多 n 个字节或正好 n 个字节
  • 支持 “scatter-read” 操作,它填充多个调用方提供的缓冲,而不是单个连续的缓冲
  • 支持 “gather-write” 操作,它在单个操作中传输多个不连续的数据缓冲中的内容
  • 支持阻塞,非阻塞,和定时 I/O 操作,和
  • 支持泛型编程技术,以通过 C++ 参数化类型启用整体替换功能。

ACE_SOCK_Stream 实例由 ACE_SOCK_AcceptorACE_SOCK_Connector 工厂初始化。ACE_SOCK_StreamACE_SOCK_IO 类如图 3.7 所示。ACE_SOCK_Stream 派生自 ACE_SOCK_IOACE_SOCK_IO 派生自 ACE_SOCK 并定义了基础的数据传输方法,它们被 ACE UDP 封装外观重用。
在这里插入图片描述
ACE_SOCK_Stream 的主要方法如下:

方法描述
send() recv()传输和接收缓冲数据。读写的字节数可能少于请求的字节数,因为 OS 中缓冲以及传输协议的流控制的约束
send_n() recv_n()传输或接收正好 n 个字节的缓冲数据,以简化应用对 “short-writes” 和 “short-read” 的处理
recvv_n()使用 OS “scatter-read” 系统函数高效完全地接收多个缓冲数据
sendv_n()使用 OS “scatter-read” 系统函数高效完全地发送多个缓冲数据

ACE_SOCK_Stream 类支持阻塞,定时,和非阻塞的 I/O,阻塞 I/O 是默认的。当与对端的通讯可能被挂起或无限阻塞时,定时的 I/O 就会很有用。两种类型的 ACE_Time_Value 值能被传递到 ACE_SOCK_Stream I/O 方法来控制超时行为:

行为
NULL ACE_Time_Value 指针指示 I/O 方法应该阻塞直到数据被传输或者发送一个错误
non-NULL ACE_Time_Value 指针指示 I/O 方法应该等待相对数量的时间来传输数据。如果在数据被发送或接收,则返回一个 -1 并将 errno 设置为 ETIME

非阻塞 I/O 在不能承受数据没有立即发送或接收时的阻塞时,十分有用。阻塞和非阻塞 I/O 可以通过继承自 ACE_IPC_SAPenable()diable() 方法控制。

peer.enable(ACE_NONBLOCK); // Enable nonblocking I/O.
peer.disable(ACE_NONBLOCK); // Disable nonblocking I/O.

如果 I/O 方法在一个处于非阻塞模式的 ACE_SOCK_Stream 实例上被调用,且调用是阻塞的,则将返回 1 且 errno 被设置为 EWOULDBLOCK。

Example

// ...Connection code from example in Section 3.5 omitted...
char buf[BUFSIZ];
iovec iov[3];
iov[0].iov_base = "GET ";
iov[0].iov_len = 4; // Length of "GET "
iov[1].iov_base = pathname;
iov[1].iov_len = strlen(pathname);
iov[2].iov_base = " HTTP/1.0\r\n\r\n";
iov[2].iov_len = 13; // Length of " HTTP/1.0\r\n\r\n"

if(peer.sendv_n(iov, 3) == -1) return -1;

for(ssize_t n; (n = peer.recv(buf, sizeof buf)) > 0; )
	ACE::write_n(ACE_STDOUT, buf, n);

return peer.close();

我们使用 iovec 结构体数组以及 ACE_SOCK_Stream::sendv_n() gather-write 方法高效地向 Web 服务器传输 HTTP GET 请求。这避免了 Nagle’s 算法地性能问题。在 UNIX/POSIX 平台上这一方法使用 writev() 实现,在 WinSock2 平台上由 WSASend() 实现。


Sidebar 6: 和 Nagle‘s 算法一起工作
默认情况下,大部分的 TCP/IP 实现使用 Nagle’s 算法[Ste93],它在发送方的 TCP/IP 栈中缓冲小的,顺序发送的 packets。虽然这一算法最小化了网络拥塞,但如果你不知道何时它会产生影响以及产生什么影响,则会增加延迟,降低吞吐量。当几个小的缓冲在连续的单向缓冲被发送,就会产生这些问题;例如,下述代码会触发 Nagle’s 算法:

peer.send_n("GTE ", 4);
peer.send_n(pathname, strlen(pathname));
peer.send_n(" HTTP/1.0\r\n\r\n", 13);

应用开发者可以通过使用 TCP_NODELAY 调用 peer.enable() 禁用 Nagle’s 算法,是 TCP 尽可能快地将 packets 发送出去。client_download_file() 函数展示了更有效率的解决方案,它使用 sendv_n 在一个系统调用中传输所有的数据缓冲。该方法被传入一个 iovec 结构数组,定义如下:

struct iovec {
	// Pointer to a buffer.
	char* iov_base;
	
	// Length of buffer pointed to by <iov_base>
	int iob_len;
};

一些 OS 平台原生地定义 iovec,在其他平台上由 ACE 定义。所有情况下的成员名都相同,但是其顺序不总是一致的,所以显式地设置它们,而非通过 struct 初始化。


如果遇到 TCP 流控制或 Web 服务器的错误行为, client_download_file() 函数中的 I/O 方法将阻塞。为了避免客户端无限期挂起,我们可以添加为这些方法调用提供超时。例如,在下述代码中,如果服务器在 10s 内不能接收数据,将返回 1 并将 errno 设置为 ETIME。

// Wait no more than 10 seconds to send or receive data.
ACE_Time_Value timeout (10);
peer.sendv_n(iov, 3, &timeout);
while(peer.recv(buf, sizeof buf, &timeout) > 0)
	// ...process the contents of the downloaded file.

3.7 ACE_SOCK_Acceptor 类

动机

ACE_SOCK_ConnectorACE_SOCK_Stream 类解决了来自通讯角色与 Socket API 函数不匹配的复杂度问题。虽然 Socket API 定义一组单独的函数来满足被动连接建立角色,依然有其他的复杂度问题。Socket API 中的 C 函数是弱类型的,很容易错误地使用它们。

例如,accept() 函数可能在 data-mode socket 上调用,而 data-mode socket 用于通过 recv()send() I/O 操作进行数据传输。同样地,I/O 操作也可能在 passive-mode socket handle 工厂上调用,但是该工厂仅打算用于接受连接。不幸地是,这些错误直到运行时才能被检测到。ACE 使用强类型的 ACE_SOCK_Acceptor 类来解决这些复杂度问题。与直接使用 Socket API 调用相反,编译器在编译期能轻松地检测到 ACE_SOCK_Acceptor 的错误使用。

类功能

ACE_SOCK_Acceptor 类是一个工厂,它被动地建立一个新的通讯 endpoint。它提供以下功能:

  • 接受来自对端 connector 的连接请求,并在连接成功建立后初始化一个 ACE_SOCK_Stream 对象。
  • 连接能以阻塞,非阻塞,和定时的方式被接受。
  • 使用 C++ 特性以支持泛型编程技术,通过 C++ 参数化类型完成功能的整体替换。

ACE_SOCKE_Acceptor 的接口如图 3.8 所示,它的两个关键方法如下表所示:

方法描述
open()初始化一个 passive-mode 的工厂 socket,以在指定的 ACE_INET_Addr 地址上被动地监听
accept()使用新接受的客户端连接初始化 ACE_SOCK_Stream 参数

在这里插入图片描述
ACE_SOCK_Accpetor open()accept() 方法使用继承自 ACE_IPC_SAP 的 socket_handle。该设计利用 C++ 类型系统以避免应用开发者的意外错误使用:

  • 底层的 socket()bind(),和 listen() 函数在 ACE_SOCK_Acceptoropen() 方法中总是以正确的顺序调用。
  • 这些函数仅在被初始化为 passive-mode 的 socket 工厂上调用。

示例

#include "ace/Auto_Ptr.h"
#include "ace/INET_Addr.h"
#include "ace/SOCK_Acceptor.h"
#include "ace/SOCK_Stream.h"
#include "ace/Mem_Map.h"

// Return a dynamically allocated path name buffer.
extern char* get_url_pathname(ACE_SOCK _Stream*);

int main() {
	ACE_INET_Addr server_addr;
	ACE_SOCK_Acceptor acceptor;
	ACE_SOCK_Stream peer;
	
	if(server_addr.set(80) == -1) return -1;
	if(acceptor.open(server_addr) == -1) return -1;
	
	for(;;) {
		if(acceptor.accept(peer) == -1) return -1;
		peer->disable(ACE_NONBLOCK); // Ensure blocking <recvs>
		
		auto_ptr<char*> pathname = get_url_pathname(peer);
		ACE_Mem_Map mapped_file(pathname.get());
		
		if(peer.send_n(mapped_file.addr(), mapped_file.size()) == -1) return -1;
		peer.close();
	}
	return acceptor.close() == -1 ? 1 : 0;
}

我们的迭代服务器的主要缺点是当很多客户端同时发送请求时,且当 Web 服务器正在处理某个请求时,OS Socket 的实现仅排队很少的数量(比如,5 ~ 10) 的连接请求。在一个产品级的 Web 服务器,OS 可能很快就被压倒,此时,Web 服务器将会拒绝客户端的连接尝试,使客户端发生连接错误。


Sidebar 7:ACE_Mem_Map 类
ACE_Mem_Map 封装外观封装 Win32 和 POSIX 平台上的 memory-mapped 文件机制。这些调用使用 OS 虚拟内存机制来映射文件到进程的地址空间中。映射文件的内容能通过指针直接访问,这比使用 read 和 write 系统 I/O 函数访问数据块更加便利和有效率,内存映射文件的内容能被同一个机器上的多个进程共享,正如 Section 1.3 所述。

原生的 Win32 和 POSIX memory-mapped 文件 API 是不可移植且复杂的。例如,开发者必须手动执行很多 bookkeeping 细节,例如显式打开一个文件,确定它的长度,并执行多次映射。相反,ACE_Mem_Map 封装外观提供一个接口,通过默认值和多个构造函数 (构造函数有几个类型签名的变体) 简化常见的内存映射文件的使用,例如,“从打开的文件 handle 映射” 或 “从文件名映射”。


3.8 Summary

本章展示了 ACE 如何使用 C++ 特性和 Wrapper Facade 模式在网络应用中正确地可移植地编程面向连接的 TCP/IP 机制。我们专注于 ACE Socket 封装外观,以简化下述内容的使用:

  • 网络地址,I/O handle,和基础 socket 操作以及
  • TCP 连接建立和数据传输操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值