1.域名系统
程序中只使用主机名和服务名的好处是,如果IP或端口变化,只需要改变映射关系,不需要重新编译程序。
1.1 资源记录
DNS的条目为资源记录,有用的项如下:
A IPv4地址 AAAA IPv6地址 CNAME 规范名字 如: ftp.unpbook.com 的 CNAME 为 linux.unpbook.com www.unpbook.com 的 CNAME 为 linux.unpbook.com
1.2 解析器和名字服务器
程序通过调用解析器库函数,调用DNS服务。常用的函数为 gethostbyname, gethostbyaddr。
与解析器相关的配置文件和库函数关系如下
2.1 gethostbyname
struct hostent *gethostbyname(const char *name); struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char **h_addr_list; /* list of addresses */ }
该函数只能返回资源记录的 A ,也就是IPv4。
若要考虑IPv6,应使用 getaddrinfo.
h_name 就是 CNAME(规范名字),如 ftp.unpbook.com 的规范名字为 linux.unpbook.com
h_addrtype 只有为 IPv4,才有用
h_addr_list 指向一个数组,数组元素为 IP地址(网络字节序,不是点分十进制)。
可以传入点分十进制,或域名调用 gethostbyname
gethostbyname 调用错误,不会设置errno,而是设置 h_errno,使用 hstrerror(h_errno) 得到错误描述的字符串。
int main(int argc, char **argv) { char *ptr, **pptr; char str[INET_ADDRSTRLEN]; struct hostent *hptr; while (--argc > 0) { ptr = *++argv; if ( (hptr = gethostbyname(ptr)) == NULL) { err_msg("gethostbyname error for host: %s: %s", ptr, hstrerror(h_errno)); continue; } printf("official hostname: %s\n", hptr->h_name); for (pptr = hptr->h_aliases; *pptr != NULL; pptr++) printf("\talias: %s\n", *pptr); switch (hptr->h_addrtype) { case AF_INET: pptr = hptr->h_addr_list; for ( ; *pptr != NULL; pptr++) printf("\taddress: %s\n", Inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str))); break; default: err_ret("unknown address type"); break; } } exit(0); }
2.2 gethostbyaddr
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
输入网络字节序IP地址,查找主机CNAME。
type 为 AF_INET
返回 hostent ,通常我们感兴趣的只有 h_name。
2.3 可重入函数
gethostbyname 和 getbostbyaddr 是不可重入的,原因是使用 static hostent host 。
不可重入带来问题通常在 多线程和信号。
如
main() { signal(SIGALRM, sig_alrm); hptr = gethostbyname(...); } void sig_alrm(int sig) { hptr= gethostbyname(...); }
可重入可不可重入总结:
gethostbyname, gethostbyaddr, getservbyname, getservbyport 都是不可重入的,原因是使用静态变量。不过有可重入版本 inet_pton, inet_ntop 不可重入 inet_ntoa 不可重入,不过有可重入版本。 getaddrinfo, getnameinfo 可重入
对于errno的处理,errno可能会被多线程,信号意外改变,可以如下处理
void sig_alrm(int sig) { int errno_save; errno_save = errno; if (write(...) != nbytes) fprintf(stderr, "write error = %d\n", errno); errno = errno_save; }
3.1 getserbyname
struct servent *getservbyname(const char *name, const char *proto); struct servent { char *s_name; /* official service name */ char **s_aliases; /* alias list */ int s_port; /* port number */ char *s_proto; /* protocol to use */ }
输入服务名和协议,查询端口号。
如果指定proto,必须保证服务器使用了对应协议,如果没有指定,因为通常服务器使用相同端口号来使用不同协议,所以也没关系。
s_port是网络字节序
相关配置文件 /etc/services
典型调用:
sptr = getservbyname("domain", "udp"); // DNS using UDP sptr = getservbyname("ftp", "tcp"); // FTP using TCP
3.2 getservbyport
struct servent *getservbyport(int port, const char *proto);
输入端口号和协议,查询服务名
port必须为网络字节序
典型调用:
sptr = getservbyport(hton(53), "udp"); // DNS using UDP
4. 使用 gethostbyname 和 getserbyname
int main(int argc, char **argv) { int sockfd, n; char recvline[MAXLINE + 1]; struct sockaddr_in servaddr; struct in_addr **pptr; struct in_addr *inetaddrp[2]; struct in_addr inetaddr; struct hostent *hp; struct servent *sp; if (argc != 3) err_quit("usage: daytimetcpcli1 <hostname> <service>"); if ( (hp = gethostbyname(argv[1])) == NULL) { if (inet_aton(argv[1], &inetaddr) == 0) { err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno)); } else { inetaddrp[0] = &inetaddr; inetaddrp[1] = NULL; pptr = inetaddrp; } } else { pptr = (struct in_addr **) hp->h_addr_list; } if ( (sp = getservbyname(argv[2], "tcp")) == NULL) err_quit("getservbyname error for %s", argv[2]); for ( ; *pptr != NULL; pptr++) { sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = sp->s_port; memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr)); printf("trying %s\n", Sock_ntop((SA *) &servaddr, sizeof(servaddr))); if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) == 0) break; /* success */ err_ret("connect error"); close(sockfd); } if (*pptr == NULL) err_quit("unable to connect"); while ( (n = Read(sockfd, recvline, MAXLINE)) > 0) { recvline[n] = 0; /* null terminate */ Fputs(recvline, stdout); } exit(0); }
5.1 getaddrinfo
int getaddrinfo(const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **res); struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *ai_next; };
getaddrinfo支持IPv4,IPv6,并且提供名字到地址,服务到端口两种转换,返回的是一个sockaddr结构而不是地址列表,因此可以直接用于套接字库函数。
hostname 是一个主机名或地址串(点分十进制或十六进制串)
service 是一个服务名或十进制端口号串
hints 可以为空,也可以是指向 struct addrinfo的指针,表示对期望返回信息的暗示。
hints可以设置成员有
int ai_flags; int ai_family; int ai_socktype; int ai_protocol;
ai_flags 可用的标志值和含义:
AI_PASSIVE 套接字将用于被动打开 AI_CANONAME 给之getaddrinfo返回主机的规范名字 AF_NUMERICHOST 防止任何类型的名字到地址的映射,hostname参数必须是一个地址串 AF_NUMERICSERV 防止任何类型的名字到端口的映射,service参数必须是一个端口号 AI_V4MAPPED 如果同时指定ai_famliy 为AF_INET6,那么如果没有可用的AAAA记录,就返回A记录对应的IPv4映射的IPv6地址 AI_ALL 如果同时指定AI_V4MAPPED,那么除了返回AAAA记录对应的IPv6地址外,还返回A记录对应的IPv4映射的IPv6地址。 AI_ADDRCONFIG 按照所在主机的配置选择返回地址类型,也就是只查找与所在主机回馈接口以外的网络接口配置的IP地址版本一致的地址。
如果hints参数是空指针,那么本函数就假设ai_flag,ai_socktype,ai_protocol的值均为空,ai_family为AF_UNSPEC
如果函数返回成功(0),result参数返回addrinfo结构的链表。
举例,在没有任何暗示信息前提下,请求查找有2个IP地址的某个主机上的domain服务,那么就将返回4个addrinfo结构,
第一个IP地址组合SOCK_STREAM套接字类型
第一个IP地址组合SOCK_DGRAM套接字类型
第二个IP地址组合SOCK_STREAM套接字类型
第二个IP地址组合SOCK_DGRAM套接字类型
且这些结构的返回顺序不确定。
返回的addrinfo结构可用于其他套接字函数,
socket调用使用 ai_family, ai_socktype,
connect,bind调用使用 ai_addr, 和 ai_addrlen
返回的res变量指向的addrinfo结构空间是动态分配的,需要使用freeaddrinfo释放。
getaddrinfo的常见使用如下:
(1)TCP或UDP客户进程,指定hostname和 service,返回后,TCP客户进程逐个使用返回的地址,以调用 socket , connect,直到一个地址调用成功。UDP客户进程调用sendto或connect,如果地址不工作(如收到错误信息,或者超时),则测试其他地址,直到成功。
(2)服务进程一般只指定 service 而不指定 hostname,同时在AI_PASSIVE标志。TCP服务进程随后调用socket,bind,listen。如果想获得accept的客户地址,那么用 ai_addrlen 来malloc 地址结构。UDP服务调用socket,bind,recvfrom,如果服务器清楚自己只处理一种类型的套接字,那么应该把hints的ai_socktype设置成 SOCK_STREAM或SOCK_DGRAM。
(3)到目前为止,我们的服务器(UDP,TCP)都只创建一个监听套接字或数据报套接字。而另一种设计是用 select或poll让服务器处理多个套接字,这种情况下服务器遍历整个addrinfo结构链表,并为每个结构创建一个套接字,再使用select或poll。
这个技术的问题是,getaddrinfo返回多个结构的原因之一是该服务可以同时由IPv4,IPv6处理,但是这两个协议并非完全独立,如果我们为某个给定端口创建一个IPv6监听套接字,那么就没必要为同一个端口创建一个IPv4套接字,因为来自IPv4的连接将由协议栈和IPv6监听套接字自动处理。
5.2 gai_strerror
const char *gai_strerror(int errcode);
getaddrinfo的错误由gai_strerror解释。
5.3 freeaddrinfo
void freeaddrinfo(struct addrinfo *res);
getaddrinfo返回的res指向的链表是动态分配的,由freeaddrinfo释放。
同时注意res的浅拷贝问题。
5.4 getnameinfo
int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags);
输入套接字地址,返回主机名和服务名。
sock_ntop和getnameinfo的区别是,前者不涉及DNS,只返回一个IP地址和端口号的一个可显示版本,后者通常尝试获得主机和服务名字。
5.5 对getaddrinfo的封装
getaddrinfo是推荐使用的,但是调用过于麻烦,所以封装常用操作
struct addrinfo * host_serv(const char *host, const char *serv, int family, int socktype) { int n; struct addrinfo hints, *res; bzero(&hints, sizeof(struct addrinfo)); hints.ai_flags = AI_CANONNAME; /* always return canonical name */ hints.ai_family = family; /* AF_UNSPEC, AF_INET, AF_INET6, etc. */ hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */ if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) return(NULL); return(res); /* return pointer to first on linked list */
int tcp_connect(const char *host, const char *serv) { int sockfd, n; struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) err_quit("tcp_connect error for %s, %s: %s", host, serv, gai_strerror(n)); ressave = res; do { sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0) continue; /* ignore this one */ if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) break; /* success */ Close(sockfd); /* ignore this one */ } while ( (res = res->ai_next) != NULL); if (res == NULL) /* errno set from final connect() */ err_sys("tcp_connect error for %s, %s", host, serv); freeaddrinfo(ressave); return(sockfd); }
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp) { int listenfd, n; const int on = 1; struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) err_quit("tcp_listen error for %s, %s: %s", host, serv, gai_strerror(n)); ressave = res; do { listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (listenfd < 0) continue; /* error, try next one */ Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0) break; /* success */ Close(listenfd); /* bind error, close and try next one */ } while ( (res = res->ai_next) != NULL); if (res == NULL) /* errno from final socket() or bind() */ err_sys("tcp_listen error for %s, %s", host, serv); Listen(listenfd, LISTENQ); if (addrlenp) *addrlenp = res->ai_addrlen; /* return size of protocol address */ freeaddrinfo(ressave); return(listenfd); } /* end tcp_listen */
上面函数有一个问题,即指定的地址族为 AF_UNSPEC,即可能返回非期待的地址族套接字,如期待IPv4,返回IPv4和IPv6。
解决方法:
我们可以强制指定地址协议,如inet_pton
inet_pton(AF_INET,"0.0.0.0", &foo); //succeeds inet_pton(AF_INET, "0::0", &foo); // fails inet_pton(AF_INET6, "0.0.0.0", &foo); // fails inet_pton(AF_INET6, "0::0", &foo); //succeeds
无连接UDP
int udp_client(const char *host, const char *serv, SA **saptr, socklen_t *lenp) { int sockfd, n; struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) err_quit("udp_client error for %s, %s: %s", host, serv, gai_strerror(n)); ressave = res; do { sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd >= 0) break; /* success */ } while ( (res = res->ai_next) != NULL); if (res == NULL) /* errno set from final socket() */ err_sys("udp_client error for %s, %s", host, serv); *saptr = Malloc(res->ai_addrlen); memcpy(*saptr, res->ai_addr, res->ai_addrlen); *lenp = res->ai_addrlen; freeaddrinfo(ressave); return(sockfd); } /* end udp_client */
有连接UDP
int udp_connect(const char *host, const char *serv) { int sockfd, n; struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) err_quit("udp_connect error for %s, %s: %s", host, serv, gai_strerror(n)); ressave = res; do { sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0) continue; /* ignore this one */ if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) break; /* success */ Close(sockfd); /* ignore this one */ } while ( (res = res->ai_next) != NULL); if (res == NULL) /* errno set from final connect() */ err_sys("udp_connect error for %s, %s", host, serv); freeaddrinfo(ressave); return(sockfd); } /* end udp_connect */
int udp_server(const char *host, const char *serv, socklen_t *addrlenp) { int sockfd, n; struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) err_quit("udp_server error for %s, %s: %s", host, serv, gai_strerror(n)); ressave = res; do { sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0) continue; /* error - try next one */ if (bind(sockfd, res->ai_addr, res->ai_addrlen) == 0) break; /* success */ Close(sockfd); /* bind error - close and try next one */ } while ( (res = res->ai_next) != NULL); if (res == NULL) /* errno from final socket() or bind() */ err_sys("udp_server error for %s, %s", host, serv); if (addrlenp) *addrlenp = res->ai_addrlen; /* return size of protocol address */ freeaddrinfo(ressave); return(sockfd); } /* end udp_server */
UDP套接字是不需要设置SO_REUSEADDR,因为UDP是没有WAIT_TIME类似的状态物。
原文地址:https://www.cnblogs.com/yangxinrui/p/12437321.html