在學習用套接字做一些有意義的事情之前,需要知道如何確定一個目標通信進程。
進程的標識有兩個部分:計算機的網絡地址可以幫助標識網絡上想與之通信的計算機,而服務可以幫助標識計算機上特定的進程。
1、字節序
運行在同一臺計算機上的進程相互通信時,一般不用考慮字節的順序(字節序),字節序是一個處理器架構特性,用于指示像整數這樣的大數據類型的內部字節順序。圖16-1顯示一個32位整數內部的字節是如何排序的。
圖16-1 32位整數內部的字節序
如果處理器架構支持大端(big-endian)字節序,那么最大字節地址對應于數字最低有效字節(LSB);小端(little-endian)字節序則相反:數字最低字節對應于最小字節地址。注意,不管字節如何排序,數字最高位總是在左邊,最低位總是在右邊。
網絡協議指定了字節序,因此異構計算機系統能夠交換協議信息而不會混淆字節序。TCP/IP協議棧采用大端字節序。應用程序交換格式化數據時,字節序問題就會出現。對于TCP/IP,地址用網絡字節序表示,所以應用程序有時需要在處理器的字節序與網絡字節序之間的轉換。
對于TCP/IP應用程序,提供了四個通用函數以實施在處理器字節序和網絡字節序之間的轉換。
#include <arpa/inet.h>uint32_t htonl(uint32_t hostint32);返回值:以網絡字節序表示的32位整型數uint16_t htons(uint16_t hostint16);返回值:以網絡字節序表示的16位整型數uint32_t ntohl(uint32_t netint32);返回值:以主機字節序表示的32位整型數uint16_t ntohs(uint16_t netint16);返回值:以主機字節序表示的16位整型數
h表示“主機(host)”字節序,
n表示“網絡(network)”字節序。
l表示“長(long)”整數(即4個字節),
s表示“短(short)”整數(即2個字節)。
這四個函數定義在<arpa/inet.h>中,也有比較老的系統將其定義在<netinet/in.h>中。
2、地址格式
地址標識了特定通信域中的套接字端點,地址格式與特定的通信域相關。為使不同格式地址能夠被傳入到套接字函數,地址被強制轉換成通用的地址結構sockaddr表示:
struct sockaddr { sa_family_t sa_family; /* address family */ char sa_data[]; /* variable-length address */ ...... };
套接字實現可以自由地添加額外的成員并且定義sa_data成員的大小。例如在linux中,該結構定義如下:
struct sockaddr { sa_family_t sa_family; /* address family */ char sa_data[14]; /* variable-length address */};
因特網地址定義在<netinet/in.h>中。在IPv4因特網域(AF_INET)中,套接字地址用如下結構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 */};
數據類型in_port_t定義為uint16_t。數據類型in_addr_t定義成uint32_t。這些整數類型在<stdint.h>中定義并指定了相應的位數。與IPv4因特網域(AF_INET)相比較,IPv6因特網域(AF_INET6)套接字地址用如下結構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必須的定義,每個實現可以自由地添加額外的字段。例如,在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相差比較大,它們均被強制轉換成sockaddr結構傳入到套接字例程中。
有時,需要打印出能被人而不是計算機所理解的地址格式。BSD網絡軟件中包含了函數inet_addr和inet_ntoa,用于在二進制地址格式與點分十進制字符串表示(a.b.c.d)之間相互轉換。這些函數僅用于IPv4地址,但功能相似的兩個函數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
函數inet_ntop將網絡字節序的二進制地址轉換成文本字符串格式,inet_pton將文本字符串格式轉換成網絡字節序的二進制地址。參數domain僅支持兩個值:AF_INET和AF_INET6。
對于inet_ntop,參數size指定了用以保存文本字符串的緩沖區(str)的大小。兩個常數用于簡化工作:INET_ADDRSTRLEN定義了足夠大的空間來存放表示IPv4地址的文本字符串,INET6_ADDRSTRLEN定義了足夠大的空間來存放表示IPv6地址的文本字符串。
對于inet_pton,如果domain是AF_INET,緩沖區addr需要有足夠大的空間來存放32位地址,如果domain是AF_INET6則需要足夠大的空間來存放128位地址。
3、地址查詢
理想情況下,應用程序不需要了解套接字地址的內部結構。如果應用程序只是簡單地傳遞類似于sockaddr結構的套接字地址,并且不依賴于任何協議相關的特性,那么可以與提供相同服務的許多不同協議協作。
歷史上,BSD網絡軟件提供接口訪問各種網絡配置信息。http://www.CUOXin.com/nufangrensheng/p/3507496.html中,簡要地討論了網絡數據文件和用來訪問這種信息的函數。在本節,將更加詳細地討論一些細節,并且引入新的函數來查詢尋址信息。
這些函數返回的網絡配置信息可能存放在許多地方。它們可以保存在靜態文件中(如/etc/hosts,/etc/services等),或者可以由命名服務管理,例如DNS(Domain Name System)或者NIS(Network Information Service)。無論這些信息放在何處,這些函數同樣能夠訪問它們。
通過調用gethostent,可以找到給定計算機的主機信息。
#include <netdb.h>struct hostent *gethostent(void);返回值:若成功則返回指針,若出錯則返回NULLvoid sethostent(int stayopen);void endhostent(void);
如果主機數據文件沒有打開,gethostent會打開它。函數gethostent返回文件的下一個條目。函數sethostent會打開文件,如果文件已經被打開,那么將其回繞。函數endhostent將關閉文件。
當gethostent返回時,得到一個指向hostent結構的指針,該結構可能包含一個靜態的數據緩沖區。每次調用gethostent將會覆蓋這個緩沖區。數據結構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 */ ...};
返回的地址采用網絡字節序。
兩個附加的函數gethostbyname和gethostbyaddr,原來包含在hostent函數里面,現在被認為是過時的,馬上將會看到其替代函數。
能夠采用一套相似的接口來獲得網絡名字和網絡號。
#include <netdb.h>struct netent *getnetbyaddr(uint32_t net, int type);struct netent *getnetbyname(const char *name);struct netent *getnetent(void);以上三個函數的返回值:若成功則返回指針,若出錯則返回NULLvoid setnetent(int stayopen);void endnetent(void);
結構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 */ ...};
網絡號按照網絡字節序返回。地址類型是一個地址族常量(例如AF_INET)。
可以將協議名字和協議號采用以下函數映射。
#include <netdb.h>struct PRotoent *getprotobyname(const char *name);struct protoent *getprotobynumber(int proto);struct protoent *getprotoent(void);以上所有函數的返回值:若成功則返回指針,出錯則返回NULLvoid setprotoent(int stayopen);void endprotoent(void);
POSIX.1定義的結構protoent至少包含如下成員:
struct protoent { char *p_name; /* protocol name */ char **p_aliases; /* pointer to alternate protocol name array */ int p_proto; /* protocol number */ ...};
服務是由地址的端口號部分表示的。每個服務由一個唯一的、熟知的端口號來提供。采用函數getservbyname可以將一個服務名字映射到一個端口號,函數getservbyport將一個端口號映射到一個服務名,或者采用函數getservent順序掃描服務數據庫。
#include <netdb.h>struct servent *getservbyname(const char *name, const char *proto);struct servent *getservbyport(int port, const char *proto);struct servent *getservent(void);以上所有函數的返回值:若成功則返回指針,出錯則返回NULLvoid setservent(int stayopen);void endservent(void);
結構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定義了若干新的函數,允許應用程序將一個主機名字和服務名字映射到一個地址,或者相反。這些函數代替老的函數gethostbyname和gethostbyaddr。
函數getaddrinfo允許將一個主機名字和服務名字映射到一個地址。
#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);
需要提供主機名字、服務名字,或者兩者都提供。如果僅僅提供一個名字,另外一個必須是個空指針。主機名字可以是一個節點名或點分十進制記法表示的主機地址。
函數getaddrinfo返回一個結構addrinfo的鏈表??梢杂胒reeaddrinfo來釋放一個或多個這種結構,這取決于用ai_next字段鏈接起來的結構有多少。
結構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 */ ...};
根據某些規則,可以提供一個可選的hint來選擇地址。hint是一個用于過濾地址的模板,僅使用ai_family、ai_flags、ai_protocol和ai_socktype字段。剩余的整數字段必須設為0,并且指針字段為空。表15-6總結了在ai_flags中所用的標志,這寫標志用來指定如何處理地址和名字。
表16-5 addrinfo結構標志
如果getaddrinfo失敗,不能使用perror或strerror來生成錯誤消息。替代地,調用gai_strerror將返回的錯誤碼轉換成錯誤消息。
#include <netdb.h>const char *gai_strerror(int error);返回值:指向描述錯誤的字符串的指針
函數getnameinfo將地址轉換成主機名或者服務名。
#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)被轉換成主機名或服務名。如果host非空,它指向一個長度為hostlen字節的緩沖區用于存儲返回的主機名。同樣,如果service非空,它指向一個長度為servlen字節的緩沖區用于存儲返回的服務名。
參數flags指定一些轉換的控制方式,表16-6總結了系統支持的標志。
表16-6 getnameinfo函數標志
實例
程序清單16-1說明了函數getaddrinfo的使用方法。
程序清單16-1 打印主機和服務信息
#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系統上運行輸出如下:
4、將套接字與地址綁定
與客戶端的套接字關聯的地址沒有太大的意義,可以讓系統選一個默認的地址。然而,對于服務器,需要給一個接收客戶端請求的套接字綁定一個眾所周知的地址。客戶端應有一種方法來發現用以連接服務器的地址,最簡單的方法就是為服務器保留一個地址并且在/etc/services或者某個名字服務(name service)中注冊。
可以用bind函數將地址綁定到一個套接字。
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t len);返回值:若成功則返回0,出錯則返回-1
對于所能使用的地址有一些限制:
對于因特網域,如果指定IP地址為INADDR_ANY,套接字端點可以被綁定到所有的系統網絡接口。這意味著可以收到這個系統所安裝的所有網卡的數據包。
可以調用函數getsockname來發現綁定到一個套接字的地址。
#include <sys/socket.h>int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);返回值:若成功則返回0,出錯則返回-1
調用getsockname之前,設置alenp為一個指向整數的指針,該整數指定緩沖區sockaddr的大小。返回時,該整數會被設置成返回地址的大小。如果該地址和提供的緩沖區長度不匹配,則將其截斷而不報錯。如果當前沒有綁定到該套接字的地址,其結果沒有定義。
如果套接字已經和對方連接,調用getpeername來找到對方的地址。
#include <sys/socket.h>int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);返回值:若成功則返回0,若出錯則返回-1
除了返回的是對方的地址之外,函數getpeername和getsockname一樣。
本篇博文內容摘自《UNIX環境高級編程》(第2版),僅作個人學習記錄所用。關于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答