Unix网络编程学习笔记之第11章 名字与地址转换
一、 域名系統(DNS)
1. 簡介
DNS主要用于主機名和IP地址之間的映射。
主機名可以是簡單的名字ljm,也可以是全限定域名ljm.localdomainbaidu.com等。
?
2.資源記錄
DNS中的條目稱為資源記錄(RR)。我們感興趣的RR類型只有幾個:
A???????????? A記錄把一個主機名映射為一個32位的IPv4地址。
AAAA??? 4A記錄把一個主機名映射為一個128位的IPv6地址。
例如:
ljm???????????????IN????? A??? 127.0.0.1
????????????????????IN????? AAAA3ffe: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
?
3. 解析器和名字服務器
1> 名字服務器。
每個組織機構都會有一個名字服務器(有時也叫DNS服務器)。它們保存著主機名和IP地址之間的映射的資源記錄。
2> 解析器
客戶端和服務器端等應用程序通過解析器的函數來對主機名或IP地址進行解析。
典型的解析器函數為gethostbyname和gethostbyaddr。
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_errno1> 我們來看看結構體hostent:
struct hostent{char* h_name; //規范名字char** h_aliases; //主機別名int h_addrtype; //地址族:AF_INETint h_length; //地址長度:4char** h_addr_list;//IPv4地址 };規范名字,也可以理解為全限定域名。
主機別名,一個主機可能有多個主機別名,所以這里是二維char數組。
地址族和地址長度,由于這里只能是IPv4的地址映射,所以這兩個值不會改變,既然不會改變,為何需要這兩個參數?感覺有點多余。
h_addr_list,主機可能會有多個IP地址,所以這里是二維數組。這里的IP地址顯然是二進制型的,如要輸出也需轉換表達式型。注意這里是char型的二維數組。為何不是in_addr結構的一維數組?不得而知。
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){printf("error for host : %s : %s\n", ptr,hstrerror(h_errno));continue;}printf("host name : %s\n",ptr);for(pptr=hptr->h_aliases;*pptr!=NULL;pptr++)printf("aliases: %s\n",*pptr);switch(hptr->h_addrtype){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的內容:
# service-name?port/protocol? [aliases ...]?? [# comment]
tcpmux?????????1/tcp?????????????????????????? #TCP port service multiplexer
tcpmux?????????1/udp?????????????????????????? # TCPport service multiplexer
http???????????80/tcp????????? www www-http??? # WorldWideWeb HTTP
http???????????80/udp????????? www www-http??? # HyperText Transfer Protocol
http???????????80/sctp???????????????????????? #HyperText Transfer Protocol
https??????????443/tcp???????????????????????? #http protocol over TLS/SSL
https??????????443/udp???????????????????????? #http protocol over TLS/SSL
https?????????? 443/sctp??????????????????????? # http protocol overTLS/SSL
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; };這里我們只關系的是port,注意返回的是網絡字節序的,所以給sockaddr_in賦值時,無需再去轉換字節序。
這里函數的第二個參數可以是NULL,此時返回哪個端口號取決于實現,但一般無所謂,因為一個服務通常不同的協議,通常對應一個端口號。
如果指定protoname,則該協議必須支持。
ser=getservbyname("tcpmux","tcp");//ok ser=getservbyname("tcpmux","sctp");//error2. 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地址和端口號來標識一個目標主機上的進程的。到目前為止,我們就可以使用主機名和服務名來標識一個目標主機上的進程。
?
四、 我們把我們之前的獲取時間客戶端改為使用主機名和服務名來標識。
#include "unp.h" #define MAXLINE 1024 #define PORT 13 void err_sys(const char* s) {fprintf(stderr, "%s\n", s);exit(1); } int main(int argc, char** argv) {int sockfd,nbytes;struct sockaddr_in servaddr;char buff[MAXLINE+1];char str[128];struct hostent* hp;struct in_addr** pptr;//注意這里是一維數組,每個元素指向一個結構體。struct servent* ser;if(argc!=3)//輸入主機名和服務名err_sys("input error");hp=gethostbyname(argv[1]);if(hp==NULL){err_sys("wrong hostname");}pptr=(struct in_addr**)hp->h_addr_list;if((ser=getservbyname(argv[2],"tcp")==NULL))err_sys("wrong server name");for(;*pptr!=NULL;pptr++){if((sockfd=socket(AF_INET,SOCK_STREAM, 0))<0)continue;bzero(&servaddr,sizeof(servaddr));servaddr.sin_family=AF_INET;servaddr.sin_port=ser->s_port;memcpy(&servaddr.sin_addr,*pptr,sizeof(struct in_addr));printf("trying %s\n",inet_ntop(AF_INET,(struct sockaddr*)&servaddr,str,sizeof(str)));if(connect(sockfd,(struct sockaddr*)&servaddr, sizeof(servaddr))==0)break;//successclose(sockfd);}if(*pptr==NULL)err_sys("unable to connect");while(nbytes=read(sockfd,buff,MAXLINE)>0){buff[nbytes]=0;fputs(buff,stdout);}exit(0); }這里我們從命令行輸入參數:服務器主機名和服務名
我們調用gethostbyname來獲得服務器IP地址列表,一個一個嘗試連接。如果連接失敗,要關閉套接字,然后重新socket,connect。不能直接重connect。
然后我們調用getservbyname來獲得端口號,這里的端口號是眾所周知的端口號,因為該函數是查看本機的/etc/services,來獲知端口號的。而服務器主機也是利用這個眾所周知的端口號服務名進行bind的。
?
五、getaddrinfo函數
因為gethostbyname和gethostbyaddr這兩個函數只適用于IPv4地址。所以有了既支持IPv4和IPv6的函數getaddrinfo。
getaddrinfo函數能夠處理IP地址和主機名的轉換,而且還能同時處理端口號和服務名的映射。
#include <netdb.h> 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指向鏈表的頭結構,注意此時我們是讓函數內部開辟內存空間,調用函數者只需提供一個指針,在函數內部修改這個指針本身,而不是要修改這個指針指向的東西。例如:
int main() {char * s1, * s2;getaddrinfo1(&s1);getaddrinfo2(s2); } void getaddrinfo1(char** s) {*s=new char[]; } void getaddrinfo2(char* s) {s=new char[]; }顯然s1指向了一個內部的合理空間,而s2經過函數調用后,仍然屬于野指針。
?
3. 這里看一下結構體addrinfo:
struct addrinfo{int ai_flags;//AI_PASSIVE, AI_CANONNAMEint ai_family;//AF_XXXint ai_socktype;//SOCK_XXXint ai_protocol;//0 or IPPROTO_XXXsocklen_tai_addrlen;//length of ai_addrchar* ai_canonname;//ptr to canonical name for hoststruct sockaddr* ai_addr;//ptr to socket addressstruct 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。
下面4個成員就不說了,很清楚。
?
4. 如果hints為NULL,則ai_flags,ai_socktype,ai_protocol的值均為0,ai_family的值為AF_UNSPEC。
?
5. 該函數返回是一個鏈表,為何?
當提供的主機名有多個IP地址時,每個IP都會返回一個對應的結構。
當未指明ai_socktype時,提供的服務名支持多個協議,則每個協議返回一個結構。
所以當主機名有2個IP地址,服務名支持tcp和udp,且未指明ai_socktype,則就會返回2*2=4個結構。
返回的鏈表的結構體的順序是不固定。
?
6. 返回的結構體中的ai_addr可直接用于socket,connect函數調用。因為其IP地址都是二進制型的,且IP地址的類型為sockaddr,和協議無關的。且IP地址和端口號都是網絡字節序的,所以無需任何轉換函數。
?
7. 函數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。所以我們根據這個值可以確認套接字的大小。
?
8. 可以說,getaddrinfo函數功能很強大,但使用起來也很復雜。
?
六、gai_strerror函數
上文提到,getaddrinfo函數,成功返回0,失敗返回非0的整數.
而gai_strerror函數就是用來解釋失敗的錯誤整數的。
const char* gai_strerror(int error); 典型的使用方法: int ret=getaddrinfo(...); if(ret!=0) {err_sys("%s\n",gai_strerror(ret)); }七、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的互補函數,即提供套接字地址,返回主機名和服務名。
<pre name="code" class="cpp">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的,因為有可能端口號相同的不同協議對應的是不同的服務。
其他的一些標志值:
注意:這里可以把標記值進行或,然后這樣就可以同時設置兩個標志值。
注意:getnameinfo和getaddrinfo都是設計DNS的,而一般服務器是不使用getnameinfo函數的,直接用IP地址來標識就可以了,因為getnameinfo設計DNS,其是很耗時的。
?
十、 可重入函數
1. 我們先來看看gethostbyname和gethostbyaddr的代碼:
static structhostent 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都是不可重入函數
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函數都是不可重入的。
?
5. 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…。
網絡信息包含:主機,服務,網絡,協議。因為我們有:
其中主機和網絡是通過DNS來獲取的。
協議和服務是通過查詢本地主機的文件來獲取的。
?
?
?
?
?
?
?
?
?
?
?
?
?
?
總結
以上是生活随笔為你收集整理的Unix网络编程学习笔记之第11章 名字与地址转换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cfa的pv怎么用计算机算,CFA考点解
- 下一篇: VSCode 远程开发:WLS 2 +