在學(xué)習(xí)用套接字做一些有意義的事情之前,需要知道如何確定一個目標(biāo)通信進(jìn)程。
進(jìn)程的標(biāo)識有兩個部分:計(jì)算機(jī)的網(wǎng)絡(luò)地址可以幫助標(biāo)識網(wǎng)絡(luò)上想與之通信的計(jì)算機(jī),而服務(wù)可以幫助標(biāo)識計(jì)算機(jī)上特定的進(jìn)程。
1、字節(jié)序
運(yùn)行在同一臺計(jì)算機(jī)上的進(jìn)程相互通信時(shí),一般不用考慮字節(jié)的順序(字節(jié)序),字節(jié)序是一個處理器架構(gòu)特性,用于指示像整數(shù)這樣的大數(shù)據(jù)類型的內(nèi)部字節(jié)順序。圖16-1顯示一個32位整數(shù)內(nèi)部的字節(jié)是如何排序的。
圖16-1 32位整數(shù)內(nèi)部的字節(jié)序
如果處理器架構(gòu)支持大端(big-endian)字節(jié)序,那么最大字節(jié)地址對應(yīng)于數(shù)字最低有效字節(jié)(LSB);小端(little-endian)字節(jié)序則相反:數(shù)字最低字節(jié)對應(yīng)于最小字節(jié)地址。注意,不管字節(jié)如何排序,數(shù)字最高位總是在左邊,最低位總是在右邊。
網(wǎng)絡(luò)協(xié)議指定了字節(jié)序,因此異構(gòu)計(jì)算機(jī)系統(tǒng)能夠交換協(xié)議信息而不會混淆字節(jié)序。TCP/IP協(xié)議棧采用大端字節(jié)序。應(yīng)用程序交換格式化數(shù)據(jù)時(shí),字節(jié)序問題就會出現(xiàn)。對于TCP/IP,地址用網(wǎng)絡(luò)字節(jié)序表示,所以應(yīng)用程序有時(shí)需要在處理器的字節(jié)序與網(wǎng)絡(luò)字節(jié)序之間的轉(zhuǎn)換。
對于TCP/IP應(yīng)用程序,提供了四個通用函數(shù)以實(shí)施在處理器字節(jié)序和網(wǎng)絡(luò)字節(jié)序之間的轉(zhuǎn)換。
#include <arpa/inet.h>uint32_t htonl(uint32_t hostint32);返回值:以網(wǎng)絡(luò)字節(jié)序表示的32位整型數(shù)uint16_t htons(uint16_t hostint16);返回值:以網(wǎng)絡(luò)字節(jié)序表示的16位整型數(shù)uint32_t ntohl(uint32_t netint32);返回值:以主機(jī)字節(jié)序表示的32位整型數(shù)uint16_t ntohs(uint16_t netint16);返回值:以主機(jī)字節(jié)序表示的16位整型數(shù)
h表示“主機(jī)(host)”字節(jié)序,
n表示“網(wǎng)絡(luò)(network)”字節(jié)序。
l表示“長(long)”整數(shù)(即4個字節(jié)),
s表示“短(short)”整數(shù)(即2個字節(jié))。
這四個函數(shù)定義在<arpa/inet.h>中,也有比較老的系統(tǒng)將其定義在<netinet/in.h>中。
2、地址格式
地址標(biāo)識了特定通信域中的套接字端點(diǎn),地址格式與特定的通信域相關(guān)。為使不同格式地址能夠被傳入到套接字函數(shù),地址被強(qiáng)制轉(zhuǎn)換成通用的地址結(jié)構(gòu)sockaddr表示:
struct sockaddr { sa_family_t sa_family; /* address family */ char sa_data[]; /* variable-length address */ ...... };
套接字實(shí)現(xiàn)可以自由地添加額外的成員并且定義sa_data成員的大小。例如在linux中,該結(jié)構(gòu)定義如下:
struct sockaddr { sa_family_t sa_family; /* address family */ char sa_data[14]; /* variable-length address */};
因特網(wǎng)地址定義在<netinet/in.h>中。在IPv4因特網(wǎng)域(AF_INET)中,套接字地址用如下結(jié)構(gòu)sockaddr_in表示:
struct in_addr { int_addr_t s_addr; /* IPv4 address */};struct sockaddr_in { sa_family_t sin_family; /* address family */ in_port_t sin_port; /* port number */ struct in_addr sin_addr; /* IPv4 address */};
數(shù)據(jù)類型in_port_t定義為uint16_t。數(shù)據(jù)類型in_addr_t定義成uint32_t。這些整數(shù)類型在<stdint.h>中定義并指定了相應(yīng)的位數(shù)。與IPv4因特網(wǎng)域(AF_INET)相比較,IPv6因特網(wǎng)域(AF_INET6)套接字地址用如下結(jié)構(gòu)sockaddr_in6表示:
struct in6_addr { uint8_t s6_addr[16]; /* IPv6 address */};struct sockaddr_in6 { sa_family_t sin6_family; /* address family */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* traffic class and flow info */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* set of interfaces for scope */};
這些是Single UNIX Specification必須的定義,每個實(shí)現(xiàn)可以自由地添加額外的字段。例如,在Linux中,sockaddr_in定義如下:
struct sockaddr_in { sa_family_t sin_family; /* address family */ in_port_t sin_port; /* port number */ struct in_addr sin_addr; /* IPv4 address */ unsigned char sin_zero[8
]; /* filler */};
其中成員sin_zero為填充字段,必須全部被置為0。
注意,盡管sockaddr_in與sockaddr_in6相差比較大,它們均被強(qiáng)制轉(zhuǎn)換成sockaddr結(jié)構(gòu)傳入到套接字例程中。
有時(shí),需要打印出能被人而不是計(jì)算機(jī)所理解的地址格式。BSD網(wǎng)絡(luò)軟件中包含了函數(shù)inet_addr和inet_ntoa,用于在二進(jìn)制地址格式與點(diǎn)分十進(jìn)制字符串表示(a.b.c.d)之間相互轉(zhuǎn)換。這些函數(shù)僅用于IPv4地址,但功能相似的兩個函數(shù)inet_ntop和inet_pton支持IPv4和IPv6地址。
#include <arpa/inet.h>const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);返回值:若成功則返回地址字符串指針,若出錯則返回NULLint inet_pton(int domain, const char *restrict str, void *restrict addr);返回值:若成功則返回1,若格式無效則返回0,若出錯則返回-1
函數(shù)inet_ntop將網(wǎng)絡(luò)字節(jié)序的二進(jìn)制地址轉(zhuǎn)換成文本字符串格式,inet_pton將文本字符串格式轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序的二進(jìn)制地址。參數(shù)domain僅支持兩個值:AF_INET和AF_INET6。
對于inet_ntop,參數(shù)size指定了用以保存文本字符串的緩沖區(qū)(str)的大小。兩個常數(shù)用于簡化工作:INET_ADDRSTRLEN定義了足夠大的空間來存放表示IPv4地址的文本字符串,INET6_ADDRSTRLEN定義了足夠大的空間來存放表示IPv6地址的文本字符串。
對于inet_pton,如果domain是AF_INET,緩沖區(qū)addr需要有足夠大的空間來存放32位地址,如果domain是AF_INET6則需要足夠大的空間來存放128位地址。
3、地址查詢
理想情況下,應(yīng)用程序不需要了解套接字地址的內(nèi)部結(jié)構(gòu)。如果應(yīng)用程序只是簡單地傳遞類似于sockaddr結(jié)構(gòu)的套接字地址,并且不依賴于任何協(xié)議相關(guān)的特性,那么可以與提供相同服務(wù)的許多不同協(xié)議協(xié)作。
歷史上,BSD網(wǎng)絡(luò)軟件提供接口訪問各種網(wǎng)絡(luò)配置信息。http://www.CUOXin.com/nufangrensheng/p/3507496.html中,簡要地討論了網(wǎng)絡(luò)數(shù)據(jù)文件和用來訪問這種信息的函數(shù)。在本節(jié),將更加詳細(xì)地討論一些細(xì)節(jié),并且引入新的函數(shù)來查詢尋址信息。
這些函數(shù)返回的網(wǎng)絡(luò)配置信息可能存放在許多地方。它們可以保存在靜態(tài)文件中(如/etc/hosts,/etc/services等),或者可以由命名服務(wù)管理,例如DNS(Domain Name System)或者NIS(Network Information Service)。無論這些信息放在何處,這些函數(shù)同樣能夠訪問它們。
通過調(diào)用gethostent,可以找到給定計(jì)算機(jī)的主機(jī)信息。
#include <netdb.h>struct hostent *gethostent(void);返回值:若成功則返回指針,若出錯則返回NULLvoid sethostent(int stayopen);void endhostent(void);
如果主機(jī)數(shù)據(jù)文件沒有打開,gethostent會打開它。函數(shù)gethostent返回文件的下一個條目。函數(shù)sethostent會打開文件,如果文件已經(jīng)被打開,那么將其回繞。函數(shù)endhostent將關(guān)閉文件。
當(dāng)gethostent返回時(shí),得到一個指向hostent結(jié)構(gòu)的指針,該結(jié)構(gòu)可能包含一個靜態(tài)的數(shù)據(jù)緩沖區(qū)。每次調(diào)用gethostent將會覆蓋這個緩沖區(qū)。數(shù)據(jù)結(jié)構(gòu)hostent至少包含如下成員:
struct hostent { char *h_name; /* name of host */ char **h_aliases; /* pointer to alternate host name array */ int h_addrtype; /* address type */ int h_length; /* length in bytes of address */ char **h_addr_list; /* pointer to array of network addresses */ ...};
返回的地址采用網(wǎng)絡(luò)字節(jié)序。
兩個附加的函數(shù)gethostbyname和gethostbyaddr,原來包含在hostent函數(shù)里面,現(xiàn)在被認(rèn)為是過時(shí)的,馬上將會看到其替代函數(shù)。
能夠采用一套相似的接口來獲得網(wǎng)絡(luò)名字和網(wǎng)絡(luò)號。
#include <netdb.h>struct netent *getnetbyaddr(uint32_t net, int type);struct netent *getnetbyname(const char *name);struct netent *getnetent(void);以上三個函數(shù)的返回值:若成功則返回指針,若出錯則返回NULLvoid setnetent(int stayopen);void endnetent(void);
結(jié)構(gòu)netent至少包含如下字段:
struct netent { char *n_name; /* network name */ char **n_aliases; /* alternate network name array pointer */ int n_addrtype; /* address type */ uint32_t n_net; /* network number */ ...};
網(wǎng)絡(luò)號按照網(wǎng)絡(luò)字節(jié)序返回。地址類型是一個地址族常量(例如AF_INET)。
可以將協(xié)議名字和協(xié)議號采用以下函數(shù)映射。
#include <netdb.h>struct PRotoent *getprotobyname(const char *name);struct protoent *getprotobynumber(int proto);struct protoent *getprotoent(void);以上所有函數(shù)的返回值:若成功則返回指針,出錯則返回NULLvoid setprotoent(int stayopen);void endprotoent(void);
POSIX.1定義的結(jié)構(gòu)protoent至少包含如下成員:
struct protoent { char *p_name; /* protocol name */ char **p_aliases; /* pointer to alternate protocol name array */ int p_proto; /* protocol number */ ...};
服務(wù)是由地址的端口號部分表示的。每個服務(wù)由一個唯一的、熟知的端口號來提供。采用函數(shù)getservbyname可以將一個服務(wù)名字映射到一個端口號,函數(shù)getservbyport將一個端口號映射到一個服務(wù)名,或者采用函數(shù)getservent順序掃描服務(wù)數(shù)據(jù)庫。
#include <netdb.h>struct servent *getservbyname(const char *name, const char *proto);struct servent *getservbyport(int port, const char *proto);struct servent *getservent(void);以上所有函數(shù)的返回值:若成功則返回指針,出錯則返回NULLvoid setservent(int stayopen);void endservent(void);
結(jié)構(gòu)servent至少包含如下成員:
struct servent { char *s_name; /* service name */ char **s_aliases; /* pointer to alternate service name array */ int s_port; /* port number */ char *s_proto; /* name of protocol */ ...};
POSIX.1定義了若干新的函數(shù),允許應(yīng)用程序?qū)⒁粋€主機(jī)名字和服務(wù)名字映射到一個地址,或者相反。這些函數(shù)代替老的函數(shù)gethostbyname和gethostbyaddr。
函數(shù)getaddrinfo允許將一個主機(jī)名字和服務(wù)名字映射到一個地址。
#include <sys/socket.h>#include <netdb.h>int getaddrinfo(const char *restrict host, const char *restrict service, const struct addrinfo *restrict hint, struct addrinfo **restrict res);返回值:若成功則返回0,出錯則返回非0錯誤碼void freeaddrinfo(struct addrinfo *ai);
需要提供主機(jī)名字、服務(wù)名字,或者兩者都提供。如果僅僅提供一個名字,另外一個必須是個空指針。主機(jī)名字可以是一個節(jié)點(diǎn)名或點(diǎn)分十進(jìn)制記法表示的主機(jī)地址。
函數(shù)getaddrinfo返回一個結(jié)構(gòu)addrinfo的鏈表。可以用freeaddrinfo來釋放一個或多個這種結(jié)構(gòu),這取決于用ai_next字段鏈接起來的結(jié)構(gòu)有多少。
結(jié)構(gòu)addrinfo的定義至少包含如下成員:
struct addrinfo { int ai_flags; /* customize behavior */ int ai_family; /* address family */ int ai_socktype; /* socket type */ int ai_protocol; /* protocol */ socklen_t ai_addrlen; /* length in bytes of address */ struct sockaddr *ai_addr; /* address */ char *ai_canonname; /* canonical(與aliases相對) name of host */ struct addrinfo *ai_next; /* next in list */ ...};
根據(jù)某些規(guī)則,可以提供一個可選的hint來選擇地址。hint是一個用于過濾地址的模板,僅使用ai_family、ai_flags、ai_protocol和ai_socktype字段。剩余的整數(shù)字段必須設(shè)為0,并且指針字段為空。表15-6總結(jié)了在ai_flags中所用的標(biāo)志,這寫標(biāo)志用來指定如何處理地址和名字。
表16-5 addrinfo結(jié)構(gòu)標(biāo)志
如果getaddrinfo失敗,不能使用perror或strerror來生成錯誤消息。替代地,調(diào)用gai_strerror將返回的錯誤碼轉(zhuǎn)換成錯誤消息。
#include <netdb.h>const char *gai_strerror(int error);返回值:指向描述錯誤的字符串的指針
函數(shù)getnameinfo將地址轉(zhuǎn)換成主機(jī)名或者服務(wù)名。
#include <sys/socket.h>#include <netdb.h>int getnameinfo(const struct sockaddr *restrict addr, socklen_t alen, char *restrict host, socklen_t hostlen, char *restrict service, socklen_t servlen, unsigned int flags);返回值:若成功則返回0,出錯則返回非0值
套接字地址(addr)被轉(zhuǎn)換成主機(jī)名或服務(wù)名。如果host非空,它指向一個長度為hostlen字節(jié)的緩沖區(qū)用于存儲返回的主機(jī)名。同樣,如果service非空,它指向一個長度為servlen字節(jié)的緩沖區(qū)用于存儲返回的服務(wù)名。
參數(shù)flags指定一些轉(zhuǎn)換的控制方式,表16-6總結(jié)了系統(tǒng)支持的標(biāo)志。
表16-6 getnameinfo函數(shù)標(biāo)志
實(shí)例
程序清單16-1說明了函數(shù)getaddrinfo的使用方法。
程序清單16-1 打印主機(jī)和服務(wù)信息
#include "apue.h"#include <netdb.h>#include <arpa/inet.h>#if defined(BSD) || defined(MACOS)#include <sys/socket.h>#include <netinet/in.h>#endifvoid print_family(struct addrinfo *aip){ printf(" family "); switch(aip->ai_family) { case AF_INET: printf("inet"); break; case AF_INET6: printf("inet6"); break; case AF_UNIX: printf("unix"); break; case AF_UNSPEC: printf("unspecified"); break; default: printf("unknown"); }}void print_type(struct addrinfo *aip){ printf(" type "); switch(aip->ai_socktype) { case SOCK_STREAM: printf("stream"); break; case SOCK_DGRAM: printf("datagram"); break; case SOCK_SEQPACKET: printf("seqpacket"); break; case SOCK_RAW: printf("raw"); break; default: printf("unknown (%d)", aip->ai_socktype); }}voidprint_protocol(struct addrinfo *aip){ printf(" protocol "); switch(aip->ai_protocol) { case 0: printf("default"); break; case IPPROTO_TCP: printf("TCP"); break; case IPPROTO_UDP: printf("UDP"); break; case IPPROTO_RAW: printf("raw"); break; default: printf("unknown (%d)", aip->ai_protocol); }}voidprint_flags(struct addrinfo *aip){ printf("flags"); if(aip->ai_flags == 0) { printf(" 0"); } else { if(aip->ai_flags & AI_PASSIVE) printf(" passive"); if(aip->ai_flags & AI_CANONNAME) printf(" canon"); if(aip->ai_flags & AI_NUMERICHOST) printf(" numhost");#if defined(AI_NUMERICSERV) if(aip->ai_flags & AI_NUMERICSERV) printf(" numserv");#endif#if defined(AI_V4MAPPED) if(aip->ai_flags & AI_V4MAPPED) printf(" v4mapped");#endif#if defined(AI_ALL) if(aip->ai_flags & AI_ALL) printf(" all");#endif }}intmain(int argc, char *argv[]){ struct addrinfo *ailist, *aip; struct addrinfo hint; struct sockaddr_in *sinp; const char *addr; int err; char abuf[INET_ADDRSTRLEN]; if(argc != 3) err_quit("usage: %s nodename service", argv[0]); hint.ai_flags = AI_CANONNAME; hint.ai_family = 0; hint.ai_socktype = 0; hint.ai_protocol = 0; hint.ai_addrlen = 0; hint.ai_canonname = NULL; hint.ai_addr = NULL; hint.ai_next = NULL; if((err = getaddrinfo(argv[1], argv[2], &hint, &ailist)) != 0) err_quit("getaddrinfo error: %s", gai_strerror(err)); for(aip = ailist; aip != NULL; aip = aip->ai_next) { print_flags(aip); print_family(aip); print_type(aip); print_protocol(aip); printf("/n/thost %s", aip->ai_canonname?aip->ai_canonname:"-"); if(aip->ai_family == AF_INET) { sinp = (struct sockaddr_in *)aip->ai_addr; addr = inet_ntop(AF_INET, &sinp->sin_addr, abuf, INET_ADDRSTRLEN); printf(" address %s", addr?addr:"unknown"); printf(" port %d", ntohs(sinp->sin_port)); } printf("/n"); } exit(0);}
程序在Linux系統(tǒng)上運(yùn)行輸出如下:
4、將套接字與地址綁定
與客戶端的套接字關(guān)聯(lián)的地址沒有太大的意義,可以讓系統(tǒng)選一個默認(rèn)的地址。然而,對于服務(wù)器,需要給一個接收客戶端請求的套接字綁定一個眾所周知的地址。客戶端應(yīng)有一種方法來發(fā)現(xiàn)用以連接服務(wù)器的地址,最簡單的方法就是為服務(wù)器保留一個地址并且在/etc/services或者某個名字服務(wù)(name service)中注冊。
可以用bind函數(shù)將地址綁定到一個套接字。
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t len);返回值:若成功則返回0,出錯則返回-1
對于所能使用的地址有一些限制:
對于因特網(wǎng)域,如果指定IP地址為INADDR_ANY,套接字端點(diǎn)可以被綁定到所有的系統(tǒng)網(wǎng)絡(luò)接口。這意味著可以收到這個系統(tǒng)所安裝的所有網(wǎng)卡的數(shù)據(jù)包。
可以調(diào)用函數(shù)getsockname來發(fā)現(xiàn)綁定到一個套接字的地址。
#include <sys/socket.h>int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);返回值:若成功則返回0,出錯則返回-1
調(diào)用getsockname之前,設(shè)置alenp為一個指向整數(shù)的指針,該整數(shù)指定緩沖區(qū)sockaddr的大小。返回時(shí),該整數(shù)會被設(shè)置成返回地址的大小。如果該地址和提供的緩沖區(qū)長度不匹配,則將其截?cái)喽粓?bào)錯。如果當(dāng)前沒有綁定到該套接字的地址,其結(jié)果沒有定義。
如果套接字已經(jīng)和對方連接,調(diào)用getpeername來找到對方的地址。
#include <sys/socket.h>int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);返回值:若成功則返回0,若出錯則返回-1
除了返回的是對方的地址之外,函數(shù)getpeername和getsockname一樣。
本篇博文內(nèi)容摘自《UNIX環(huán)境高級編程》(第2版),僅作個人學(xué)習(xí)記錄所用。關(guān)于本書可參考:http://www.apuebook.com/。
新聞熱點(diǎn)
疑難解答
圖片精選