《Unix 網路程式設計》11:名字和地址轉換

2022-05-29 12:01:08

名字和地址轉換

系列文章導航:《Unix 網路程式設計》筆記

域名系統

簡介

域名系統主要用於主機名字和 IP 地址之間的對映。主機名可以是:

  • 簡單名字,如:centos01
  • 全限定域名(FQDN[1]),如:xxx.com

資源記錄

記錄 作用
A 指向IPv4
AAAA 指向IPv6
PTR 把IP地址對映為主機名
MX 郵件記錄
CNAME 為二級域名指定域名或IP

解析器和名字伺服器

DNS

DNS替代方法

如果使用 DNS 查詢主機名,則使用 /etc/resolv.conf 指定的 DNS

有如下替代方法:

  • 靜態主機檔案,如 /etc/hosts
  • 網路資訊系統(NIS)
  • 輕權目錄存取協定(LDAP)

所有這些差異對應用開發人員是透明的,我們只需呼叫相關的解析器函數即可

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 中定義的如下常數之一:

  • HOST_NOT_FOUND
  • TRY_AGAIN
  • NO_RECOVERY
  • NO_DATA(等同於 NO_ADDRESS):表明主機在,但是沒有 A 記錄

多數解析器提供名為 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 介紹

優勢:

  • 這個函數可以處理域名和地址的轉換,以及服務和埠的轉換
  • 返回一個 sockaddr 列表,而不是上面的結構,這些 sockaddr 可以直接使用
  • 把協定相關的內容隱藏了起來
#include <netdb.h>

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 結構

// 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 個可以選擇的引數組合:

  • hostname、service
  • ai_flags、ai_family、ai_socktype、ai_protocol

常見的組合方式如下所述

使用者端

  • TCP

    1. 指定 hostname 和 service
    2. 返回後,針對返回的所有 IP 地址,逐一呼叫 socket 和 connect,直到有一個連線成功,或全部失敗
  • UDP

    1. 返回後,呼叫 sendto 或 connect

    2. 如果客戶能夠判定第一個地址看起來不工作,則嘗試其餘的地址

      兩種判斷方式:

      1. 已連線,獲取到錯誤資訊
      2. 未連線,等待訊息超時
  • 如果使用者端清楚通訊端的型別,則應該設定 hints 為合適的值

伺服器

  • TCP

    1. 只指定 service,不指定hostname
    2. 在 hints 結構中指定 AI_PASSIVE 標誌
    3. TCP 伺服器隨後呼叫 socket、bind、listen
  • UDP

    1. 呼叫 socket、bind、recvfrom
  • 如果伺服器清楚通訊端的型別,則應該設定 hints 為合適的值

返回的 addrinfo 結構的數目

返回的 addrinfo 的數目和暗示資訊中 ai_socktype 的對應關係:

gai_strerror

#include <netdb.h>

const char *gai_strerror(int error);

作用:對於 getaddrinfo 的非 0 錯誤碼,將該數值作為引數,輸出其對應的錯誤資訊

freeaddrinfo

作用

getaddrinfo 返回的所有儲存空間都是動態獲取的(比如 malloc),包括:

  • addrinfo 結構
  • ai_addr 結構
  • ai_canonname 字串

這些儲存空間通過 freeaddrinfo 返還給系統

#include <netdb.h>

void freeaddrinfo(struct addrinfo *ai);

注意

  • 在頭節點上呼叫,會釋放整個 addrinfo 連結串列
  • 注意如果你採用淺拷貝使用了某些屬性,則再存取時可能會因為該地址已經被釋放而出錯

自己封裝函數

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

再次改進

上述程式碼有一個問題:

  • tcp_listen 的第一個引數是 NULL
  • 而且 tcp_listen 內部指定的地址族為 AF_UNSPEC
  • 兩者結合可能導致 getaddrinfo 返回非期望地址族的通訊端地址結構

用一個小技巧,可以指定,使用 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

  • 由於 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);
}

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

  • getaddrinfo 的互補函數
  • 以一個通訊端地址為引數,返回描述其中的主機的一個字串和描述其中的服務的另一個字串
  • 協定無關,函數內部自行處理
#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 排程下去執行另外一段程式碼,而返回控制時不會出現什麼錯誤;而不可重入的函數由於使用了一些系統資源,比如 全域性變數區, 中斷向量表 等,所以它如果被中斷的話,可能會出現問題,這類函數是不能執行在多工環境下的。

在使用時需要注意:

  • gethostbynamegethostbyaddrgetservbynamegetservbyport 都是不可重入的,因為它們都返回指向同一個靜態結構的指標

  • inet_ptoninet_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

具體描述暫略

本文沒有提到的

  • 作廢的IPv6地址解析函數
  • 其他網路相關資訊

由於此兩章暫時用不到,故略


  1. Full Qualified Domain Name ↩︎