0x0a
IP地址
一个IP地址就是一个32位无符号整数。
struct in_addr { uint32_t s_addr; };
TCP/IP为任意整数数据项定义了统一的网络字节顺序(大端字节序)。Unix提供了函数在网络和主机字节顺序间实现转换。
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); unit16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(unit16_t netshort);
应用程序使用inet_pton和inet_ntop函数来实现IP地址和点分十进制串之间的转换。
#include <arpa/inet.h> // 若成功返回1,若src为非法点分十进制地址则为0,若出错则为-1 int inet_pton(AF_INET, const char *src, void *dst); // 若成功则指向点分十进制串的指针,若出错返回NULL const char *inet_ntop(AF_INET, const vod *src, char *dst, socklen_t size);
套接字接口
套接字接口是一组函数,和UnixI/O函数结合起来,用以创建网络应用。大多数现代系统上都实现套接字接口。
套接字地址结构
从Linux内核的角度来看,一个套接字就是通信的一个端点。从Linux程序的角度来看,套接字就是一个有相应描述符的打开文件。
因特网的套接字地址存在类型为sockaddr_in的16字节结构中。
struct sockaddr_in { uint16_t sin_family; uint16_t sin_port; struct in_addr sin_addr; unsigned char sin_zero[8]; // pad to sizeof(struct sockaddr) }; struct sockaddr { uint16_t sa_family; char sa_data[14]; }; typedef struct sockaddr SA;
对于因特网应用,sin_family成员是AF_INET,sin_port成员是一个16位端口号,sin_addr成员就是一个32位的IP地址。IP地址和端口号总是以网络字节顺序存放的。
connect、bind和accept函数要求一个指向与协议相关的套接字地址结构的指针。如何定义函数使之能接受各种类型的套接字地址结构,解决办法是定义套接字函数要求一个指向通用sockaddr结构的指针。然后要求应用程序将与协议特定的结构的指针强制转换成这个通用结构。
socket函数
客户端和服务器使用socket函数来创建一个套接字描述符。
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
如果想要使套接字成为连接的一个端点,就用如下硬编码的参数来调用socket函数:
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
其中AF_INET表明正在使用32位IP地址,而SOCK_STREAM表示这个套接字是连接的一个端点。不过最好的方法是用getaddrinfo函数来自动生成这些参数。
socket返回的clientfd描述符仅是部分打开的,还不能用于读写。如何完成打开套接字的工作,取决于我们是客户端还是服务器。
connect函数
客户端通过调用connect函数来建立和服务器的连接。
#include <sys/socket.h> int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
connect函数试图与套接字地址为addr的服务器建立一个因特网连接。其中addrlen是sizeof(sockaddr_in)。connect函数会阻塞,一直到连接成功建立或是发生错误。如果成功,clientfd描述符现在就准备好可以读写了。并且得到的连接是由套接字对(x:y, addr.sin_addr:addr.sin_port)
表示的。对于socket,最好的方法是用getaddrinfo来为connect提供参数。
bind函数
服务器用它来和客户端建立连接。
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind函数告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来。参数addrlen就是sizeof(sockaddr_in)。最好的方式是用getaddrinfo来为bind提供参数。
listen函数
客户端是发起连接请求的主动实体。服务器是等待来自客户端的连接请求的被动实体。默认情况下,内核会认为socket函数创建的描述符对应于主动套接字,它存在于一个连接的客户端。服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用的。
#include <sys/socket.h> int listen(int sockfd, int backlog);
listen函数将sockfd从一个主动套接字转化为一个监听套接字。该套接字可以接收来自客户端的连接请求。backlog参数暗示了内核在开始拒绝连接请求之前,队列中要排队的未完成的连接请求的数量。
accept函数
服务器通过调用accept函数来等待来自客户端的连接请求。
#include <sys/socket.h> int accept(int listenfd, struct sockaddr *addr, int *addrlen);
accept函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个已连接描述符,这个描述符可被用来利用Unix I/O函数与客户端通信。
监听描述符是作为客户端连接请求的一个端点。它通常被创建一次,并存在于服务器的整个生命周期。已连接描述符是客户端和服务器之前已经建立起来了的连接的一个端点。服务器每次接受连接请求时都会创建一次,它只存在于服务器为一个客户端服务的过程中。
第一步,服务器调用accept,等待连接请求到达监听描述符,具体地我们设定为描述符3(描述符0-2是预留给了标准文件的)。第二步,客户端调用connect函数,发送一个连接请求到listenfd。第三步,accept函数打开了一个新的已连接描述符connfd,在clientfd和connfd之间建立连接,并且随后返回connfd给应用程序。客户端也从connect返回。此后,客户端和服务器就可以分别通过读和写clientfd和connfd来回传送数据了。
主机和服务的转换
有一些函数实现二进制套接字地址结构和主机名、主机地址、服务名和端口号的字符串表示之间的相互转化。当和套接字接口一起使用时,这些函数能使我们编写独立于任何特定版本的IP协议的网络程序。
getaddrinfo函数
getaddrinfo函数将主机名、主机地址、服务名和端口号的字符串表示转化成套接字地址结构。
#include <sys/types.h> #include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result); void freeaddrinfo(struct addrinfo *result); const char *gai_strerror(int errcode);
给定host和service(套接字地址的两个组成部分),getaddrinfo返回result,一个指向addrinfo结构的链表,其中每个结构指向一个对应于host和service的套接字地址结构。
客户端调用了getaddrinfo后,会遍历这个列表,依次尝试每个套接字地址,直到调用socket和connect成功,建立起连接。服务器会尝试遍历列表中的每个套接字地址,直到调用socket和bind成功,描述符会被绑定到一个合法的套接字地址。为了避免内存泄漏,应用程序必须在最后调用freeaddrinfo,释放该链表。
getaddrinfo的host参数可以是域名,也可以是数字地址(如点分十进制IP地址)。service参数可以是服务名(如http),也可以是十进制端口号。如果不想把主机名转换成地址,可以把host设置为NULL。
可选的参数hints是一个addrinfo结构,它提供对getaddrinfo返回的套接字地址列表的更好的控制。如果要传递hints参数,只能设置下列字段:ai_family、ai_socktype、ai_protocol和ai_flags字段,其他字段必须设置为0或NULL。
-
getaddrinfo默认可以返回IPv4和IPv6套接字地址,ai_family设置为AF_INET会将列表限制为IPv4地址;设置为AF_INET6则限制为IPv6地址。
-
对于host关联的每个地址,getaddrinfo函数默认最多返回三个addrinfo结构,每个的ai_socktype字段不同:一个是连接,一个是数据报,一个是原始套接字。ai_socktype设置为SOCK_STREAM将列表限制为对每个地址最多一个addrinfo结构,该结构的套接字地址可以作为连接的一个端点。
-
ai_flags字段是一个位掩码,可以进一步修改默认行为。可以把各种值用OR组合起来。如:
AI_ADDRCONFIG。如果在使用连接,就推荐这个标志。它要求只有当本地主机被配置为IPv4时,getaddrinfo返回IPv4地址,对IPv6也类似。
AI_CANONNAME。ai_canonname字段默认为NULL。如果设置了该标志,就是告诉getaddrinfo将列表中第一个addrinfo结构的ai_canonnaame字段指向host的官方名字。
AI_NUMERICSERV。参数service默认可以是服务名或端口号。这个标志强制参数service为端口号。
AI_PASSIVE。getaddrinfo默认返回套接字地址,客户端可以在调用connect时用作主动套接字。这个标志告诉该函数,返回的套接字地址可能被服务器用作监听套接字。在这种情况中,参数host应该为NULL。得到的套接字地址结构中的地址字段会是通配符地址,告诉内核这个服务器会接受发送到该主机所有IP地址的请求。
struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; char *ai_canonname; size_t ai_addrlen; struct sockaddr *ai_addr; struct addrinfo *ai_next; };
当getaddrinfo创建输出列表中的addrinfo结构时,会填写每个字段,除了ai_flags。ai_addr字段指向一个套接字地址结构,ai_addrlen字段给出这个套接字地址结构的大小,ai_next字段指向列表中的下一个addrinfo结构。其他字段描述这个套接字地址的各种属性。
getnameinfo函数
getnameinfo函数和getaddrinfo是相反的,将一个套接字地址结构转换成相应的主机和服务名字符串。
#include <sys/socket.h> #incluee <netdb.h> int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags);
参数sa指向大小为salen字节的套接字地址结构,host指向大小为hostlen直接的缓冲区,service指向大小为servlen字节的缓冲区。getnameinfo函数将套接字地址结构sa转换成对应的主机和服务名字符串,并将它们复制到host和service缓冲区。如果不想要主机名,可以把host设置为NULL,hostlen设置为0。
参数flags是一个位掩码,能够修改默认的行为。可以把各种值用OR组合起来得到该掩码。
- NI_NUMERICHOST。getnameinfo默认试图返回host中的域名。设置该标志会使该函数返回一个数字地址字符串。
- NI_NUMERICSERV。getnameinfo会默认 检查/etc/service,如果可能,会返回服务名而不是端口号。设置该标志会使该函数跳过查找,简单地返回端口号。
套接字接口的辅助函数
open_clientfd函数
客户端调用open_clientfd建立与服务器的连接。
int open_clientfd(char *hostname, char *port) { int clientfd; struct addrinfo hints, *listp, *p; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_socketype = SOCK_STREAM; hints.ai_flags = AI_NUMERICSERV; hints.ai_flags |= AI_ADDRCONFIG; getaddrinfo(hostname, port, &hints, &listp); for (p = listp; p; p = p->ai_next) { if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) continue; if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1) break; close(clientfd); } freeaddrinfo(listp); if (!p) return -1; else return clientfd; }
open_clientfd函数建立与服务器的连接,该服务器运行在主机hostname上,并在端口号port上监听连接请求。调用getaddrifno,它返回addrifo结构的列表。每个结构指向一个套接字地址结构,可用于建立与服务器的连接。然后遍历该列表,依次尝试列表中的每个条目,直到调用socket和connect成功。如果connect失败,在尝试下一个条目之前,要小心地关闭套接字描述符。如果connect成功,会释放列表内存,并把套接字描述符返回给客户端,客户端可以立即开始用Unix I/O与服务器通信。
open_listenfd函数
int open_listenfd(char *port) { struct addrinfo hints, *listp, *p; int listenfd, optval = 1; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; hints.ai_flags |= AI_NUMERICSERV; getaddrinfo(NULL, port, &hints, &listp); for (p = listp; p; p = p->ai_next) { if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) continue; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int)); if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0) break; close(listenfd); } freeaddrinfo(listp); if (!p) return -1; if (listen(listenfd, LISTENQ) < 0) { close(listenfd); return -1; } return listenfd; }
调用getaddrinfo,然后遍历结果列表,直到调用socket和bind成功。使用setsockopt函数来配置服务器,使得服务器能够被终止、重启和立即开始接收连接请求。因为调用getaddrinfo时,使用了AI_PASSIVE标志并将host参数设置为NULL,每个套接字地址结构中的地址字段会被设置为通配符地址,这告诉内核这个服务器会接收发送到本主机所有IP地址的请求。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)