Chapter 2. Socket API 概览
文章目录
早先的章节专注于网络应用通讯
设计领域
的模式化,提供了很多解决内在固有的复杂度的指导。本章开始深入
解决方法领域
,从最常见的进程间通讯 (IPC) 机制开始:
socket。描述 Socket API 和它在 TCP/IP 中的使用,随后分析当开发者使用原生 OS APIs 进行网络应用编程时会发生的常见意外复杂度。
2.1 操作系统 IPC 机制的概览
网络应用需要进程间通讯机制进行客户端和服务器之间的信息交换。IPC 机制由操作系统提供,通常被分为两类:
- Local IPC 某些 IPC 机制,例如共享内存,管道,UNIX-domain sockets,doors,或信号,仅用于在相同计算机上的实体间进行通讯。
- Remote IPC 其他 IPC 机制,例如 Internet-domain sockets,X.25 circuits,和 Win32 命名管道,支持跨网络分布的实体间的通讯。
虽然网络应用主要关注远程 IPC 机制,我们所展现的模式和 ACE 封装外观也应用于大部分的本地 IPC 机制。
2.2 Socket API
BSD UNIX 开发的 Socket API 提供对 TCP/IP 协议套件的应用层接口。这一 API 已经被移植到了大部分的操作系统中。它现在是在 TCP/IP 上进行进程间通讯编程的事实上的标准。
应用可以使用 Socket API 中的 C 函数来创建或管理本地通讯 endpoints,被称为 sockets。每个 socket 通过一个 handle 访问,在 UNIX 文章中它也被引用为描述符。Socket handle 标识一个由 OS 管理的通讯 endpoint,同时保护应用远离底层 OS 内核实现细节的不同,以及对其的依赖,例如:
- 在 UNIX 中,socket handles 和其他 I/O handles,例如文件,pipe,和终端设备 handles,在大多数操作中可以交换使用。
- 在 Microsoft Windows 中,socket handles 与 I/O handles 对于大部分操作来说不能互换使用,虽然它们服务于类似的目的。
每个 socket 都能被绑定到本地和远程地址。这些地址定义两个和多个通过 socket 通讯的 peer 之间的关联。
Socket API 包含大约 24 个系统函数,能被分类为下列 5 类:
- Local context management. Socket API 提供函数以管理本地上下文信息,这些信息通常存储于 OS 内核或系统库中。
函数 | 描述 |
---|---|
Socket() | 分配 socket handle 并向调用者返回的工厂函数 |
bind() | 关联 socket handle 到本地或远程地址 |
getsockname() | 返回 socket 绑定的本地地址 |
getpeername() | 返回 socket 绑定的远程地址 |
close() | 解分配一个 socket handle,使其能够被重用 |
- Connection establishment and connection termination. Socket API 提供函数用于建立和终止连接:
函数 | 描述 |
---|---|
Connect() | 在 socket handle 上主动建立连接 |
listen() | 表示愿意被动地监听到来的客户端连接请求 |
Accept() | 为服务连接请求而创建一个新的通讯 endpoint 的工厂函数 |
Shutdown() | 选择性地终止双向连接的 read-side 流和/或 write-side 流 |
- Data transfer mechanisms. Socket API 提供通过 socket 发送和接收数据的功能:
函数 | 描述符 |
---|---|
send() recv() | 通过特定的 I/O handle 传输和接收数据缓冲 |
sendto() recvfrom() | 交换无连接的数据报,每个 sendto() 调用都提供接收方的网络地址 |
sendmsg() recvmsg() | 通用目的的函数,包含其他数据传输函数的行为 |
- Options management. Socket API 定义函数,允许程序员改变默认的 socket 行为以启用 multicasting,broadcasting,和修改/查询运输缓冲的大小。
函数 | 描述 |
---|---|
setsockopt() | 修改不同协议栈层的选项 |
getsockopt() | 查询不同协议栈层的选项 |
- Network addressing. 除了上述描述的函数,网络应用通常使用函数来解析人类可读的名字,例如,tango.ece.uci.edu,到底层的网络地址,例如 128.195.174.35:
函数 | 描述 |
---|---|
gethostbyname() gethostbyaddr() | 处理在主机名和 IPv4 地址之间的网络地址映射 |
getipnodebyname() getipnodebyaddr() | 处理主机名和 IPv4/IPv6 地址之间的网络地址映射 |
getservbyname() | 通过人类可读的名字标识服务 |
虽然 Socket API 总被用于写 TCP/IP 应用,但它足够广泛,能支持多种通讯域。通讯域由一个协议族和一个地址族定义,如下:
- Protocol family. 如今的网络环境包含大量的协议,能提供多种通讯服务,例如面向连接的可靠的递交,不可靠的多播,等。
协议族是协议的集合
,提供一组不同的相关服务。当使用 Socket API 创建 socket 时,协议由下述两个参数结合指定:- Protocol family 例如,UNIX-domain(PF_UNIX),Internet-domain IPv4(PF_INET),ATM(PF_ATMSVC),X.25(PF_x25),Appletalk(PF_APPLETALK), 等。
- Service type 例如,序列化的可靠字节流 (SOCK_STREAM),不可靠的 datagram (SOCK_DGRAM),等。
例如,TCP/IP 协议通过传入 PF_INET (或 PF_INET6) 和 SOCK_STREAM 标识到 socket() 函数中指定。
- Address family.
地址族定义地址的格式
,以字节为单位描述地址的大小,以及地址的字段的数字,类型和顺序。另外,地址族定义了一组函数来解释地址格式,例如,为了确定 IP datagram 的目的子网。地址族和协议族密切对应,例如,IPv4 地址族 AF_INET 仅和 IPv4 协议族 PF_INET 一起工作。
2.3 Socket API 的限制
原生 Socket API 有几个限制:容易出错,极度复杂,不可移植/不统一。虽然下述的讨论专注于 Socket API,评论同样也应用于其他原生的 OS IPC APIs。
2.3.1 Error-Prone APIs
正如 Section 2.2 描述的那样,Socket API 使用 handles 标识 socket endpoint。通常,操作系统也使用 handle 标识其他的 I/O 设备,比如文件,pipes,和 terminal。这些 handle 使用 weakly typed 的整型或指针类型实现,这会导致隐晦的错误发生在运行期间。为了解释可能发生的这些或其他问题,考虑下述 echo_server() 函数:
// This example contains bugs! Do not copy this example!
#include <syc/types.h>
#include <sys/socket.h>
const int PORT_NUM = 10000;
int echo_server() {
struct sockaddr_in addr;
int addr_len;
cahr buf[BUFSIZ];
int n_handle;
// Create local endpoint
int s_handle = socket(PF_UNIX, SOCK_DGRAM, 0);
if(s_handle == -1) return -1;
// Set up the address information where the server listens.
addr.sin_family = AF_INET;
addr.sin_port = PORT_NUM;
addr.sin_addr.addr = INADDR_ANY;
if(bind(s_handle, (struct sockaddr *) &addr, &addr_len) == -1)
return -1;
// Create a new communication endpoint.
if (n_handle = accept(s_handle, (struct sockaddr *) & addr, &addr_len) != -1) {
int n;
while ((n== read(s_handle, buf, sizeof buf)) > 0)
write (n_handle, buf, n);
close(n_handle);
}
return 0;
}
2.3.2 极度复杂的 API
Socket API 提供支持多个功能的单个接口:
- Protocol families, 例如 TCP/IP,IPX/SPX,ISO OSI,ATM,和 UNIX-domian sockets
- Communication/connection 角色,例如 active connection establishment vs passive connection extablishment vs 数据传输
- Communication optimizations, 例如集中写入函数,
writev()
,在单个系统函数中发送多个缓冲,以及 - Options 对于不太常见的功能,比如 broadcasting,multicasting,异步 I/O,和紧急数据递交。
Socket 将所有的这些功能组合成一个 API,在 Section 2.2 中的表中被列出来。结果很复杂难以掌握。如果你对 socket API 进行小心的分析,你会看到它的接口可以被解耦成下列维度:
- Type of communication service,例如流 vs 数据报 vs 连接的数据报。
- Communication/connection role; 例如,客户端总是主动初始化连接,服务器总是被动的接受它们。
- Communication domain, 例如仅本地主机 vs 本地和远程主机。
图 2.1 依据这三个维度集群了相关的 Socket 函数。然而,这种自然的聚类在 Socket API 中是模糊的,因为所有的这些功能都被塞到了一组函数中。进一步来说,Socket API 不能在编译期保证其对不同通讯和连接的功能的正确使用,例如主动 vs 被动连接的建立,或数据报 vs 流式通讯。