Unix/Linux编程:Internet domain socket
文章目錄
- Internet domain socket
- 網絡字節序
- 數據表示
- 套接字地址結構
- IPv4 socket 地址:struct sockaddr_in
- IPv6 socket 地址:struct sockaddr_in6
- sockaddr_storage 結構(新的通用套接字)
- sockaddr結構(老的通用套接字、很少用)
- 主機和服務轉換函數
- 概述
- 準備
- 域名系統
- /etc/services 文件
- inet_pton()和 inet_ntop()函數
- 獨立于協議的主機和服務轉換
- 過時的主機和服務轉換 API
- inet_aton()和 inet_ntoa()函數
- gethostbyname()和 gethostbyaddr()函數
- 理論
- 錯誤h_errno
- 實踐
- gethostbyname
- gethostbyaddr
- getserverbyname()和 getserverbyport()函數
- 理論
- 實踐
- getservbyname
- 值-結果參數
Internet domain socket
Internet domain 流socket是基于TCP之上的,它們提供了可靠的雙向字節流通信信道。
Internet domain數據報socket是預計UDP之上的。 UDP socket與之在Unix domin中的對應實體類似,但要注意如下差別
- Unix domin數據報socket是可靠的,而UDP socket是不可靠的
- 在一個Unix domain數據報socket上發送數據會在接受socket的數據隊列為滿時阻塞。與之不同的是,使用UDP時如果進入的數據報會使接受者的隊列溢出,那么數據報被靜默丟棄
網絡字節序
IP地址和端口號是整數值。在將這些值在網絡中傳遞時碰到的一個問題是不同的硬件結構會以不同的順序來存儲一個多字節整數的字節。存儲整數時先存儲(即在最小內存地址處)最高有效位的被稱為大端,那些先存儲最低有效位的被稱為小端。一些硬件結構可以在這兩種格式之間切換。在特定主機上使用的字節序被稱為主機字節序
由于端口號和IP地址必須在網絡中的所有字節之間傳遞并且需要被它們所理解,因此必須要使用一個標準的字節序,這種字節序被稱為網絡字節序(它是大端的)
有時候可能會直接使用 IP 地址和端口號的整數常量形式,如可能會選擇將端口號硬編碼進程序中,或者將端口號作為一個命令行參數傳遞給程序,或者在指定一個 IPv4 地址時使用諸如INADDR_ANY 和 INADDR_LOOPBACK 之類的常量。這些值在 C 中是按照主機的規則來表示的,因此它們是主機字節序的,在將它們存儲進socket 地址結構中之前需要將這些值轉換成網絡字節序。
htons()、htonl()、ntohs()以及 ntohl()函數被定義(通常為宏)用來在主機和網絡字節序之間轉換整數。
#include <netinet/in.h>// 返回一個32位的網絡字節序,h表示主機uint32_t htonl (uint32_t __hostlong)// 返回一個16位的網絡字節序,h表示主機uint16_t htons (uint16_t __hostshort)// 返回一個32位的主機字節序,n表示網絡 uint32_t ntohl (uint32_t __netlong) // 返回一個16位的主機字節序,n表示網絡 uint16_t ntohs (uint16_t __netshort)總結
網絡中用的是字節而不是字符串,我們常說的字節序指的是二進制值
主機有不同的存儲字節方式(主機字節序):大端模式和小端模式;在不同的主機通信時,為了避免轉換和錯誤,我們引入了網絡字節序。這樣我們就只需要關注主機字節序和網絡字節序的轉換了.
其他用于處理字節的函數:
數據表示
在編寫網絡程序時需要清除不同的計算機架構中會使用不同的規則表示各種數據類型。比如整數類型可以以大端或小端的形式存儲。此外,還存在其他的差別,如 C long 數據類型在一些系統中可能是 32 位的,但在其他系統上可能是 64 位的。當考慮結構中,問題就更加復雜了,因為不同的實現采用了不同的規則來將一個結構中的字段對齊到主機系統的地址邊界,從而使得字段之間的填充字節數量是不同的。
由于在數據表現上存在這些差異,因此在網絡的異構系統之間交換數據的應用程序必須要采用一些公共編碼來編碼數據。發送者必須要根據這些規則來對數據進行編碼,而接收者則必須要遵循同樣的規則對數據進行解碼。將數據變成一個標準格式以便在網絡上傳輸的過程被稱為信號編集
然而,一種比信號編集更簡單的方法通常會被采用:將所有傳輸的數據編碼成文本形式,其中數據項之間使用特定的字符來分隔開,這個特定的字符通常是換行符。
如果將在一個流 socket 上傳輸的數據編碼成使用換行符分隔的文本,那么定義一個諸如readLine()之類的函數將是比較便捷的。
readLine()函數從文件描述符參數 fd 引用的文件中讀取字節直到碰到換行符為止。輸入字節序列將會返回在 buffer 指向的位置處,其中 buffer 指向的內存區域至少為 n 字節。返回的字符串總是以 null 結尾,因此實際上至多有(n–1)個字節會返回。在成功時,readLine()會返回放入 buffer 的數據的字節數,結尾的 null 字節不會計算在內。
ssize_t readLine(int fd, void *buffer, size_t n) {ssize_t numRead; /* # of bytes fetched by last read() */size_t totRead; /* Total bytes read so far */char *buf;char ch;if (n <= 0 || buffer == NULL) {errno = EINVAL;return -1;}buf = buffer; /* No pointer arithmetic on "void *" */totRead = 0;for (;;) {numRead = read(fd, &ch, 1);if (numRead == -1) {if (errno == EINTR) /* Interrupted --> restart read() */continue;elsereturn -1; /* Some other error */} else if (numRead == 0) { /* EOF */if (totRead == 0) /* No bytes read; return 0 */return 0;else /* Some bytes read; add '\0' */break;} else { /* 'numRead' must be 1 if we get here */if (totRead < n - 1) { /* Discard > (n - 1) bytes */totRead++;*buf++ = ch;}if (ch == '\n')break;}}*buf = '\0';return totRead; }如果在遇到換行符之前讀取的字節數大于或等于(n–1),那么 readLine()函數會丟棄多余的字節(包括換行符)。如果在前面的(n–1)字節中讀取了換行符,那么在返回的字符串中就會包含這個換行符。(因此可以通過檢查在返回的 buffer 中結尾 null 字節前是否是一個換行符來確定是否有字節被丟棄了。)
套接字地址結構
Internet domain socket地址由兩種:IPv4和IPv6
IPv4 socket 地址:struct sockaddr_in
一個 IPv4 socket 地址會被存儲在一個 sockaddr_in 結構中,該結構在<netinet/in.h>中進行定義,具體如下:
#include <netinet/in.h>typedef uint16_t in_port_t;typedef uint32_t in_addr_t;struct in_addr /* IPv4 4-byte address */{ in_addr_t s_addr; /* unsigned 32-bit integer */};struct sockaddr_in /* IPv4 socket address */{sa_family sin_family; /* Address family(AF_INET) */in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. */unsigned char sin_zero[x]; /* Pad to size of `struct sockaddr'. */};- sin_family 字段用來標識socket domain,其值總是為AF_INET
- sin_port 和 sin_addr 字段是端口號和 IP 地址,它們都是網絡字節序的
- in_port_t 和in_addr_t t 數據類型是無符號整型,其長度分別為 16 位和 32 位。
- sin_sero在綁定一個非通配的IPv4地址時,必須為0
- 套接字地址結構僅在給定主機上使用:雖然結構中的IP地址、端口號等可以用在不同主機之間的通信中,但是結構本身并不在主機之間傳遞
IPv6 socket 地址:struct sockaddr_in6
與 IPv4 地址一樣,一個 IPv6 socket 地址包含一個 IP 地址和一個端口號,它們之間的差別在于 IPv6 地址是 128 位而不是 32 位的。一個 IPv6 socket 地址會被存儲在一個 sockaddr_in6結構中,該結構在<netinet/in.h>中進行定義,具體如下。
struct in6_addr // IPv6 address structure{union{uint8_t __u6_addr8[16]; // 16 bytes == 128bits #if defined __USE_MISC || defined __USE_GNUuint16_t __u6_addr16[8]; uint32_t __u6_addr32[4]; #endif} __in6_u;};struct sockaddr_in6 /* IPv6 socket address */{sa_family sin_family; /* Address family(AF_INET) */in_port_t sin6_port; /* Port number. */uint32_t sin6_flowinfo; /* IPv6 flow information */struct in6_addr sin6_addr; /* IPv6 address */uint32_t sin6_scope_id; /* IPv6 scope-id */};- sockaddr_in6 結構中的所有字段都是以網絡字節序存儲的
- sin_family 字段會被設置成 AF_INET6
- sin6_port 和 sin6_addr 字段分別是端口號和 IP地址。
IPv6 和 IPv4 一樣也有通配和回環地址,但它們的用法要更加復雜一些,因為 IPv6 地址是存儲在數組中的(并沒有使用標量類型),下面將會使用 IPv6 通配地址(0::0)來說明這一點。系統定義了常量 IN6ADDR_ANY_INIT 來表示這個地址,具體如下
#define IN6ADDR_ANY_INIT { { { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 } } }在變量聲明的初始化器中可以使用IN6ADDR_ANY_INIT ,但無法在一個賦值語句中的右邊使用這個常量。因為C語法不允許在賦值語句中使用一個結構化的常量。取而代之的做法是必須要使用一個預先定義的變量in6addr_any,C庫會按照下面的方式對該變量進行初始化:
const struct in6_addr in6addr_any = IN6ADDR_ANY_INIT;因此可以向如下這樣使用通配地址來初始化一個IPv6 socket地址:
struct sockaddr_in6 addr;memset(&addr, 0, sizeof(struct sockaddr_in6));addr.sin6_family = AF_INET6;addr.sin6_addr = in6addr_any;addr.sin6_port = htons(SOME_PORT_NUM);IPv6 環回地址(::1)的對應常量和變量是 IN6ADDR_LOOPBACK_INIT 和 in6addr _loopback
如果 IPv4 和 IPv6 共存于一臺主機上,那么它們將共享同一個端口號空間。這意味著如果一個應用程序將一個 IPv6 socket 綁定到了 TCP 端口 2000 上(使用 IPv6 通配地址),那么 IPv4 TCP socket 將無法綁定到同一個端口上。(TCP/IP 實現確保位于其他主機上的socket 能夠與這個 socket 進行通信,不管那些主機運行的是 IPv4 還是 IPv6。
sockaddr_storage 結構(新的通用套接字)
在IPv6 socket API中新引入了一個通用的sockaddr_storage結構,這個結構的空間足以存儲任意類型的socket地址(即可以將任意類型的socket地址結構強制轉換并存儲在這個結構中)。特別地,這個結構允許透明地存儲 IPv4 或 IPv6 socket 地址,從而刪除了代碼中的 IP 版本依賴性。sockaddr_storage 結構在 Linux 上的定義如下所示。
struct sockaddr_storage{__SOCKADDR_COMMON (ss_); /* Address family, etc. */char __ss_padding[_SS_PADSIZE];__ss_aligntype __ss_align; /* Force desired alignment. */};sockaddr結構(老的通用套接字、很少用)
#include <sys/socket.h> struct sockaddr{__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */char sa_data[14]; /* Address data. */};從應用開發的角度來看,這個結構的唯一用途就是對指向特定于協議的套接字地址結構指針指向類型轉換
用于將主機名(如 www.kernel.org)和服務名(如 http)轉換成對應的數字形式的函數。這些函數一般會返回用網絡字節序表示的整數,并且可以直接將這些整數復制進一個 socket 地址結構的相關字段中。
主機和服務轉換函數
概述
計算機以二進制形式來表示IP地址和端口號,但是名字比數字更容易記憶,而且可以在名字不變的情況下隨意改變底層的數字值
主機名是連接在網絡上的一個系統的符號標識符。服務名是端口號的符號標識。
主機地址和端口的標識有如下兩種方法:
- 主機地址可以標識為一個二進制值或者一個符號主機名展現格式(IPv4 是點分十進制,IPv6 是十六進制字符串)
- 端口號可以表示為一個二進制值或一個符號服務名
在二進制和人類可讀的形式之間轉換IPv4的地址(已過時)
inet_aton、inet_addr、inet_ntoa函數將一個 IPv4 地址在點分十進制表示形式(127.0.0.1)和32位的網絡字節序二進制值之間轉換表示形式之間進行轉換,僅適用于IPv4地址。現在它們已經被廢棄了。請避免在程序中使用它們
在二進制和人類可讀的形式之間轉換 IPv4 和 IPv6 地址
inet_pton()和 inet_ntop()函數將二進制 IPv4 和 IPv6 地址轉換成展現格式—即以點分十進制表示或十六進制字符串表示,或將展現格式轉換成二進制 IPv4 和 IPv6 地址。
由于人類對名字的處理能力要比對數字的處理能力強,因此通常偶爾才會在程序中使用這些函數。inet_ntop()的一個用途是產生IP地址的一個可打印的表示形式以便記錄日志。在有些情況下,最好使用這個函數而不是將一個IP地址解析成主機名,其原因如下:
- 將一個IP地址解析成主機名可能需要向一臺服務器發送一個耗時比較長的請求
- 在一些場景中,可能并不存在一個 DNS(PTR)記錄將 IP 地址映射到對應的主機名上
主機和服務名與二進制形式之間的轉換(已過時)
gethostbyname()函數返回與主機名對應的二進制 IP 地址,getservbyname()函數返回與服務名對應的端口號。對應的逆向轉換是由 gethostbyaddr()和 getservbyport()來完成的。現在它們已經過時了,新代碼應該使用 getaddrinfo()和getnameinfo()函數來完成此類轉換
主機和服務名與二進制形式之間的轉換(現代的)
getaddrinfo()函數是 gethostbyname()和 getservbyname()兩個函數的現代繼任者。給定一個主機名和一個服務名,getaddrinfo()會返回一組包含對應的二進制IP地址和端口號的結構。與gethostbyname()不同,getaddrinfo()會透明地處理 IPv4 和 IPv6 地址。因此使用這個函數可以編寫不依賴于 IP 版本的程序。所有新代碼都應該使用 getaddrinfo()來將主機名和服務名轉換成二進制表示。
getnameinfo()函數執行逆向轉換,即將一個 IP 地址和端口號轉換成對應的主機名和服務名。
使用 getaddrinfo()和 getnameinfo()還可以在二進制 IP 地址與其展現格式之間進行轉換。
準備
域名系統
在DNS出現之前,主機名和IP地址之間的映射關系是在一個手工維護的本地文件/ect/hosts中定義的,該文件包含了形如下面的記錄。
gethostbyname()函數(被 getaddrinfo()取代的函數)通過搜索這個文件并找出與規范主機名(即主機的官方或主要名稱)或其中一個別名(可選的,以空格分隔)匹配的記錄來獲取一個IP 地址。
然而,/etc/hosts 模式的擴展性交叉,并且隨著網絡中主機數量的增長(如因特網中存在著數以億計的主機),這種方式已經變得不太可行了。
DNS被設計用來解決這個問題。
當一個程序調用 getaddrinfo()來解析(即獲取 IP 地址)一個域名時,getaddrinfo()會使用一組庫函數(resolver庫)來與本地的DNS服務器通信。如果這個服務器無法提供所需的信息,那么它就會與位于層級中的其他DNS服務器進行通信以便獲取信息。有時候,這個解析過程可能會花費很多時間,DNS服務器采用了緩存技術來避免在查詢常見域名時所發送的不必要的通信
- DSN解析請求可以分為兩類:遞歸和等待,在一個遞歸請求中,請求者要求服務器處理整個解析任務,包括在必要的時候與其他DNS服務器進行通信。當位于本地主機上的一個應用程序調用getaddrinfo()時,該函數會向本地DNS服務器發起一個遞歸請求。如果本地DNS服務器自己并沒有相關信息來進行解析,那么它就會迭代地解析這個域名。
- 如果向 gethostbyname()傳遞了一個不完整的域名,那么解析器在解析之前會嘗試補全。域名補全的規則是在/etc/resolv.conf 中定義的(參見 resolv.conf(5)手冊)。在默認情況下,解析器至少會使用本機的域名來補全。例如,如果登錄機器 oghma.otago.ac.nz 并輸入了命令 ssh octavo,得到的 DNS 查詢將會以 octavo.otago.ac.nz 作為其名字。
/etc/services 文件
眾所周知的端口號是由 IANA 集中注冊的,其中每個端口都有一個對應的服務名。由于服務器是集中管理并且不會像IP地址那樣頻繁編號,因此沒有必要采用DNS服務來管理它們。相反,端口號的服務名會記錄在/ect/serivces中。getaddrinfo()和getnameinfo()函數會使用這個文件中的信息在服務名和端口號之間進行轉換
inet_pton()和 inet_ntop()函數
inet_pton、inet_ntop函數用于:在 IPv4 和 IPv6 地址的網絡字節序二進制形式和點分十進制表示法或十六進制字符串表示法之間進行轉換
/* * 參數: family: AF_INET或者AF_INET6。否則函數返回一個錯誤,并將error置EAFNOSUPPORT * strptr:點分十進制指針。如果指向的字符串不是有效的表達式函數返回一個0 * addrptr: 該指針指向的空間存放轉換出來的二進制結果。 * 返回值:成功1,如果輸入不是有效的表達式則0,出錯返回-1 */ int inet_pton (int family, const char *__restrict strptr,void *__restrict addrptr)/* * 參數: family: AF_INET或者AF_INET6。否則函數返回一個錯誤,并將error置EAFNOSUPPORT * strptr: 指向函數的結果。必須不為空(為目標存儲單位分配內存并指定其大小) * __len: 目標出錯單元的大小(包括結尾的空字符),以免溢出。與之有關的宏定義 * #define INET_ADDRSTRLEN 16#define INET6_ADDRSTRLEN 46如果__len太小,該函數返回一個控制在,并將error置ENOSPC * * 返回值:成功返回指向結果的指針,出錯為null */ const char *inet_ntop (int family, const void *__restrict addrptr,char *__restrict strptr, socklen_t __len)p表示presentation(展現)表達式[ASCI字符串],n表示number數值[二進制值]
展現形式是人類可讀的字符串,如:
- 204.152.189.116(IPv4 點分十進制地址);
- ::1(IPv6 冒號分隔的十六進制地址);
- ::FFFF:204.152.189.116(IPv4 映射的 IPv6 地址)。
inet_pton()函數將 src_str 中包含的展現字符串轉換成網絡字節序的二進制 IP 地址。domain 參數應該被指定為 AF_INET 或 AF_INET6。轉換得到的地址會被放在 addrptr 指向的結構中,它應該根據在 domain 參數中指定的值指向一個 in_addr 或 in6_addr 結構。
inet_ntop()函數執行逆向轉換。同樣,domain 應該被指定為 AF_INET 或 AF_INET6,addrptr 應該指向一個待轉換的 in_addr 或 in6_addr 結構。得到的以 null 結尾的字符串會被放置在 dst_str 指向的緩沖器中。len 參數必須被指定為這個緩沖器的大小。inet_ntop()在成功時會返回 dst_str。如果 len 的值太小了,那么 inet_ntop()會返回 NULL 并將 errno設置成 ENOSPC。
要正確計算 dst_str 指向的緩沖器的大小可以使用在<netinet/in.h>中定義的兩個常量。這些常量標識出了 IPv4 和 IPv6 地址的展現字符串的最大長度(包括結尾的 null 字節)。
#define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 46使用示例:
char str_v4[INET_ADDRSTRLEN];struct sockaddr_in addr;inet_ntop(AF_INET, &addr.sin_addr, str_v4, sizeof(str_v4));char str_v6[INET6_ADDRSTRLEN];struct sockaddr_in6 addr6;inet_ntop(AF_INET6, &addr6.sin6_addr, str_v6, sizeof(str_v6)); #include <stdlib.h> #include <stdio.h> #include <getopt.h> #include <zconf.h> #include <sys/socket.h> #include <rpc/types.h> #include <arpa/inet.h> #include <memory.h> #define DEST_IP "127.0.0.1" int main(int argc, char *argv[]) {int sockfd, result;socklen_t len;struct sockaddr_in servaddr;struct sockaddr_in sa;len = sizeof(sa);sockfd = socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(struct sockaddr_in));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(8000);inet_pton(AF_INET, DEST_IP, &servaddr.sin_addr);result = bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr));if(result < 0){perror("bind");close(sockfd);exit(1);}bzero(&sa, sizeof(struct sockaddr_in));if (getsockname(sockfd, (struct sockaddr *)&sa, &len) == -1) {perror("getsockname error");exit(0);}if(sa.sin_family == AF_INET){char str_v4[INET_ADDRSTRLEN];inet_ntop(AF_INET, &sa.sin_addr, str_v4, sizeof(str_v4));printf("s_addr = %u ==> %s, sin_port = %d ==> %hu \n",sa.sin_addr.s_addr, str_v4, sa.sin_port, ntohs(sa.sin_port));// s_addr = 16777343 ==> 127.0.0.1, sin_port = 16415 ==> 8000 }else{char str_v6[INET6_ADDRSTRLEN];inet_ntop(AF_INET6, &sa.sin_addr, str_v6, sizeof(str_v6));printf("s_addr = %u ==> %s, sin_port = %d ==> %hu \n",sa.sin_addr.s_addr, str_v6, sa.sin_port, ntohs(sa.sin_port));}close(sockfd);exit(0); }自己實現inet_pton、inet_ntop的IPv4
#include <stdlib.h> #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include<arpa/inet.h> #include <errno.h>int Inet_pton(int family, const char*strptr, void *addrptr){if(family == AF_INET){struct in_addr inAddr;if (inet_aton(strptr, &inAddr)){memcpy(addrptr, &inAddr, sizeof(inAddr));return 1;}return 0;}return -1; }const char * Inet_ntop(int family, const void * addrptr, char *strptr, size_t len){const u_char * p = (const u_char*)addrptr;if (family == AF_INET){char temp[INET_ADDRSTRLEN];snprintf(temp, sizeof(temp), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);if (strlen(temp) >= len){errno = ENOSPC;return NULL;}strcpy(strptr, temp);return strptr;}return NULL; } int main(int argc, char **argv){uint32_t inAddr;const char *p = "127.0.0.1";Inet_pton(AF_INET, p, &inAddr);printf("Inet_pton(%s) = (%u)\n", p, inAddr); // 16777343char strptr[INET_ADDRSTRLEN];const char * n = Inet_ntop(AF_INET, &inAddr, strptr, sizeof(strptr));printf("Inet_ntop(%u) = (%s, %s)", inAddr, strptr, n); //127.0.0.1, 127.0.0.1 }獨立于協議的主機和服務轉換
getaddrinfo()函數將主機和服務名轉換成 IP 地址和端口號,它作為過時的 gethostbyname()和 getservbyname()函數的(可重入的)接替者被定義在了 POSIX.1g 中。
getnameinfo()函數是 getaddrinfo()的逆函數,它將一個 socket 地址結構(IPv4 或 IPv6)轉換成包含對應主機和服務名的字符串。這個函數是過時的 gethostbyaddr()和 getservbyport()函數的(可重入的)等價物。
具體請參考這里
過時的主機和服務轉換 API
inet_aton()和 inet_ntoa()函數
inet_aton()和 inet_ntoa()函數將一個 IPv4 地址在點分十進制標記法和二進制形式(以網絡字節序)之間進行轉換。這些函數現在已經被 inet_pton()和 inet_ntop()所取代了。
/* * 功能:將__cp所指的C字符串轉換成一個32位的網絡字節序二進制值,并使用指針__inp來存儲 * 如果__cp指針為空,該函數仍然對輸入的字符串指向有效性檢查,但是不存儲任何結果 * 返回值:成功1,失敗0 */ int inet_aton (const char *__cp, struct in_addr *__inp) /* *功能:將32位的網絡字節二進制轉為點位十進制。 * 這個函數是不可重入的 */ char * inet_ntoa (struct in_addr __in)/* * 這個函數已經被廢棄 * 功能:將__cp所指的C字符串轉換成一個32位的網絡字節序二進制值,并使用指針__inp來存儲 * 返回值:返回32位的網絡字節序二進制值。失敗則返回INADDR_NONE(是一個32位均為1的值)。但是Ipv4的廣播地址位255.255.255.255. */ in_addr_t inet_addr (const char *__cp)使用示例:
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <stdio.h> int main(int aargc, char* argv[]) {struct in_addr addr1,addr2;const char *p1 = "192.168.0.74", *p2 = "211.100.21.179";ulong l1,l2;l1= inet_addr(p1), l2 = inet_addr(p2);printf("inet_addr函數已經被廢棄 inet_addr(%s) = %u , inet_addr(%s) = %u\n", p1, inet_addr(p1), p2, inet_addr(p2));memcpy(&addr1, &l1, 4); memcpy(&addr2, &l2, 4);printf("memcpy(%lu) = %u , memcpy(%lu) = %ld\n",l1, addr1.s_addr, l2, addr2);printf("inet_ntoa(%u) = %s , inet_ntoa(%u) = %s\n", addr1.s_addr, inet_ntoa(addr1), addr2.s_addr, inet_ntoa(addr2));return 0; }gethostbyname()和 gethostbyaddr()函數
gethostbyname()和 gethostbyaddr()函數允許在主機名和 IP 地址之間進行轉換。現在這些函數已經被 getaddrinfo()和 getnameinfo()所取代了。
理論
#include <netdb.h> struct hostent {char *h_name; /* 主機名 */char **h_aliases; /*主機別名列表,可能有多個 */int h_addrtype; /* 地址類型 */int h_length; /* 地址長度 */char **h_addr_list; /* 根據網絡字節序列出的主機IP地址列表 */ #endif }; /* * 功能: 根據主機名獲取主機的完整信息 * 參數: __name --- 目標主機的主機名 * 返回值:非空指針——成功,空指針——出錯,同時設置h_errno * 說明: gethostbyname 先到本地的/etc/hosts配置文件中查找主機,如果沒有直到,再去訪問DNS服務器 * 注意: 執行對A記錄的查詢。所以它只能返回IPv4的地址 */ struct hostent *gethostbyname (const char *__name)/* * 功能: 根據IP地址獲取主機的完整信息 * 參數: addr --- 目標主機的IP地址 * len -- 直到addr的長度 * family -- 指定IP地址的類型,包括AF_INET、AF_INET6 * 返回值:非空指針——成功,空指針——出錯,同時設置h_errno */ struct hostent * gethostbyaddr(const char *addr, size_t len , int family);1、返回值:
從該結構體可以看出,不只返回 IP 地址,還會附帶其他信息,各位讀者只需關注最后一個成員 * h_addr_list。下面是對各成員的說明:
- h_name:官方域名/主機名(Official domain name)。官方域名代表某一主頁,但實際上一些著名公司的域名并未用官方域名注冊。
- h_aliases:別名,可以通過多個域名訪問同一主機。同一 IP 地址可以綁定多個域名,因此除了當前域名還可以指定其他域名。
- h_length:IP地址長度。IPv4 的長度為 4 個字節,IPv6 的長度為 16 個字節。
- h_addr_list【按照網絡字節序給出的主機IP地址族】:這是最重要的成員。通過該成員以整數形式保存域名對應的 IP 地址。對于用戶較多的服務器,可能會分配多個 IP 地址給同一域名,利用多個服務器進行均衡負載。
2、出錯: 當發生錯誤時,它不設置errno變量,設置全局整數變量h_errno
錯誤h_errno
在發生錯誤時(如無法解析一個名字),gethostbyname()和 gethostbyaddr()都會返回一個 NULL指針并設置全局變量 h_errno。
正如其名字所表達的那樣,這個變量與 errno 類似(gethostbyname(3)手冊描述了這個變量的可取值),herror()和 hstrerror()函數類似于 perror()和 strerror()。herror()函數(在標準錯誤上)顯示了在 str 中給出的字符串,后面跟著一個冒號(😃,然后再顯示一條與當前位于 h_errno 中的錯誤對應的消息。或者可以使用 hstrerror()獲取一個指向與在 err 中指定的錯誤值對應的字符串的指針。
void herror(const char *s);const char *hstrerror(int err);實踐
gethostbyname
獲取當前主機的名字
#include <stdio.h> #include <zconf.h>#define HOSTNAME_MAX 1024 int main(int argc, char **argv){char buf[HOSTNAME_MAX + 1];if(gethostname(buf, HOSTNAME_MAX) == -1){printf("Cannot get machine hostname.");}printf("%s", buf);return 0; }獲取域名的主機名字
#include <stdio.h> #include <stdlib.h> #include <netdb.h> #include <arpa/inet.h>int main(){int i;struct hostent *host;host = gethostbyname("www.baidu.com");if(!host){printf("Get IP address error: %s", hstrerror(h_errno));exit(0);}for(i=0; host->h_aliases[i]; i++){printf("Aliases(別名) %d: %s\n", i+1, host->h_aliases[i]);}printf("Address type(地址類型): %s\n", (host->h_addrtype==AF_INET) ? "AF_INET": "AF_INET6");for( i=0; host->h_addr_list[i]; i++){printf("IP addr(IP地址) %d: %s\n", i+1, inet_ntoa( *(struct in_addr*)host->h_addr_list[i] ) );}return 0; } #include <stdio.h> #include <stdlib.h> #include <netdb.h> #include <arpa/inet.h>main(int argc, char **argv){char *ptr, **pptr;char str[INET_ADDRSTRLEN];struct hostent *hptr;while (--argc){ptr = *++argv;if( (hptr = gethostbyname(ptr)) == NULL){printf("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:printf("unknown address type");break;}}return 0; }通過主機名獲取主機的完整信息
gethostbyaddr
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <netdb.h>int main(int argc, char **argv) {struct in_addr addr;struct hostent *phost;if (inet_pton(AF_INET, argv[1], &addr) <= 0) {printf("inet_pton error:%s\n", strerror(errno));return -1;}phost = gethostbyaddr((const char*)&addr, sizeof(addr), AF_INET);if (phost == NULL) {printf("gethostbyaddr error:%s\n", strerror(h_errno));return -1;} printf("host name:%s\n", phost->h_name);return 0; }通過IP地址獲取主機的完整信息:
解決:/etc/hosts下寫上 115.239.211.110 www.baidu.com
getserverbyname()和 getserverbyport()函數
getservbyname()和 getservbyport()函數從/etc/services 文件中獲取記錄。現在這些函數已經被 getaddrinfo()和 getnameinfo()所取代了。
類似域名映射到點位十進制標記一臺主機,我們也可以將一個別名映射到一個端口標記一個服務。這個名字到端口的映射通常保存在/etc/service中。(如果端口號改變,我們所需做的所有改動就是改動文件/etc/services中的一行,而不是重新編譯應用程序)
理論
#include <netdb.h> /* * 功能: * 參數:name: 一個指向服務名的指針。 * proto: 指向協議名的指針(可選)。如果這個指針為空,getservbyname()返回第一個name與s_name或者某一個s_aliases匹配的服務條目。否則getservbyname()對name和proto都進行匹配。 * 返回值:成功返回非空指針,失敗返回空指針 **/ struct servent * getservbyname(const char * name, const char *proto);/* * 功能: 給定端口號和可選協議后查找相應的服務 * 參數:name: 一個指向服務名的端口。 * proto: 指向協議名的指針(可選)。如果這個指針為空,getservbyname()返回第一個name與s_name或者某一個s_aliases匹配的服務條目。否則getservbyname()對name和proto都進行匹配。 * 返回值:成功返回非空指針,失敗返回空指針 **/ struct servent *getservbyport(int port, const char *protoname);返回值:
struct servent {char *s_name; //正規的服務名char **s_aliases; // 一個以空指針結尾的可選服務名隊列int s_port;// 連接該服務時需要用到的端口號,返回的端口號是以網絡字節順序排列的char *s_proto;// 連接該服務時用到的協議名 };典型調用:
struct servent *sptr; sptr = getservbyname("domain", "udp"); // DNS using UDP sptr = getservbyname("ftp", "tcp");//FTP using TCP sptr = getservbyname("ftp", NULL); //FTP using TCP sptr = getservbyname("ftp", "udp");// this call will fail 由于FTP僅支持TCP,所以第二個和第三個調用 是相同的,第四個調用將失敗。struct servent *sptr; sptr = getservbyport(htons(53), "udp"); // DNS using UDP sptr = getservbyport(htons(21), "tcp");//FTP using TCP sptr = getservbyport(htons(21), NULL);//FTP using TCP sptr = getservbyport(htons(21), "udp");// this call will fai 由于UDP中沒有使用21端口,所以第4個會失敗Linux平臺,從/etc/services文件中讀取信息,一次讀取name(如smtp),port(如25),proto(如tcp),alias(如mail,部分服務有,部分沒有)。
$ grep -e ^ftp -e ^domain /etc/services ftp 21/tcp ftp 21/udp fsp fspd domain 53/tcp # name-domain server domain 53/udp ftp 21/sctp # FTP$ grep 514 /etc/services shell 514/tcp cmd # no passwords used syslog 514/udp實踐
getservbyname
#include "netdb.h" #include "stdio.h" int main() {struct servent *se = NULL;int i = 0;se = getservbyname("domain", "udp");if (!se)return -1;printf("name : %s\n", se->s_name);printf("port : %d\n", ntohs(se->s_port));printf("proto : %s\n", se->s_proto);for (i = 0; se->s_aliases[i]; i++)printf("aliases : %s\n", se->s_aliases[i]);return 0;} #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); }值-結果參數
當往一個套接字函數傳遞一個套接字地址結構時,該結構總是以引用形式傳遞,也就是說傳遞的是指向該結構的一個指針。該結構的長度也轉為一個參數來傳遞,不過起傳遞方式取決于該結構的傳遞方向:是從進程到內核,還是從內核到進程
(1) 從進程到內核傳遞套接字地址結構的函數有3個:bind、connect和sendto。這些函數的一個參數是指向某個套接字地址結構的指針。比如:
struct sockaddr_in servaddr; connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)既然指針和指針所指的內容的大小都傳遞給了內核,內核就知道需要從進程復制多少數據進來
(2) 從內核到進程傳遞套接字地址結構的函數有4個:accept、recvfrom、getsockname、getpeername。它們其中有兩個參數:一個是指向某個套接字地址結構的指針、一個指向表示該結構大小的整形變量的指針
#include <sys/socket.h> /* * 功能: 獲取sockfd對應的本地地址,并且存儲于address參數指定的內存中,地址長度由addrlen指向的變量指定: * * 如果實際長度 大于address指定的內存區大小,那么該地址就會被階段 * * 如果實際長度 <= address指定的內存區大小,那么該地址能夠正確獲取 * 返回值:成功返回0,失敗-1并設置error */ int getsockname (int socket, struct sockaddr *__restrict address,socklen_t *__restrict addrlen) /* * 功能: 獲取sockfd對應的遠程地址,并且存儲于address參數指定的內存中,地址長度由addrlen指向的變量指定: * * 如果實際長度 大于address指定的內存區大小,那么該地址就會被階段 * * 如果實際長度 <= address指定的內存區大小,那么該地址能夠正確獲取 * 返回值:成功返回0,失敗-1并設置error */ int getpeername (int __fd, struct sockaddr *__restrict address,socklen_t *__restrict addrlen)為什么要傳遞結構體大小的指針呢?原因在于:
- 當函數被調用是,結構大小是一個值,它告訴內核該結構的大小,這樣內核在寫該結構的時候不至于越界;
- 當函數返回時,結構大小又是一個結果,它告訴內核在該結構中究竟存儲了多少信息。這種類型的參數稱為值-結構參數
總結:套接字地址結構是每個網絡程序的重要組成部分,我們分配它們,填寫它們,把指向它們的指針傳遞給各個套接字函數。有時我們把指向這些結構之一的指針傳遞給一個套接字函數,并由該函數填寫結構內容。我們總是以引用形式來傳遞這些結構,而且把該結構的大小作為另外一個參數來傳遞。當一個套接字函數需要填寫一個函數時,該結構的長度以引用形式傳遞,這樣它的值也可以被函數更改,我們把這樣的參數稱為值-結構參數
總結
以上是生活随笔為你收集整理的Unix/Linux编程:Internet domain socket的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: aptana手动配置python环境_P
- 下一篇: 事件循环、webpack、vue<前端学