16.1 引言
本章将考察不同计算机(通过网络相连)上的进程相互通信的机制:网络进程间通信(network IPC)。
16.2 套接字描述符
为创建一个套接字,调用socket
函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 返回值:若成功,返回文件(套接字描述符);若出错,返回-1
- 参数domain(域)确定通信的特征,包括地址格式,POSIX.1指定的各个域如下:
- 参数type确定套接字的类型,进一步确认通信特征,POSIX.1定义的套接字类型如下:
- 参数protocol通常为0,表示为给定的域和套接字类型选择默认协议,因特网域套接字定义的协议如下:
套接字通信是双向的,可以采用shutdown
来禁止一个套接字的I/O:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
// 返回值:若成功,返回0;若出错,返回-1
- 参数how的可能值有3种:
- SHUT_RD(关闭读端),那么无法从套接字读取数据;
- SHUT_WR(关闭写端),那么无法使用套接字发送数据;
- SHUT_RDWR(关闭读写端),既无法读取数据,又无法发送数据。
16.3 寻址
进程标识由两部分组成:
- 网络地址:标识网络上想与之通信的计算机;
- 端口号:计算机上用端口号表示服务,用于标识特定的进程。
16.3.1 字节序
- 大端字节序:最大字节地址出现在最低有效字节(Least Significant Byte,LSB)上;
- 小端字节序:最大字节地址出现在最高有效字节(Most Significant Byte,MSB)上。
对于TCP/IP应用程序,有4个用来在处理器字节序和网络字节序之间实施转换的函数:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
// 返回值:以网络字节序表示的32位整数
uint16_t htons(uint16_t hostint16);
// 返回值:以网络字节序表示的16位整数
uint32_t ntohl(uint32_t netint32);
// 返回值:以主机字节序表示的32位整数
uint16_t ntohs(uint16_t netint16);
// 返回值:以主机字节序表示的16位整数
- h表示“主机”字节序,n表示“网络”字节序;
- l表示“长”(即4字节)整数,s表示“短”(即2字节)整数。
16.3.2 地址格式
为使不同格式地址能够传入到套接字函数,地址会被强制转换成一个通用的地址结构 sockaddr:
struct sockaddr {
sa_family_t sa_family; /* address family */
char sa_data[]; /* variable-length address */
...
};
在IPv4因特网域(AF_INET)中,套接字地址用结构 sockaddr_in 表示:
struct in_addr {
in_addr_t s_addr; /* IPv4 address */
};
struct sockaddr_in {
sa_family_t sin_family; /* address family */
in_port_t sin_port; /* port number */
struct in_addr sin_addr; /* IPv4 address */
};
IPv6因特网域(AF_INET6)套接字地址用结构 sockaddr_in6 表示:
struct in6_addr {
uint8_t s6_addr[16]; /* IPv6 address */
};
struct sockaddr_in6 {
sa_family_t sin6_family; /* address family */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* traffic class and flow info */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* set of interfaces for scope */
};
inet_ntop
和inet_pton
用于二进制地址格式与点分十进制字符(a.b.c.d)之间的相互转换:
#include <arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr,
char *restrict str, socklen_t size);
// 返回值:若成功,返回地址字符串指针;若出错,返回NULL
int inet_pton(int domain, const char *restrict str,
void *restrict addr);
// 返回值:若成功,返回1;若格式无效,返回0;若出错,返回-1
- 参数 domain 仅支持两个值:AF_INET 和 AF_INET6;
- 对于
inet_ntop
,参数size指定了保存文本字符串的缓冲区(str)的大小,INET_ADDRSTRLEN、INET6_ADDRSTRLEN分别定义了足够大的空间来存放一个表示IPv4和IPv6地址的文本字符串;
16.3.3 地址查询
通过调用gethostent
,可以找到给定计算机系统的主机信息:
#include <netdb.h>
struct hostent *gethostent(void);
// 返回值:若成功,返回指针;若出错,返回NULL
void sethostent(int stayopen);
void endhostent(void);
- 如果主机数据库文件没有打开,
gethostent
会打开它,函数gethostent
返回文件中的下一个条目,得到一个指向 hostent 结构的指针,该结构至少包含以下成员:
struct hostent {
char *h_name; /* name of host */
char **h_aliases; /* pointer to alternate host name array */
int h_addrtype; /* address type */
int h_length; /* length in bytes of address */
char **h_addr_list; /* pointer to array of network addresses */
};
- 函数
sethostent
会打开文件,如果文件已经被打开,那么将其回绕;当stayopen参数设置成非0值时,调用gethostent
之后,文件将依然是打开的; - 函数
endhostent
可以关闭文件。
#include <netdb.h>
struct netent *getnetbyaddr(uint32_t net, int type);
struct netent *getnetbyname(const char *name);
struct netent *getnetent(void);
// 3个函数的返回值:若成功,返回指针;若出错,返回NULL
void setnetent(int stayopen);
void endnetent(void);
netent 结构至少包含以下字段:
struct netent {
char *n_name; /* network name */
char **n_aliases; /* alternate network name array pointer */
int n_addrtype; /* address type */
uint32_t n_net; /* network number */
...
};
下列函数在协议名字和协议编号之间进行映射:
#include <netdb.h>
struct protoent *getprotobyname(const char *name);
struct protoent *getprotobynumber(int proto);
struct protoent *getprotoent(void);
// 3个函数的返回值:若成功,返回指针;若出错,返回NULL
void setprotoent(int stayopen);
void endprotoent(void);
POSIX.1定义的 protoent 结构至少包含以下成员:
struct protoent {
char *p_name; /* protocol name */
char **p_aliases; /* pointer to altername protocol name array */
int p_proto; /* protocol number */
...
};
服务是由地址的端口号部分表示的,每个服务由一个唯一的众所周知的端口号来支持。函数getservbyname
将一个服务名映射到一个端口号,函数getservbyport
将一个端口号映射到一个服务名,函数getservent
顺序扫描服务数据库:
#include <netdb.h>
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
struct servent *getservent(void);
// 3个函数的返回值:若成功,返回指针;若出错,返回NULL
void setservent(int stayopen);
void endservent(void);
servent 结构至少包含以下成员:
struct servent {
char *s_name; /* service name */
char **s_aliases; /* pointer to alternate service name array */
int s_port; /* port number */
char *s_proto; /* name of protocol */
...
};
getaddrinfo
函数允许将一个主机名和一个服务名映射到一个地址:
#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);
// 返回值:若成功,返回0;若出错,返回非0错误码
void freeaddrinfo(struct addrinfo *ai);
- 需要提供主机名、服务名,或者两者都提供;如果仅提供一个名字,另一个必须是一个空指针;主机名可以是一个节点名或点分格式的主机地址;
getaddrinfo
函数返回一个链表结构 addrinfo,可以用freeaddrinfo
来释放一个或多个这种结构,这取决于用 ai_next 字段链接起来的机构由多少;addrinfo 结构的定义至少包含以下成员:
struct addrinfo {
int ai_flags; /* customize behavior */
int ai_family; /* adddress family */
int ai_sockettype; /* socket type */
int ai_protocol; /* protocol */
socklen_t ai_addrlen; /* length in bytes of address */
struct sockaddr *ai_addr; /* address */
char *ai_canonname; /* canonical name of host */
struct addrinfo *ai_next; /* next in list */
...
};
- 可以提供一个可选的hint来选择符合特定条件的地址,hint是一个用于过滤地址的模板,包括ai_family、ai_flags、ai_protocol和ai_socktype字段;剩余的整数字段必须设置为0,指针字段必须为空;下图总结了ai_flags字段中的标志:
如果getaddrinfo
失败,不能使用perror
或strerror
来生成错误消息,而是要调用gai_strerror
将返回的错误码转换成错误消息:
#include <netdb.h>
const char *gai_strerror(int error);
// 返回值:指向描述错误的字符串的指针
getnameinfo
函数将一个地址转换成一个主机名和一个服务名:
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *restrict addr, socklen_t alen,
char *restrict host, socklen_t hostlen,
char *restrict service, socklen_t servlen, int flags);
// 返回值:若成功,返回0;若出错,返回非0值
- flags参数提供了一些控制翻译的方式,支持的标志如下:
16.3.4 将套接字与地址关联
对于服务器,需要给一个接收客户端请求的服务器套接字关联上一个众所周知的地址;使用bind
函数来关联地址和套接字:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
// 返回值:若成功,返回0;若出错,返回-1
可以调用getsockname
函数来发现绑定到套接字上的地址:
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict alenp);
// 返回值:若成功,返回0;若出错,返回-1
如果套接字已经和对等方连接,可以调用getpeername
函数来找到对方的地址:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *restrict addr
socklen_t *restrict alenp);
// 返回值:若成功,返回0;若出错,返回-1
16.4 建立连接
如果要处理一个面向连接的网络服务(SOCK_STREAM 或 SOCK_SEQPACKET),那么在开始交换数据以前,需要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之前建立一个连接;使用connect
函数来建立连接:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
// 返回值:若成功,返回0;若出错,返回-1
- 在
connect
中指定的地址是我们想与之通信的服务器地址; - 如果sockfd没有绑定到一个地址,
connect
会给调用者绑定一个默认地址。
服务器调用listen函数来宣告它愿意接受连接请求:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 返回值:若成功,返回0;若出错,返回-1
- 参数backlog提示系统该进程所要入队的未完成连接请求数量。
服务器使用accept
函数获得连接请求并建立连接:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict len);
// 返回值;若成功,返回文件(套接字)描述符;若出错,返回-1
- 如果不关心客户端标识,可以将参数 addr 和 len 设置为NULL。
16.5 数据传输
send
函数用于发送数据,它可以指定标志来改变处理传输数据的方式:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
// 返回值:若成功,返回发送的字节数;若出错,返回-1
- flags参数指定了套接字调用标志,如下:
sendto
函数在无连接的套接字上指定一个目标地址:
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,
const struct sockaddr *destaddr, socklen_t destlen);
// 返回值:若成功,返回发送的字节数;若出错,返回-1
可以调用带有msghdr结构的sendmsg
来指定多重缓冲区传输数据:
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
// 返回值:若成功,返回发送的字节数;若出错,返回-1
POSIX.1定义了msghdr结构,它至少有以下成员:
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* address size in bytes */
struct iovec *msg_iov; /* array of I/O buffers */
int msg_iovlen; /* number of elements in array */
void *msg_control; /* ancillary data */
socklen_t msg_controllen; /* number of ancillary bytes */
int msg_flags; /* flags for received message */
...
};
函数recv
可以指定标志来控制如何接收数据:
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
// 返回值:返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1
- 下图总结了这些标志:
可以使用recvfrom
来得到数据发送者的源地址:
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
struct sockaddr *restrict addr,
socklen_t *restrict addrlen);
// 返回值:返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1
- 如果addr非空,它将包含数据发送者的套接字端点地址。
为了将接收到的数据送入多个缓冲区,类似于readv
,或者想接收辅助数据,可以使用recvmsg
:
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
// 返回值:返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1
- msghdr结构指定接收数据的输入缓冲区;
- 可以设置flags来改变
recvmsg
的默认行为;返回时,msghdr结构中的msg_flags字段被设为所接收数据的各种特征(进入recvmsg
时msg_flags被忽略);recvmsg
中返回的各种可能值如下:
16.6 套接字选项
可以使用setsockopt
函数来设置套接字选项:
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len);
// 返回值:若成功,返回0;若出错,返回-1
- 参数level标识了选项应用的协议;如果选项是通用的套接字层次选项,则level设置成
SOL_SOCKET
;否则,设置成控制这选项的协议编号,对于TCP选项,level是IPPROTO_TCP
,对于IP,level是IPPROTO_IP
;下图总结了Single UNIX Specification中定义的通用套接字选项:
- 参数val根据选项的不同指向一个数据结构或者一个整数;一些选项是on/off开关,如果整数非0,则启用选项,如果整数为0,则禁止选项;
- 参数len指定了val指向对象的大小。
可以使用getsockopt
函数来查看选项的当前值:
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict len);