本笔记参考了BrianLeeLXT的笔记
第十六章 网络IPC:套接字
16.2 套接字描述符
套接字是通信端点的抽象。套接字描述符在UNIX系统中被当作是一种文件描述符。
使用socket函数创建一个套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数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);
how参数取值: SHUT_RD:关闭读端 SHUT_WR:关闭写端 SHUT_RDWR:关闭读写端
16.3 寻址
16.3.1 字节序
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
uint64_t htons(uint16_t hostint16);
uint32_t ntohl(uint32_t netint32);
uint16_t ntohs(uint16_t netint16);
16.3.2 地址格式
为使不同格式地址能够传入到套接字函数,地址会被强制转换成一个通用的地址结构sockaddr
struct sockaddr {
sa_family_t sa_family;
char sa_data[];
};
因特网地址定义在<netinet/in.h>头文件中。 在IPv4因特网域中,套接字地址用结构 sockaddr_in表示:
struct in_addr_t {
in_addr_t s_addr;
};
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
IPv6因特网域套接字地址用结构 sockaddr_in6表示:
struct in6_addr {
uint8_t s6_addr[16];
};
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
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);
int inet_pton(int domain, const char *restrict str, void *restrict addr);
函数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);
void sethostent(int stayopen);
void endhostent(void);
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **haddr_list;
};
如果主机数据库文件没有打开,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);
void setnetent(int stayopen);
void endnetent(void);
struct netent {
char *n_name;
char **naliases;
int n_addrtype;
uint32_t n_net;
};
我们可以用以下函数在协议名字和协议编号之间进行映射。
#include <netdb.h>
struct protoent *getprotobyname(const char*name);
struct protoent *getprotobynumber(int proto);
struct protoent *getprotoent(void);
void setprotoent(int stayopen);
void endprotoent(void);
struct protoent {
char *p_name;
char **p_aliases;
int p_proto;
}
使用函数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);
void setservent(int stayopen);
void endservent(void);
struct servent {
char *s_name;
char **s_aliases;
int s_port;
char *s_proto;
};
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);
void freeaddrinfo(struct addrinto *ai);
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char* ai_canonname;
struct addrinfo *ai_next;
}
主机名可以是一个节点名或点分格式的主机地址。 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);
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);
对于因特网域,如果指定IP地址为INADDR_ANY,套接字端点可以被绑定到所有的系统网络接口上。
可以调用getsockname函数来发现绑定到套接字上的地址。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
返回时,alenp指向的整数会被设置成返回地址的大小。
如果套接字已经和对等方连接,可以调用getpeername函数来找到对方的地址。
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
16.4 建立连接
使用connect函数来建立连接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
在connect中指定的地址是我们想与之通信的服务器地址。如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。
可迁移的应用程序需要关闭套接字。
若套接字处于非阻塞模式,那么在连接不能马上建立时,connect会返回?1并将errno设置为特殊的错误码EINPROGRESS。应用程序可以使用poll或者select来判断套接字描述符何时可写,如果可写,连接完成。
connect函数还可以用于无连接的网络服务(SOCK_DGRAM),传送的报文的目标地址会设置成connect调用中指定的地址,这样每次传送报文时就不需要再提供地址。另外,仅能接收来自指定地址的报文。
服务器调用listen函数来宣告它愿意接受连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数backlog提供了一个提示,提示系统该进程要入队的未完成连接请求数量。
一旦服务器调用了listen,所用的套接字就能接收连接请求。使用accept函数获得连接请求并建立连接
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
如果没有连接请求在等待,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);
使用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);
调用带有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);
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
int msg_iovlen;
void *msg_control;
socklen_t msg_controllen;
int msg_flags;
};
recv函数
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
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);
进入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);
参数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);
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);
当下一个要读取的字节在紧急标志处时,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);
|