系列文章導航:《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);
}
優勢:
#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
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
- 指定 hostname 和 service
- 返回後,針對返回的所有 IP 地址,逐一呼叫 socket 和 connect,直到有一個連線成功,或全部失敗
UDP
返回後,呼叫 sendto 或 connect
如果客戶能夠判定第一個地址看起來不工作,則嘗試其餘的地址
兩種判斷方式:
- 已連線,獲取到錯誤資訊
- 未連線,等待訊息超時
如果使用者端清楚通訊端的型別,則應該設定 hints 為合適的值
伺服器
TCP
- 只指定 service,不指定hostname
- 在 hints 結構中指定 AI_PASSIVE 標誌
- TCP 伺服器隨後呼叫 socket、bind、listen
UDP
- 呼叫 socket、bind、recvfrom
如果伺服器清楚通訊端的型別,則應該設定 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);
}
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 <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
、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
具體描述暫略
由於此兩章暫時用不到,故略
Full Qualified Domain Name ↩︎