APUE 学习笔记(十一) 网络IPC:套接字
1. 网络IPC
套接字接口既可以用于计算机之间进程通信,也可以用于计算机内部进程通信
套接字描述符在Unix系统中是用文件描述符实现的
/* 创建一个套接字 */ #include <sys/socket.h> int socket(int domain, int type, int protocol);
protocol通常是0,表示按给定的域或套接字类型选择默认协议
在AF_INET中,SOCK_STREAM的默认协议是 TCP
在AF_INET中,SOCK_DGRAM的默认协议是 UDP
套接字通信是双向的,可以采用 shutdown来禁止套接字上的 输入/输出
#include <sys/socket.h> int shutdown(int sockfd, int how);
how为SHUT_RD(关闭读端)时,无法从套接字上读取数据
how为SHUT_WR(关闭写端)时,无法向套接字发送数据
2. 套接字地址
大端:最大字节地址对应于数字最低有效字节,阅读序
小端:最小字节地址对应于数字最低有效字节,逆阅读序
例如:0x04030201
大端机:04==> ch[0] 01==> ch[3]
小端机:04==> ch[3] 01==> ch[0]
不管字节如何排序,数字最高位总是在左边,最低位总是在右边
TCP/IP协议栈采用大端字节序
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 ntohl(uint16_t netint16); // 返回值:以主机字节序表示的16位整型数
"h"代表 host主机字节序, “n”代表net网络字节序,“l”代表32位long整型,“s”代表16位short整型
通用地址结构sockaddr:
struct sockaddr { sa_family_t sa_family; /* address family */ char sa_data[14]; /* variable-length address */ };
struct sockaddr 一共为18字节(32位地址+14字节填充)
在IPv4 套接字地址结构 sockaddr_in (in表示internet网络):
struct in_addr { in_addr_in 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 */ unsigned char sin_zero[8]; };
struct sockaddr_in 一共为18字节(4字节family + 16位端口号 + 32位IPv4地址 + 8字节填充)
二进制地址格式和 点分十进制格式转换:
#include <arpa/inet.h> const char* inet_ntop(int domain, const char* addr, char* str, socklen_t size); int inet_pton(int domain, const char* str, void* str);
参数domain可以支持 AF_INET和AF_INET6
地址信息查询:
#include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char* host, const char* service, const struct addrinfo* hint, struct addrinfo* res); void freeaddrinfo(struct addrinfo* 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; struct addrinfo* ai_next; /* next in list */ .... };
函数getaddrinfo 可以将 IPv4和IPv6代码统一起来,所以网络编程中套接字地址信息都必须使用此函数,便于统一和移植
函数getaddrinfo 允许将一个主机名和服务器名映射到一个地址,需要提供 host主机名 或 service 服务器名,否则指针设为空
主机名字可以是一个 节点名或者点分十进制表示的主机地址
函数getaddrinfo返回 一个 结构体 struct addrinfo的链表,freeaddrinfo来释放这个结构体链表
#include <sys/socket.h> #include <netdb.h> #include <arpa/inet.h> #include <stdio.h> void print_family(struct addrinfo* aip) { fprintf(stdout, "family:"); switch (aip->ai_family) { case AF_INET: fprintf(stdout, "inet"); break; case AF_INET6: fprintf(stdout, "inet6"); break; case AF_UNIX: fprintf(stdout, "unix"); break; case AF_UNSPEC: fprintf(stdout, "unspecfied"); break; default: fprintf(stdout, "unknown"); } } void print_type(struct addrinfo* aip) { fprintf(stdout, "type"); switch (aip->ai_socktype) { case SOCK_STREAM: fprintf(stdout, "stream"); break; case SOCK_DGRAM: fprintf(stdout, "datagram"); break; case SOCK_SEQPACKET: fprintf(stdout, "seqpacket"); break; case SOCK_RAW: fprintf(stdout, "raw"); break; default: fprintf(stdout, "unknown (%d)", aip->ai_socktype); } } void print_protocol(struct addrinfo* aip) { fprintf(stdout, "protocol"); switch (aip->ai_protocol) { case 0: fprintf(stdout, "default"); break; case IPPROTO_TCP: fprintf(stdout, "tcp"); break; case IPPROTO_UDP: fprintf(stdout, "udp"); break; case IPPROTO_RAW: fprintf(stdout, "raw"); break; default: fprintf(stdout, "unknown (%d)", aip->ai_protocol); } } void print_flags(struct addrinfo* aip) { fprintf(stdout, "flags"); if (aip->ai_flags == 0) { fprintf(stdout, "0"); } else { if (aip->ai_flags & AI_PASSIVE) { fprintf(stdout, "passive"); } if (aip->ai_flags & AI_CANONNAME) { fprintf(stdout, "canon"); } if (aip->ai_flags & AI_NUMERICHOST) { fprintf(stdout, "numhost"); } } } int main(int argc, char* argv[]) { struct addrinfo* ailist = NULL; struct addrinfo* aip = NULL; struct addrinfo hint; struct sockaddr_in* sinp; const char* addr = NULL; char abuf[INET_ADDRSTRLEN]; if (argc != 3) { fprintf(stdout, "usage:%s <hostname> <service>", argv[0]); return 1; } hint.ai_flags = AI_CANONNAME; hint.ai_family = 0; hint.ai_socktype = 0; hint.ai_protocol = 0; hint.ai_addrlen = 0; hint.ai_canonname = NULL; hint.ai_addr = NULL; hint.ai_next = NULL; int ret = getaddrinfo(argv[1], argv[2], &hint, &ailist); if (ret != 0) { fprintf(stderr, "getaddrinfo error\n"); return 1; } for (aip = ailist; aip != NULL; aip = aip->ai_next) { print_flags(aip); print_family(aip); print_type(aip); print_protocol(aip); fprintf(stdout, "\n\thost %s", aip->ai_canonname ? aip->ai_canonname : '-'); if (aip->ai_family == AF_INET) { sinp = (struct sockaddr_in*)aip->ai_addr; addr = inet_ntop(AF_INET, &sinp->sin_addr, abuf, INET_ADDRSTRLEN); fprintf(stdout, "address %s", addr ? addr : "unknown"); fprintf(stdout, "port %d", ntohs(sinp->sin_port)); } fprintf(stdout, "\n"); } return 0; }
套接字与地址绑定:
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr* addr, socklen_t len);
如果将addr指定为 INADDR_ANY,则套接字可以接收到这个系统所安装的所有网卡的数据包
3. 建立连接
#include <sys/socket.h> int connect(int sockfd, const struct sockaddr* addr, socklen_t len);
客户端调用connect函数,请求与服务器建立连接,addr为服务器地址
处理瞬时connect错误:
#include <sys/socket.h> #define MAXSLEEP 128 int connect_retry(int sockfd, const struct sockaddr* addr, socklen_t len) { /* try to connect with exponential backoff */ for (int nsec = 1; nsec <= MAXSLEEP; nsec << 1) { if (connect(sockfd, addr, len) == 0) { /* connection accepted */ return 0; } /* delay before trying again */ if (nsec <= MAXSLEEP / 2) sleep(nsec); } return -1; }
这个函数使用了 指数补偿的算法,如果调用connect失败,进程就休眠一小段时间再尝试连接,每循环一次就加倍每次尝试的延迟
#include <sys/socket.h> int listen(int sockfd, int backlog);
服务器调用listen函数来宣告自己可以接受连接请求
参数backlog提供了一个提示,用于表示该进程所要入队的连接请求数量,一旦队列满,系统会拒绝多余连接请求
#include <sys/socket.h> int accept(int sockfd, struct sockaddr* addr, socklen_t* len);
服务器调用accept函数来获取连接请求并建立连接,函数返回 已连接描述符,已连接描述符与监听描述符不同
addr和len都是客户端地址参数,如果不关心客户端标识,可以将这两个参数设为NULL,否则,在调用accept之前,必须将addr设为足够大的缓冲区来存放地址,accept调用返回时 会回填客户端的地址和地址大小
服务器可以使用select或epoll来等待一个连接请求,一个等待连接的客户端请求套接字会以可读的形式出现
4. 数据传输
#include <sys/socket.h> ssize_t send(int sockfd, const void* buf, size_t bytes, int flags); // 等同于 write,套接字必须已连接 ssize_t sendto(int sockfd, const void* buf, size_t bytes, int flags, const struct sockaddr* dstaddr, socklen_t dstlen);
对于面向连接的套接字,使用send函数,目标地址蕴含在连接中,在此忽略
对于面向无连接的套接字,使用sendto函数,必须指定 目标地址
#include <sys/socket.h> ssize_t recv(int sockfd, void* buf, size_t bytes, int flags); //类似于 read ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* addr, socklen_t* addrlen);
对于面向连接的套接字,使用recv函数,目标地址蕴含在连接中,在此忽略
对于面向无连接的套接字,使用recvfrom函数,必须指定 目标地址
/* tcp_connect for client: * hostname or ip: www.google.com or 127.0.0.1 * service or port: http or 9877 */ int tcp_connect(const char* hostname, const char* service) { struct addrinfo hints; struct addrinfo* result; struct addrinfo* rp; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; int res = getaddrinfo(hostname, service, &hints, &result); if (res != 0) { fprintf(stderr, "tcp_connect error for %s, %s: %s", hostname, service, gai_strerror(res)); exit(0); } int sockfd; for (rp = result; rp != NULL; rp = rp->ai_next) { sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (sockfd < 0) continue; int rc = connect(sockfd, rp->ai_addr, rp->ai_addrlen); if (rc == 0) break; close(sockfd); } if (rp == NULL) { unix_error("tcp_connect error"); } freeaddrinfo(result); return sockfd; }
/* tcp_listen for server: * hostname or ip: www.google.com or 127.0.0.1 * service or port: http or 9877 */ int tcp_listen(const char* hostname, const char* service, socklen_t* paddrlen) { struct addrinfo hints; struct addrinfo* result; struct addrinfo* rp; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; int res = getaddrinfo(hostname, service, &hints, &result); if (res != 0) { fprintf(stderr, "tcp_listen error for %s, %s: %s", hostname, service, gai_strerror(res)); exit(0); } int listenfd; for (rp = result; rp != NULL; rp = rp->ai_next) { listenfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (listenfd < 0) continue; int rc = bind(listenfd, rp->ai_addr, rp->ai_addrlen); if (rc == 0) break; Close(listenfd); } if (rp == NULL) { unix_error("tcp_listen error"); } Listen(listenfd, LISTENQ); if (paddrlen) { *paddrlen = rp->ai_addrlen; } freeaddrinfo(result); return listenfd; }
5. 套接字选项
#include <sys/socket.h> int setsockopt(int sockfd, int level, int option, const void* val, socklen_t len); int getsockopt(int sockfd, int level, int option, void* val, socklen_t lenp);
6. 带外数据
带外数据 允许更高优先级的数据比普通数据优先传输,TCP支持带外数据,UDP不支持
TCP仅支持一个字节的带外数据,但是允许带外数据在普通传输机制流之外传输,为了产生带外数据,需要在send函数中指定 MSG_OOB标志
当带外数据出现在套接字读取队列时,select函数会返回一个文件描述符并且拥有一个异常状态挂起
7. 非阻塞I/O
recv函数没有数据可读时会阻塞等待,当套接字输出队列没有足够空间来发送消息时 send函数会阻塞
如果套接字是非阻塞模式,这些情况下,这些函数不是阻塞而是失败,设置errno为EWOULDBLOCK或者EAGAIN