系列文章导航:《Unix 网络编程》笔记
域名系统主要用于主机名字和 IP 地址之间的映射。主机名可以是:
记录
作用
A
指向IPv4
AAAA
指向IPv6
PTR
把IP地址映射为主机名
MX
邮件记录
CNAME
为二级域名指定域名或IP
DNS
如果使用 DNS 查找主机名,则使用 /etc/resolv.conf
指定的 DNS
有如下替代方法:
/etc/hosts
所有这些差异对应用开发人员是透明的,我们只需调用相关的解析器函数即可
执行对 A 记录的查询,返回 IPv4 地址:
#include <netdb.h>
struct hostent *gethostbyname(const char * hostname);
// hostent:
struct hostent {
char *h_name; // 正式主机名
char **h_aliases; // 别名s
int h_addrtype; // AF_INET
int h_length; // 4 (32位IP地址)
char **h_addr_list; // IP地址s
}
错误情况
发生错误时,不设置 errno 变量,而是将全局整型变量 h_errno 设置为在头文件 netdb.h 中定义的如下常量之一:
多数解析器提供名为 hstrerror 函数,可以将某个 h_errno 代表的具体错误信息返回
案例
#include "unp.h"
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);
}
[root@centos-5610 names]# ./hostent ethy.cn www.ethy.cn smtp.ethy.cn mail.ethy.cn
official hostname: ym.163.com
alias: ethy.cn
address: 117.147.199.37
gethostbyname error for host: www.ethy.cn: Unknown host
official hostname: cli.ym.ntes53.netease.com
alias: smtp.ethy.cn
alias: smtp.ym.163.com
address: 101.71.155.42
与上一个的功能正好相反,查询 PTR 记录
#inlcude <netdh.h>
struct hostent *gethostbyaddr(const char *addr,
socklen_t len, // 对于 IPv4 为4
int family); // AF_INET
/etc/services
文件中保存了许多知名服务的端口和服务名称的映射,如下:
time 37/tcp timserver
time 37/udp timserver
rlp 39/tcp resource # resource location
rlp 39/udp resource # resource location
nameserver 42/tcp name # IEN 116
nameserver 42/udp name # IEN 116
nicname 43/tcp whois
nicname 43/udp whois
tacacs 49/tcp # Login Host Protocol (TACACS)
tacacs 49/udp # Login Host Protocol (TACACS)
re-mail-ck 50/tcp # Remote Mail Checking Protocol
re-mail-ck 50/udp # Remote Mail Checking Protocol
domain 53/tcp # name-domain server
domain 53/udp
whois++ 63/tcp whoispp
whois++ 63/udp whoispp
#include <netdb.h>
struct servent *getservbyname(const char* servname, const char *protoname);
// servent
struct servent {
char *s_name;
char **s_aliases;
int s_port;
char *s_proto;
}
几个案例:
getservbyname("domain", "udp");
getservbyname("ftp", "tcp");
getservbyname("ftp", NULL);
getesrvbyname("ftp", "udp");
如果没有指定协议,则会自动匹配(一般来说同一服务的 TCP 和 UDP 端口是相同的),但是如果指定的协议没有,则会报错
#include <netdb.h>
struct servent *getservbyport(int port, const char *protoname);
其中 port 参数必须为网络字节序,例如:
getservbyport(htons(53), "udp");
相同的端口上,不同的协议可能有不同的服务!
可以通过上面所学对时间服务的客户端进行改进:
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>");
printf("%s:%s\n", argv[1], argv[2]);
// 获取域名对应的地址
if ((hp = gethostbyname(argv[1])) == NULL) {
// 获取失败, 猜测可能是用户输入了IP地址,所以进行转换
// 将 IP 地址从点分十进制转换为32位二进制
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 {
// 直接转换出来的就是32位二进制
pptr = (struct in_addr**)hp->h_addr_list;
}
// 服务转换
if ((sp = getservbyname(argv[2], "tcp")) == NULL)
err_quit("getservbyname error for %s", argv[2]);
// 循环,对查询得到的所有IP进行访问
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);
}
优势:
这个函数可以处理域名和地址的转换,以及服务和端口的转换
返回一个 sockaddr 列表,而不是上面的结构,这些 sockaddr 可以直接使用
把协议相关的内容隐藏了起来
#include
int getaddrinfo(const char *hostname, // 主机名或地址串
const char *service, // 服务名或十进制端口号数串
const struct addrinfo *hints, // 填写对期望结果的暗示
struct addrinfo **result); // 返回的信息保存在这里
参数解释
hints
- hints 可以是一个空指针,也可以是一个指向某个 addrinfo 结构的指针
- 调用者在其中可以填写关于期望返回信息类型的暗示,前四个属性都可以设置
result
- 如果函数返回0,则会更新 result 对应的 addrinfo 链表
- 链表可能有多个项,这取决于 hostname 关联了几个地址,以及是否有不同的协议类型
- 链表是无序的,也就是说 TCP 未必会放在前面
- 返回的 addrinfo 中的信息可以用于 socket 的相关操作,如 connect、sendto、bind
如果 ai_flags 设置了 AI_CANONNAME
,那么返回的第一个 addrinfo 结构的 ai_canonname 指向所查找主机的规范名字
// addrinfo
struct addrinfo {
int ai_flags; // 一些标志位,用来进行特殊的设置
int ai_family; // AF_XXX 如 AF_INET、AF_INET6
int ai_socktype; // SOCK_XXX 如 SOCK_STREAM SOCK_DGRAM
int ai_protocol; // 协议名称,如 IPPROTO_TCP,如果前面两项可以唯一确认,则此项可为0
socklen_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
}
下面对各个结构进行详细的解释:
ai_flags
用来设置一些标志位
可用的标志值和含义如下:请忽略点符号,这里只是为了方便阅读
标志值
作用
AI_PASSIVE
套接字将用于被动打开(如服务器端)
AI_CAN.ON.NAME
返回主机的规范名称,保存在返回的链表的第一项的
ai_canonname
中AI_NUMERIC.HOST
防止任何类型的名字到地址映射,hostname 必须是一个地址串
AI_NUMERIC.SERV
放置任何类型的服务到端口映射,service 必须是十进制端口号数串
AI_V4.MAPPED
如果同时指定 ai_family 为 AF_INET6,又没有 AAAA 记录,就返回 IPv4 对应的 IPv6 地址
AI_ALL
如果同时指定上一项,则返回 IPv6 和 IPv4 对应的 IPv6 地址
AI_ADDR.CONFIG
按照所在主机的配置选择返回地址类型
family、socktype、protocol
已经在注释上说的比较清楚了,他们可以直接被使用来操作 socket
ai_addrlen
ai_addr 套接字结构的大小
ai_canonname
在上表格中提到了,在需要的时候是主机的规范名称
ai_addr
指向套接字地址的指针,已经被函数填充好了,且类型自适应
ai_next
指向下一位
一个调用的案例:
struct addrinfo hints, *res;
bzero(&hints, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_INET;
getaddrinfo("freebsd4", "domain", &hints, &res);
一个可能的结果:
在调用 getaddrinfo
时,共有 6 个可以选择的参数组合:
常见的组合方式如下所述
客户端
TCP
UDP
如果客户端清楚套接字的类型,则应该设置 hints 为合适的值
服务器
TCP
UDP
如果服务器清楚套接字的类型,则应该设置 hints 为合适的值
返回的 addrinfo 结构的数目
返回的 addrinfo 的数目和暗示信息中 ai_socktype 的对应关系:
#include <netdb.h>
const char *gai_strerror(int error);
作用:对于 getaddrinfo 的非 0 错误码,将该数值作为参数,输出其对应的错误信息
作用
getaddrinfo
返回的所有存储空间都是动态获取的(比如 malloc),包括:
这些存储空间通过 freeaddrinfo
返还给系统
#include <netdb.h>
void freeaddrinfo(struct addrinfo *ai);
注意
作用:简化 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 */
}
作用:创建一个 TCP 套接字并连接到一个服务器
其步骤和上文最佳实践部分基本一致
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);
}
时间程序客户端改进
这个类似于前面部分的,只不过把部分步骤封装在 tcp_connect 中了!
int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t len;
struct sockaddr_storage ss;
if (argc != 3)
err_quit("usage: daytimetcpcli <hostname/IPaddress> <service/port#>");
sockfd = Tcp_connect(argv[1], argv[2]);
len = sizeof(ss);
Getpeername(sockfd, (SA*)&ss, &len);
printf("connected to %s\n", Sock_ntop_host((SA*)&ss, len));
while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
exit(0);
}
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);
}
时间程序服务器端改进
用 tcp_listen 代替部分步骤
int main(int argc, char** argv) {
int listenfd, connfd;
socklen_t len;
char buff[MAXLINE];
time_t ticks;
struct sockaddr_storage cliaddr;
if (argc != 2)
err_quit("usage: daytimetcpsrv1 <service or port#>");
listenfd = Tcp_listen(NULL, argv[1], NULL);
for (;;) {
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA*)&cliaddr, &len);
printf("connection from %s\n", Sock_ntop((SA*)&cliaddr, len));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
再次改进
上述代码有一个问题:
用一个小技巧,可以指定,使用 IPv6 还是 IPv4:
int main(int argc, char** argv) {
int listenfd, connfd;
socklen_t len, addrlen;
char buff[MAXLINE];
time_t ticks;
struct sockaddr_storage cliaddr;
if (argc == 2)
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
else if (argc == 3)
listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: daytimetcpsrv2 [ <host> ] <service or port>");
for (;;) {
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA*)&cliaddr, &len);
printf("connection from %s\n", Sock_ntop((SA*)&cliaddr, len));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
测试案例:
[root@centos-5610 names]# ./daytimetcpsrv2 0::0 daytime
connection from [fe80::5054:ff:fe4d:77d3]:37428
[root@centos-5610 names]# ./daytimetcpsrv2 0.0.0.0 daytime
connection from 10.0.2.15:52178
这个套接字地址结构的大小在 lenp 中返回,不允许是一个空指针(而TCP允许),因为 sendto 和 recvfrom 调用都需要直到套接字地址结构的长度
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);
}
协议无关时间获取客户程序(UDP)
这里协议无关指的是 IPv4 or IPv6
int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t salen;
struct sockaddr* sa;
if (argc != 3)
err_quit("usage: daytimeudpcli1 <hostname/IPaddress> <service/port#>");
sockfd = Udp_client(argv[1], argv[2], (void**)&sa, &salen);
printf("sending to %s\n", Sock_ntop_host(sa, salen));
Sendto(sockfd, "", 1, 0, sa, salen); /* send 1-byte datagram */
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = '\0'; /* null terminate */
Fputs(recvline, stdout);
exit(0);
}
由于 connect 会保存相关的端口和端口信息,所以我们只需要知道返回的描述符就可以了
和 tcp_connect 不同,UDP 的错误在发送一个数据报才能知晓(没有三次握手的过程)
int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t salen;
struct sockaddr* sa;
if (argc != 3)
err_quit("usage: daytimeudpcli1 <hostname/IPaddress> <service/port#>");
sockfd = Udp_client(argv[1], argv[2], (void**)&sa, &salen);
printf("sending to %s\n", Sock_ntop_host(sa, salen));
Sendto(sockfd, "", 1, 0, sa, salen); /* send 1-byte datagram */
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = '\0'; /* null terminate */
Fputs(recvline, stdout);
exit(0);
}
int main(int argc, char** argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t salen;
struct sockaddr* sa;
if (argc != 3)
err_quit("usage: daytimeudpcli1 <hostname/IPaddress> <service/port#>");
sockfd = Udp_client(argv[1], argv[2], (void**)&sa, &salen);
printf("sending to %s\n", Sock_ntop_host(sa, salen));
Sendto(sockfd, "", 1, 0, sa, salen); /* send 1-byte datagram */
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = '\0'; /* null terminate */
Fputs(recvline, stdout);
exit(0);
}
和上面 tcp_server 一样,可以通过那个小技巧来决定使用 IPv4 还是 IPv6
是 getaddrinfo
的互补函数
以一个套接字地址为参数,返回描述其中的主机的一个字符串和描述其中的服务的另一个字符串
协议无关,函数内部自行处理
#include
int getnameinfo(const struct sockaddr *sockaddr, socklen_t addrlen,
char *host, sockelne_t hostlen, // 调用者预先分配
char *serv, socklent_t servlen, // 调用者预先分配
int flags);
flags:
常值
说明
备注
NI_DGRAM
数据报服务
如果知道是UDP,则应设置,以免部分服务端口的冲突
NI_NAME.REQD
若不能从地址解析出名字则返回错误
NI_NO.FQDN
只返回FQDN的主机名部分
如a.foo.com,将截断为a
NI_NUMERIC.HOST
以数串格式返回主机字符串
不要调用 DNS,
NI_NUMERIC.SCOPE
以数串格式返回范围标识字符串
NI_NUMERIC.SERV
以数串格式返回服务字符串
服务器通常应该设置这个标识
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入 OS 调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如 全局变量区, 中断向量表 等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
在使用时需要注意:
gethostbyname
、gethostbyaddr
、getservbyname
、getservbyport
都是不可重入的,因为它们都返回指向同一个静态结构的指针
inet_pton
、inet_ntop
总是可重入的
因为历史原因,inet_ntoa
是不可重入的,不过部分实现提供了使用线程特定数据的可重入版本
getaddrinfo
可重入的前提是由它调用的函数都可重入,这就是说,它应该调用可重入版本的 gethostbyname 和 getservbyname
getnameinfo
可重入的前提是由它调用的函数都可重入,这就是说,它应该调用可重入版本的 gethostbyaddr 和 getservbyport
errno
在每一个进程各有一个副本,但是多线程下也会发生被其他线程修改的情况
解决方案
不使用函数的不可重入版本
就 errno 例子而言,可以使用类似如下的代码进行避免:
void sig_alrm(int signo) {
int errno_save;
errno_save = errno;
if (write( ... ) != nbytes) {
fprintf(stderr, "Errno = %d\n", errno);
}
errno = errno_save;
}
gethostbyname_r
gethostbyaddr_r
具体描述暂略
由于此两章暂时用不到,故略
手机扫一扫
移动阅读更方便
你可能感兴趣的文章