第十六章 网络IPC:套接字
16.2 套接字描述符
套接字是通信端点的抽象。套接字描述符在UNIX系统中被当作是一种文件描述符。
使用socket函数创建一个套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//返回值:若成功,返回套接字描述符;若出错,返回 -1
参数domain(域)确定通信的特性:
AF_INET:IPv4因特网域
AF_INET6:IPv6因特网域
AF_LOCAL、AF_UNIX:UNIX域
AF_UPSPEC:未指定,“任何”域
参数type确定套接字的类型,进一步确定通信特征。
SOCK_DGRAM:固定长度的、无连接的、不可靠的报文传递
SOCK_RAW:IP协议的数据报接口
SOCK_SEQPACKET:固定长度的、有序的、可靠的、面向连接的报文传递
SOCK_STREAM:有序的、可靠的、双向的、面向连接的字节流
参数protocol通常是0,表示为给定的域和套接字类型选择默认协议。
IPPROTO_IP:IPv4网际协议
IPPROTO_IPV6:IPv6网际协议
IPPROTO_ICMP:因特网控制报文协议
IPPROTO_RAW:原始IP数据包协议
IPPROTO_TCP:传输控制协议
IPPROTO_UDP:用户数据报协议
套接字通信是双向的 ,可以采用 shutdown 函数来禁止一个套接字的 I/O
#include <sys/socket.h>
int shutdown(int sockfd, int how);
//返回值:若成功,返回 0;若出错,返回 -1
how参数取值:
SHUT_RD:关闭读端
SHUT_WR:关闭写端
SHUT_RDWR:关闭读写端
16.3 寻址
16.3.1 字节序
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32); //返回:以网络字节序表示的 32 位整数
uint64_t htons(uint16_t hostint16); //返回:以网络字节序表示的 16 位整数
uint32_t ntohl(uint32_t netint32); //返回:以主机字节序表示的 32 位整数
uint16_t ntohs(uint16_t netint16); //返回:以主机字节序表示的 16 位整数
16.3.2 地址格式
为使不同格式地址能够传入到套接字函数,地址会被强制转换成一个通用的地址结构sockaddr
struct sockaddr {
sa_family_t sa_family; /* address family */
char sa_data[]; /* variable-length address */
};
因特网地址定义在<netinet/in.h>头文件中。
在IPv4因特网域中,套接字地址用结构 sockaddr_in表示:
struct in_addr_t {
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因特网域套接字地址用结构 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
函数inet_ntop将网络字节序的二进制地址转换成文本字符串格式。函数inet_pton将文本字符串格式转换成网络字节序的二进制地址。
参数domain仅支持两个值:AF_INET和AF_INET6。
两个常数用于简化工作:
INET_ADDRSTRLEN:定义了足够大的空间来存放一个表示IPv4地址的文本字符串。
INET6_ADDRSTRLEN:定义了足够大的空间来存放一个表示IPv6地址的文本字符串。
16.3.3 地址查询
通过调用gethostent,可以找到给定计算机系统的主机信息
#include <netdb.h>
struct hostent *gethostent(void);
//返回值:若成功,返回指针;若出错,返回 NULL
void sethostent(int stayopen);
void endhostent(void);
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 **haddr_list; /* pointer to array of network addresses */
};
如果主机数据库文件没有打开,gethostent会打开它。函数 gethostent返回文件中的下一个条目。函数 sethostent会打开文件,如果文件已经被打开,那么将其回绕。当stayopen参数设置成非0值时,调用gethostent之后,文件将依然是打开的。函数endhostent可以关闭文件。
返回的地址采用网络字节序。
一套相似的接口来获得网络名字和网络编号。
#include <netdb.h>
struct netent *getnetbyaddr(uint32_t net, int bype);
struct netent *getnetbyname(const char *name);
struct netent *getnetent(void);
//3个函数的返回值:若成功,返回指针;若出错,返回 NULL
void setnetent(int stayopen);
void endnetent(void);
struct netent {
char *n_name; /* network name */
char **naliases; /* 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);
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 *getserbyport(int port, const char *proto);
struct servent *getservent(void);
///3个函数的返回值:若成功,返回指针,若出错,返回NULL
void setservent(int stayopen);
void endservent(void);
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 nes);
//返回值:若成功,返回0;若出错,返回非0错误码
void freeaddrinfo(struct addrinto *ai);
struct addrinfo {
int ai_flags; /* customize behavior */
int ai_family; /* address family */
int ai_socktype; /* 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 */
}
主机名可以是一个节点名或点分格式的主机地址。
getaddrinfo函数返回一个链表结构addrinfo。
freeaddrinfo函数释放一个或多个这种结构。
可以提供一个可选的hint来选择符合特定条件的地址。hint是一个用于过滤地址的模板,包括ai_family、ai_flags、ai_protocol和ai_socktype字段。剩余的整数字段必须设置为0,指针字段必须为空。
ai_family:
AI_ADDRCONEIG:查询配置的地址类型(IPv4或IPv6)
AI_ALL:查找IPv4和IPv6地址(仅用于AI_V4MAPPED)
AI_CANONNAME:需要一个规范的名字(与别名相对)
AI_NUMERICHOST:以数字格式指定主机地址,不翻译
AI_NUMERICSERV:将服务指定为数字端口号,不翻译
AI_PASSIVE:套接字地址用于监听绑定
AI_V4MAPPED:如没有找到IPv6地址,返回映射到IPv6格式的IPv4地址
如果getaddrinfo失败要调用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值
flag标志:
NI_DGRAM:服务基于数据报而非基于流
NI_NAMEREQD:如果找不到主机名,将其作为一个错误对待
NI_NOFQDN:对于本地主机,仅返回全限定域名的节点名部分
NI_NUMERICHOST:返回主机地址的数字形式,而非主机名
NI_NUMERICSCOPE:对于Pv6,返回范围ID的数字形式,而非名字
NI_NUMERICSERV:返回服务地址的数字形式(即端口号),而非名字
16.3.4 将套接字与地址关联
使用bind函数来关联地址和套接字
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
//返回值:若成功,返回 0;若出错,返回 -1
对于因特网域,如果指定IP地址为INADDR_ANY,套接字端点可以被绑定到所有的系统网络接口上。
可以调用getsockname函数来发现绑定到套接字上的地址。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
//返回值:若成功,返回 0;若出错,返回 -1
返回时,alenp指向的整数会被设置成返回地址的大小。
如果套接字已经和对等方连接,可以调用getpeername函数来找到对方的地址。
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
//返回值:若成功,返回 0;若出错,返回 -1
16.4 建立连接
使用connect函数来建立连接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
//返回值:若成功,返回 0;若出错,返回 -1
在connect中指定的地址是我们想与之通信的服务器地址。如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。
可迁移的应用程序需要关闭套接字。
若套接字处于非阻塞模式,那么在连接不能马上建立时,connect会返回−1并将errno设置为特殊的错误码EINPROGRESS。应用程序可以使用poll或者select来判断套接字描述符何时可写,如果可写,连接完成。
connect函数还可以用于无连接的网络服务(SOCK_DGRAM),传送的报文的目标地址会设置成connect调用中指定的地址,这样每次传送报文时就不需要再提供地址。另外,仅能接收来自指定地址的报文。
服务器调用listen函数来宣告它愿意接受连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//返回值:若成功,返回 0;若出错,返回 -1
参数backlog提供了一个提示,提示系统该进程要入队的未完成连接请求数量。
一旦服务器调用了listen,所用的套接字就能接收连接请求。使用accept函数获得连接请求并建立连接
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
//返回值:若成功,返回套接字描述符;若出错,返回 -1
如果没有连接请求在等待,accept会阻塞直到一个请求到来。如果sockfd处于非阻塞模式,accept会返回-1,并将errno设置为EAGAIN或EWOULDBLOCK。
16.5 数据传输
只要建立连接,就可以使用read和write来通过套接字通信。
send函数
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
//返回值:若成功,返回发送的字节数;若出错,返回 -1
使用send时套接字必须已经连接。
flag参数:
MSG_CONEIRM:提供链路层反馈以保持地址映射有效
MSG_DONTROUTE:勿将数据包路由出本地网络
MSG_DONTWAIT:允许非阻塞操作
MSG_EOF:发送数据后关闭套接字的发送端
MSG_EOR:如果协议支持,标记记录结束
MSG_MORE.:延迟发送数据包允许写更多数据
MSG_NOSIGNAL:在写无连接的套接字时不产生SIGPIPE信号
MSG_00B:如果协议支持,发送带外数据
对于支持报文边界的协议,如果尝试发送的单个报文的长度超过协议所支持的最大长度,那么send会失败,并将errno设为EMSGSIZE。对于字节流协议,send会阻塞直到整个数据传输完成。
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 sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);
//返回值:若成功,返回发送的字节数;若出错,返回 -1
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
flag参数:
MSG_DONTWAIT:启用非阻塞操作
MSG_ERRQUEUE:接收错误信息作为辅助数据
MSG_00B:如果协议支持,获取带外数据
MSG_PEEK:返回数据包内容而不真正取走数据包
MSG_TRUNC:即使数据包被截断,也返回数据包的实际长度
MSG_WAITALL:等待直到所有的数据可用(仅SOCK_STREAM)
如果发送者已经调用shutdown来结束传输,或者网络协议支持按默认的顺序关闭并且发送端已经关闭,那么当所有的数据接收完毕后,recv会返回0。
使用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
进入recvmsg时msg_flags被忽略,使用参数flags代替
MSG_CTRUNC:控制数据被截断
MSG_EOR:接收记录结束符
MSG_ERRQUEUE:接收错误信息作为辅助数据
MSG_00B:接收带外数据
MSG_TRUNC:一般数据被截断
16.6 套接字选项
可以使用setsockopt函数来设置套接字选项。
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int option, const void *val, socklent len);
//返回值:若成功,返回0:若出错,返回-1
参数level标识了选项应用的协议。如果选项是通用的套接字层次选项,则level设置成SOL_SOCKET。否则,level设置成控制这个选项的协议编号。对于TCP选项,level是IPPROTO_TCP,对于IP,level是rpPROTO_IP。
选项 | 参数val的类型 | 描述 |
---|---|---|
SO_ACCEPTCONN | int | 返回信息指示该套接字是否能被监听(仅getsockopt) |
SO_BROADCAST | int | 如果*val非0,广播数据报 |
SO_DEBUG | int | 如果*val非0,启用网络驱动调试功能 |
SO_DONTROUTE | int | 如果*va1非0,绕过通常路由 |
SO_ERROR | int | 返回挂起的套接字错误并清除(仅 getSOckopt) |
SO_KEEPALIVE | int | 如果*val非0,启用周期性keep-alive报文 |
SO_LINGER | struct 1inger | 当还有未发报文而套接字已关闭时,延迟时间 |
SO_OOBINLINE | int | 如果*val非0,将带外数据放在普通数据中 |
SO_RCVBUE | int | 接收缓冲区的字节长度 |
SO_RCVLOWAT | int | 接收调用中返回的最小数据字节数 |
SO_RCVTIMEO | struct timeval | 套接字接收调用的超时值 |
SO_REUSEADDR | int | 如果*val非0,重用bind中的地址 |
SO_SNDBUE | int | 发送缓冲区的字节长度 |
SO_SNDLOWAT | int | 发送调用中传送的最小数据字节数 |
SO_SNDTIMEO | struct timeval | 套接字发送调用的超时值 |
SO_TYPE | int | 标识套接字类型(仅getsockopt) |
可以使用getsockopt函数来查看选项的当前值。
#include <sys/socket.h>
int getsockopt(int socyd, int level, int option, void *restrict val, socklen_t *restrict lenp);
//返回值:若成功,返回0;若出错,返回-1
16.7 外带数据
带外数据允许更高优先级的数据传输。带外数据先行传输,即使传输队列已经有数据。TCP支持带外数据,但是UDP不支持。
TCP将带外数据称为紧急数据,仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输。
为了产生紧急数据,可以在3个send函数中的任何一个里指定MSG_OOB标志。如果带MSG_OOB标志发送的字节数超过一个时,最后一个字节将被视为紧急数据字节。
如果通过套接字安排了信号的产生,那么紧急数据被接收时,会发送SIGURG信号。
可以通过调用以下函数安排进程接收套接字的信号:fcntl(sockfdm F_SETOWNM, pid);
TCP支持在普通数据流中紧急数据所在的位置。如果采用套接字选项SO_OOBINLINE,那么可以在普通数据中接收紧急数据。为帮助判断是否已经到达紧急标记,可以使用函数sockatmark。
#include <sys/socket.h>
int sockatmark(int sockfd);
//返回值:若在标记处,返回1;若没在标记处,返回0;若出错,返回-1
当下一个要读取的字节在紧急标志处时,sockatmark返回1。
16.8 非阻塞和异步I/O
在基于套接字的异步VO中,当从套接字中读取数据时,或者当套接字写队列中空间变得可用时,可以安排要发送的信号SIGIO。
fcntl(fd, F_SETOWN, pid)
ioctl(fd, FIOSETOWN, pid)
ioctl(fd, SIOCSPGRP, pid)
fcntl(fd, F_SETFL, flags l O_ASYNC)
ioctl(fd, FIOASYNC, &n);