(一) 域名系統(DNS)
DNS主要用於主機名和IP地址之間的映射。主機名可以是簡單的名字ljm,也可以是全限定域名ljm.localdomainbaidu.com等。
1.資源記錄
DNS中的條目稱爲資源記錄(RR)。我們感興趣的RR類型只有幾個:
A A記錄把一個主機名映射爲一個32位的IPv4地址。
AAAA 4A記錄把一個主機名映射爲一個128位的IPv6地址。
ljm IN A 127.0.0.1
IN AAAA 3ffe:1f8d:9bc3:1234:ef93:ac89
PTR 稱爲指針記錄。把IP地址映射爲主機名。對IPv4地址,32位的4個字節先反轉順序,再添加in-addr.arpa。
對於IPv6地址,128位地址每四位組先反轉,再添加ip6.arpa。
例如上面主機ljm的兩個PTR記錄爲:1.0.0.127.in-addr.arpa和8.9.c.a.3.9.f.e….ip6.arpa
2. 解析器和名字服務器
1> 名字服務器。
每個組織機構都會有一個名字服務器(有時也叫DNS服務器)。它們保存着主機名和IP地址之間的映射的資源記錄。
2> 解析器
客戶端和服務器端等應用程序通過解析器的函數來對主機名或IP地址進行解析。
典型的解析器函數爲gethostbyname和gethostbyaddr。前者把主機名映射爲IPv4地址,後者相反。
3> 具體過程
當我們需要解析某個主機名或IP地址時,我們在代碼中調用解析器函數,則解析器函數就會根據解析器配置文件(/etc/resolv.conf)
中的本地名字服務器的IP地址,去發出UDP查詢,如果找不到,則會在整個因特網上查詢其他名字服務器。
如果答案太長,則本地解析器會自動切換到TCP。
(二)gethostbyname和gethostbyaddr函數
1. gethostbyname函數
從主機名到IPv4地址的映射。
該函數執行的只是查詢A記錄,所以即使主機有IPv6地址,也不會返回。
#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
//返回:若成功返回非NULL指針,若出錯返回NULL並設置h_errno
1> 結構體hostent:
struct hostent
{
char* h_name; //規範名字,也可以理解爲全限定域名。
char** h_aliases; //主機別名,一個主機可能有多個主機別名,所以這裏是二維char數組。
int h_addrtype; //地址族:AF_INET
int h_length; //地址長度:4
char** h_addr_list;//IPv4地址
};
1>地址族和地址長度,由於這裏只能是IPv4的地址映射,所以這兩個值不會改變,既然不會改變,爲何需要這兩個參數?感覺有點多餘。
h_addr_list,主機可能會有多個IP地址,所以這裏是二維數組。這裏的IP地址顯然是二進制型的,如要輸出也需轉換表達式型。
2>當gethostbyname返回錯誤時:
這裏當函數發送錯誤時,不是設置errno,而是設置h_errno。一般我們使用函數hstrerror來解析這個h_errno錯誤值。
const char* hstrerror(int h_errno);
3> 我們來寫一個例子程序,輸入任意多個主機名,輸出每個主機名對應的規範名字,別名,IP地址。
#include "unp.h"
int main(int argc, char** argv)
{
char* ptr, ** pptr;
char buff[4];
struct hostent * hptr;
while(--argc>0)
{
ptr=*++argv;
if((hptr=gethostbyname(ptr))==NULL)
{//給每個命令行參數調用gethostbyname()
printf("error for host : %s : %s\n", ptr,hstrerror(h_errno));
continue;
}
printf("host name : %s\n",ptr); //輸出 host name
for(pptr=hptr->h_aliases;*pptr!=NULL;pptr++)
printf("aliases: %s\n",*pptr); //輸出 aliases
switch(hptr->h_addrtype)
{//pptr指向一個指針數組。每個指針指向一個地址,對每個地址調用inet_ntop並輸出返回的字符串
case AF_INET:
for(pptr=hptr->h_addr_list;*pptr!=NULL;pptr++)
printf("aliases: %s\n",Inet_ntop(hptr->h_addrtype, *pptr, buff, sizeof(buff)));
break;
default:
printf("unknown address type\n");
break;
}
}
}
2. gethostbyaddr函數。
與上面的函數作用相反,從一個二進制的IPv4地址轉換到相應的主機名。
#include <netdb.h>
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);// len爲4,family爲AF_INET
//返回:若成功返回非NULL指針,若出錯返回NULL並設置h_errno
函數查詢的是in_addr.arpa消息記錄。
函數返回的是和gethostbyname返回一樣的結構體,只是這裏我們只關心結構體中的h_name。
(三)getservbyname和getservbyport函數
上面提到我們可以用主機名來代替IP地址,下面我們可以使用服務名來代替端口號。
0. 像主機一樣:可以使用主機名和IP地址來標識一臺主機。
服務:可以使用服務名和端口號來標識一個服務。
一個服務可以支持多個協議(TCP,UDP,SCTP),就像一個進程可以同時監聽TCP套接字和UDP套接字一樣。一般一個服務只有一個端口號如http的80服務名和端口號的映射保存在一個文件(/etc/services)中。這樣即使端口號發生變動,我們只需要修改文件(/etc/services)即可。而不需要重新編譯程序。看一下/etc/services的內容:vim /etc/services
1. getservbyname函數:從服務名映射到端口號。
struct servent* getservbyname(const char* servname, const char* protoname);
//返回:如果成功返回非空指針,如果失敗返回NULL
此函數就是查找本地的/etc/services裏面的內容,上面可知,/etc/services每一行有三個東西,
service-name,port,protocol。所以本函數提供了兩個參數來唯一標識端口號。函數返回的結構體servent:
struct servent
{
char* s_name; //服務名
char** s_aliases; //別名
int port; //端口名
char* s_proto; //protocol
};
這裏我們只關心port,注意返回的是網絡字節序的,所以給sockaddr_in賦值時,無需再去轉換字節序。
這裏函數的第二個參數可以是NULL,此時返回哪個端口號取決於實現,但一般無所謂,因爲一個服務不同的協議,通常對應一個端口號。
如果指定protoname,則該協議必須支持。
ser=getservbyname("tcpmux","tcp");//ok
ser=getservbyname("tcpmux","sctp");//error
2. getservbyport函數:從端口號到服務名的映射
struct servent* getservbyport(int port, const char* protoname);
//返回:如果成功返回非空指針,如果失敗返回NULL
注意這裏的port參數必須是網絡字節序的。
struct servent* ser;
ser=getservbyport(htons(1),"tcp");//ok
ser=getservbyport(htons(1),NULL);//ok
ser=getservbyport(htons(1),"sctp");//error
之前的程序我們是用IP地址和端口號來標識一個目標主機上的進程的。現在,我們就可以使用主機名和服務名來標識一個目標主機上的進程。
(四)我們把我們之前的獲取時間客戶端改爲使用主機名和服務名來標識。
names/daynametcpcli1.c
#include "unp.h"
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);
}
這裏我們從命令行輸入參數:服務器主機名和服務名.
我們調用gethostbyname來獲得服務器IP地址列表,一個一個嘗試連接。
如果連接失敗,要關閉套接字,然後重新socket,connect。不能直接重connect。
然後我們調用getservbyname來獲得端口號,這裏的端口號是衆所周知的端口號,因爲該函數是查看本機的/etc/services,來獲知端口號的。而服務器主機也是利用這個衆所周知的端口號服務名進行bind的。
(五)getaddrinfo函數:名->號
因爲gethostbyname和gethostbyaddr這兩個函數只適用於IPv4地址。所以有了既支持IPv4和IPv6的函數getaddrinfo。
getaddrinfo函數能夠處理IP地址和主機名的轉換,而且還能同時處理端口號和服務名的映射。
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo **result);
//返回:成功返回0,失敗返回非0.
1. hostname爲輸入的主機名,service爲輸入服務名,hints爲需要返回結果的提示信息,
該信息會影響返回結果,可以是NULL。Result爲函數返回的結果。
2. 說一下爲何返回結果result前有兩個**,該函數返回一個鏈表,*result指向鏈表的頭結構,注意此時我們是讓函數內部
開闢內存空間,調用函數者只需提供一個指針,在函數內部修改這個指針本身,而不是要修改這個指針指向的東西。
struct addrinfo
{
int ai_flags;//AI_PASSIVE, AI_CANONNAME
int ai_family;//AF_XXX
int ai_socktype;//SOCK_XXX
int ai_protocol;//0 or IPPROTO_XXX
socklen_tai_addrlen;//length of ai_addr
char* ai_canonname;//ptr to canonical name for host
struct sockaddr* ai_addr;//ptr to socket address
struct addrinfo* ai_next;//ptr to next struct in linked list
};
這裏前4個成員是給hints參數設置的。後4個成員是result返回的結果。因爲前4個成員的不同值會影響後4個成員的值。
1> ai_flags:一般的標識值爲AI_PASSIVE和AI_CANONNAME
AI_PASSIVE:套接字將用於被動打開。
AI_CANONNAME:告知函數返回主機的規範名字。即ai_canonname
2> ai_family:即返回的是主機名對應的IPv4地址還是IPv6地址。
3> ai_socktype:即因爲服務名可能對應多個協議,所以這裏指定返回TCP還是UDP
4> ai_protocol:指定具體的協議,當ai_socktype不能唯一標識特定的協議時,就要用到此參數。
即因爲SCTP也屬於流協議,其socktype也是SOCK_STREAM,所以如果某個服務支持TCP和SCTP,則我們就必須指明協議名。即:
IPPROTO_TCP或IPPROTO_UDP。
3 .如果hints爲NULL,則ai_flags,ai_socktype,ai_protocol的值均爲0,ai_family的值爲AF_UNSPEC。
4. 該函數返回是一個鏈表,爲何?
當提供的主機名有多個IP地址時,每個IP都會返回一個對應的結構。
當未指明ai_socktype時,提供的服務名支持多個協議,則每個協議返回一個結構。
所以當主機名有2個IP地址,服務名支持tcp和udp,且未指明ai_socktype,則就會返回2*2=4個結構。
返回的鏈表的結構體的順序是不固定。
5. 返回的結構體中的ai_addr可直接用於socket,connect函數調用。因爲其IP地址都是二進制型的,且IP地址的類型爲sockaddr,和協議無關的。且IP地址和端口號都是網絡字節序的,所以無需任何轉換函數。
6. 函數getaddrinfo的一些常見的輸入
函數有六個可輸入的參數值:hostname,,service,以及hints的前4個成員。
(1) 客戶端
對於TCP和UDP客戶端而言,我們使用該函數,用來創建連接,連接服務器的。所以我們需要指定hostname和service的值,而至於hints的4個成員,如果確認知道自己處理的是哪一類型套接字,應該指定ai_socktype或和ai_protocol。一般ai_flags爲AI_PASSIVE。而一般不指定ai_family,因爲主機有可能用於IPv4和IPv6地址,我們需要一個一個嘗試socket->connect。
(2) 服務器端
對於服務器,我們使用該函數,只是用來指定端口號的。所以我們一般不指定主機名,只指定service。而主機名爲空,則返回的套接字地址中的IP地址爲通配地址,也正是我們想要的。
注意如果不指定ai_family或指定爲AF_UNSPEC,這時至少返回兩個結構,一個包含Ipv4的通配地址INADDR_ANY,另一個包含IPv6的通配地址IN6ADDR_ANY_INIT。這時我們可以使用select來監聽這兩個套接字。
對於hints的前4個成員,我們指定ai_flags爲AI_PASSIVE。且應該指定套接字的類型,以防止返回多個結構。
注意:因爲我們後續需要調用accept函數來獲得套接字,我們就需要先new一個套接字結構,這個套接字結構的大小如何確定?
函數返回的鏈表中,addrinfo的每個結構體中都會有其套接字結構的大小ai_addrlen。所以我們根據這個值可以確認套接字的大小。
7. 可以說,getaddrinfo函數功能很強大,但使用起來也很複雜。. 如果hints爲NULL,則ai_flags,ai_socktype,ai_protocol的值均爲0,ai_family的值爲AF_UNSPEC。
(七)freeaddrinfo函數:
函數getaddrinfo函數返回一個結構體鏈表,其所有的存儲空間都是動態獲取的,如new或malloc。
所以我們用完之後要釋放這些內存。就是調用freeaddrinfo來釋放的。
void freeaddrinfo(struct addrinfo* ai);
典型的用法:
struct addrinfo* result;
intret=getaddrinfo(...,&result);
freeaddrinfo(result);
這樣就可以釋放返回的整個鏈表。
但是這有個問題,比如我們通過遍歷找到了我們所需的結構,然後把這個結構體複製出來,然後調用該函數釋放所有內存。
但是:如果只複製這個結構體本身的話,是有問題的,因爲這個結構體內有指針(套接字結構指針和規範名字指針),複製的時候切記把這些指針指向的空間也要複製。不然只複製指針的話,則freeaddrinfo會釋放掉所有內存,這樣我們複製出來的這個結構體的指針指向的是已釋放的內存,這樣很危險。所以複製結構體時,切記需要深度複製。
(八) 一些實際的例子
1. 對於TCP客戶端:
提供確認的主機名和服務名,然後對於服務器的每個IP地址,進行:
while(){
socket->connect
}
2. 對於TCP服務器端:
一般不提供主機名,而是提供服務名,然後對於本地的每個類型的通配地址以及服務名,進行:
while(){
socket->bind
}
accept()
…
如果bind成功,則跳出循環。有點問題,這樣只能綁定一個IP地址:或爲IPv4的通配地址,或爲IPv6的通配地址。
(九)getnameinfo函數:該函數爲getaddrinfo的互補函數,即提供套接字地址,返回主機名和服務名。
int getnameinfo(const structsockaddr* addr, socklen_t addrlen,
char*hostname, socklen_t hostlen,
char* serv,socklen_t servlen, int flags);
//返回:成功返回0,失敗返回非0
各個參數都很明顯,只有最後一個flag,其用於指示一些東西。比如:
NI_DGRAM:指示返回的服務是基於UDP的,因爲有可能端口號相同的不同協議對應的是不同的服務。
其他的一些標誌值:p267
注意1:這裏可以把標記值進行或,然後這樣就可以同時設置兩個標誌值。
注意2:getaddrinfo和getnameinfo都是設計DNS的,而一般服務器是不使用getnameinfo函數的,
直接用IP地址來標識就可以了,因爲getnameinfo設計DNS,其是很耗時的。
(十) 可重入函數
1. 我們先來看看gethostbyname和gethostbyaddr的代碼:
static hostent host;
struct hostent* gethostbyname(const char* hostname)
{
/*.....*/
return (&host);
}
struct hostent* gethostbyaddr(const char* addr,socklen_t len, int family)
{
/*.....*/
return (&host);
}
可以看到函數返回的都是一個static的對象。問題就在這上面。
假如我們在一個主程序中調用gethostbyname函數,且在信號處理函數中也調用gethostbyname,看看會發生什麼:
main()
{
struct hostent* hp;
...
signal(SIGALRAM,sig_alrm);
...
hp=gethostbyname(...);
}
void sig_alrm(intsigno)
{
struct hostent* hp1;
hp1=gethostbyname(...);
}
假如此時主程序中執行到函數gethostbyname期間,而且函數已經處理好static的host對象了,準備返回了,此時來了個信號,則主程序中斷,去處理這個信號,而在信號處理函數中重新調用了gethostbyname,那麼該host對象將被重用,因爲此時只有一個進程,只保留一個副本,這麼一來,原先由主程序計算出的值被重寫成了信號處理函數調用計算出的值。則這樣就會產生錯誤。這樣就是不可重入函數。
2. 看看以前的函數可重入性:
1> gethostbyname、gethostbyaddr、getservbyname、getservbyport都是不可重入函數(返回的是static的結構指針)
2> inet_pton、inet_ntop都是可重入的。
3> getaddrinfo可重入的前提是調用的函數都是可重入的,比如函數內調用的是gethostbyname可重入版本,getservbyname的可重入版本。
4> getnameinfo可重入的前提是調用的函數都是可重入的,比如函數內調用的是gethostbyaddr可重入版本,getservbyport的可重入版本。
3. errno變量也有類似的問題。
首先,每個進程有一個errno的副本。而在同一個進程中,例如如下代碼:
if(close(fd)<0)
{
fprintf(stderr,"close error, errno= %s\n", errno);
}
如果此時close函數產生錯誤,則內核設置errno,當程序調用結束close時,還未來得及執行輸出時,此時信號來了,信號處理函數中也產生了錯誤,則errno被重置,則返回到主程序時,就有問題了。
4. 解決可重入性問題的一個方法:
就是在信號處理函數中永遠不要調用不可重入函數,且可把errno先進行保存,然後在函數最後還原回來。
如:
void sig_handler(int signo)
{
int errno_save=errno;
/*...other code*/
errno=errno_save;
}
還有在信號處理函數中,不要調用標準I/O函數,如fprintf等。因爲很多版本實現的標準I/O函數都是不可重入的。
(十一)gethostbyname_r和gethostbyaddr_r函數(可重入版本)
首先,把不可重入函數改爲可重入函數有兩種方法:
1> 不可重入函數的問題在於返回一個全局static對象。而我們可以由調用者動態開闢一個對象空間,然後交由函數進行修改。如gethostbyname,我們可以讓調用者先動態開闢一個hostent對象,然後讓該函數來進行處理。
gethostbyname_r和gethostbyaddr_r函數就是這麼做的。
但引入的問題:
調用者不僅需要new一個hostent對象,還要提供hostent對象中的指針指向的空間。
struct hostent* gethostbyname_r(const char* hostname,struct hosten* result, char*buf, int buflen, int*h_errnop);
其中result就是交由函數修改的對象。而buf就是這個對象中的指針指向的一塊內存空間。錯誤碼爲h_errnop,而不是全局變量h_errno
這裏很難確認這個buf的大小。不好用。
2> 我們可以讓不可重入函數本身new一個對象,最後返回。而不是返回一個全局static對象。getaddrinfo函數就是這麼做的。
引入的問題:該函數內部分配的空間,必須需要調用者顯示釋放掉,即調用函數freeaddrinfo。否則造成內存泄漏。
(十二) 其他網絡相關信息
我們上面提到了gethostby…、getservby…。
網絡信息包含:主機,服務,網絡,協議。因爲我們有:p273
其中主機和網絡是通過DNS來獲取的。
協議和服務是通過查詢本地主機的文件來獲取的。