第十六章 网络 IPC:套接字
1、概述
套接字的原始版本是 BSD 套接字,它是通信端点的抽象。可用于同一机器上的进程间通信,典型应用为 Unix 域套接字;也可用于通信网络上任何体系结构的计算机之间的通信,典型应用为互联网套接字及在此基础上的 TCP/IP 协议栈实现。
1983 年,4.2 BSD 发布了基于套接字技术的第一个 TCP/IP 协议栈 API 实现,它成为此后其它系统TCP/IP 实现的基础。POSIX 的 socket(7)标准是在 4.4 BSD 的基础上制定,微软则于 1990 年代初期在成功移植 BSD 套接字的基础上开发了 winsock,此外使用 TCP/IP 技术进行通信的各种嵌入式系统也有诸多基于 Socket API 的移植版本。
套接字是在文件 I/O 机制的基础上实现的,包括匿名和有名两种文件形式。典型的有名套接字是/dev/log,它使用的是Unix 域套接字,守护进程syslogd(8)使用它和使用系统日志服务的客户进程通信。下面的内容除非特别注明,否则“套接字”特指匿名套接字。
用于分析 TCP/IP 协议的经典 Unix 工具包括 netcat(1)和 tcpdump(1)。前者被称为网络瑞士军刀,可以建立任意基于 TCP/IP 的网络连接并进行输入输出;后者可以把所在网络上的数据流转储到当前的标准输出,这些输出可通过管道线连接到一些文本过滤器之类的程序进行分析。
本章只讲述套接字的建立、设置、数据收发等基本接口。关于套接字机制与 TCP/IP 实现细节可参考:
- TCP/IP Illustrated Volume 2: The Implementation中文译名《TCP/IP 详解 卷 2:实现》。
基于 4.4 BSD-Lite 的套接字机制讲述 TCP/IP 实现;
- The Design and Implementation of 4.4BSD中文译名《4.4 BSD 设计与实现》。
讲述包括 Sockets 机制在内的 4.4 BSD 设计原理与实现细节;
- Understanding Linux Network Internals中文译名《深入理解 Linux 网络技术内幕》。
包括 Linux 环境的网络实现细节及解决方案。暂无简体中文版。
关于 TCP/IP 协议及应用可参考:
- TCP/IP Illustrated Volume 1: The Protocols
中文译名为《TCP/IP 详解 卷 1:协议》。讲述 TCPIP 协议族的体系结构及细节;
- UNIX Network Programming中文译名为《UNIX 网络编程》。
其中第二版分为两卷,第一卷 The Sockets Networking API(中文译名:《套接口 API》)讲述了 Sockets 编程的细节;
- Internetworking With TCP/IP Vol Ⅲ:Client-Server Programming And Applications
中文译名为《用 TCP/IP 进行网际互联 第三卷:客户-服务器编程与应用》。讲述 C/S程序设计的典型模型与应用;
- RFC
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain 为通信域选项,即地址族(Address Family)。
- AF_UNIX(AF_LOCAL) Unix 域
- AF_INET ipv4 因特网域
- AF_INET6 ipv6 因特网域
- SOCK_STREAM 面向流的套接字,默认为 TCP 协议;
- SOCK_DGRAM 面向数据报的套接字,默认为 UDP 协议;
- SOCK_RAW 访问 IP 层的数据报接口,用户自行构造协议,此选项需要 root 权限;
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t len);
int setsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict len);
level 表明 option 应用在哪个协议上,例如:
- SOL_SOCKET 套接字通用选项;
- IPPROTO_TCP TCP 协议选项;
- IPPROTO_IP IP 协议选项;
#include <sys/socket.h>
int shutdown(int sockfd, int how);
how 包括 SHUT_RD、SHUT_WR、SHUT_RDWR;
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
uint32_t htons(uint32_t hostint16);
uint32_t ntohl(uint32_t netint32);
uint32_t ntohs(uint32_t netint16);
异种体系结构的不同字节顺序同时也带来带有位操作的程序的可移植性问题,移植时需要特别注意。
#include <sys/types.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
对 addr 的成员有如下限制:
- 必须使用本地地址;
- 必须与 socket(2)创建时的 domain 格式匹配;
- 只有 root 进程的端口号可以小于 1024;
- 对于 AF_INET,如果 IP 地址为 INADDR_ANY,则 sockfd 绑定到本地系统的全部链路层接口;
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
查找和转换进程地址标识信息的传统API包括:
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *restrict host,const char *restrict service,const struct addrinfo *restrict hint,struct addrinfo **restrict res);
int getnameinfo(const struct sockaddr *restrict addr,socklen_t alen, char *restrict host,socklen_t hostlen, char *restrict service,socklen_t servlen, unsigned int flags);
getaddrinfo(3)通过给定的参数 host, service, hint 填充结构 res。
- 其中 host 的字符串可以是主机名或者点分十进制 IP 地址;
- service 为标准的服务名;
- hint 只使用 ai_flags, ai_family, ai_socktype, ai_protocol 这几个成员,其它成员必须设为 0 或者NULL;
- res 是指向一个 addrinfo 结构的链表首址,它的后趋结点的指针为 ai_next;
- 释放这个链表的函数为 freeaddrinfo(3);
#include <netdb.h>
const char *gai_strerror(int error);
5、基于套接字的数据传输
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
该函数请求套接字sockfd连接到地址标识addr 端对应的套接字。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
listen 将套接字设置为监听模式,backlog 指定了等待队列的最大长度,超过此长度的连接请求将直接被拒绝;
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
send(2)除了带一个标志参数外,其它用法同 write(2)。flags 有 4 个选项,包括
- MSG_DONTROUTE 该套接字仅在本地使用
- MSG_DONTWAIT 使套接字非阻塞
- MSG_EOR 表明发送记录结束
- MSG_OOB 表明发送带外数据(若协议支持)
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recv(2)可用的标志包括
- MSG_OOB 表明接收带外数据(若协议支持)
- MSG_PEEK 只查看数据而不取出队;
- MSG_TRUNC 要求返回报文的实际长度而不是实际收到的长度;
- MSG_WAITALL 等待到数据都已可用(对于可靠连接的 SOCK_STREAM 而言)
fcntl(sockfd, F_SETOWN, pid);
还可以使用fcntl(2)实现基于套接字的异步 I/O:
fcntl(sockfd, F_SETFL, O_ASYNC);
在 sockfd 的 I/O 可用时,进程将收到 SIGIO 信号。这样,进程就可以在 SIGIO 的信号捕捉函数中处理 sockfd 的 I/O。