编程语言
首页 > 编程语言> > 《Unix 网络编程》11:名字和地址转换

《Unix 网络编程》11:名字和地址转换

作者:互联网

名字和地址转换

系列文章导航:《Unix 网络编程》笔记

域名系统

简介

域名系统主要用于主机名字和 IP 地址之间的映射。主机名可以是:

资源记录

记录 作用
A 指向IPv4
AAAA 指向IPv6
PTR 把IP地址映射为主机名
MX 邮件记录
CNAME 为二级域名指定域名或IP

解析器和名字服务器

DNS

DNS替代方法

如果使用 DNS 查找主机名,则使用 /etc/resolv.conf 指定的 DNS

有如下替代方法:

所有这些差异对应用开发人员是透明的,我们只需调用相关的解析器函数即可

IPv4 函数学习

域名和地址转换

gethostbyname

执行对 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

gethostbyaddr

与上一个的功能正好相反,查询 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

getservbyname

#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 端口是相同的),但是如果指定的协议没有,则会报错

getservbyport

#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);
}

getaddrinfo 函数

getaddrinfo 介绍

优势:

#include <netdb.h>

int getaddrinfo(const char *hostname, // 主机名或地址串
               const char *service, // 服务名或十进制端口号数串
               const struct addrinfo *hints, // 填写对期望结果的暗示
               struct addrinfo **result); // 返回的信息保存在这里

参数解释

addrinfo 结构

// 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;
}

下面对各个结构进行详细的解释:

案例

一个调用的案例:

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 个可以选择的参数组合:

常见的组合方式如下所述

客户端

服务器

返回的 addrinfo 结构的数目

返回的 addrinfo 的数目和暗示信息中 ai_socktype 的对应关系:

gai_strerror

#include <netdb.h>

const char *gai_strerror(int error);

作用:对于 getaddrinfo 的非 0 错误码,将该数值作为参数,输出其对应的错误信息

freeaddrinfo

作用

getaddrinfo 返回的所有存储空间都是动态获取的(比如 malloc),包括:

这些存储空间通过 freeaddrinfo 返还给系统

#include <netdb.h>

void freeaddrinfo(struct addrinfo *ai);

注意

自己封装函数

host_serv

作用:简化 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_connect

作用:创建一个 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);
}

tcp_listen

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

udp_client

这个套接字地址结构的大小在 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);
}

udp_connect

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);
}

udp_server

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

getnameinfo

#include <netdb.h>

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_r

gethostbyaddr_r

具体描述暂略

本文没有提到的

由于此两章暂时用不到,故略


  1. Full Qualified Domain Name ↩︎

标签:11,struct,ai,res,编程,char,int,Unix,hints
来源: https://www.cnblogs.com/lymtics/p/16320004.html