1. 程式人生 > >linux網路程式設計系列-地址結構(1)

linux網路程式設計系列-地址結構(1)

linux socket程式設計中經常用到各種型別的地址, 最近在一些開原始碼中經常見到, 它們是進行socket程式設計的基礎, 本文對常見的地址結構進行簡單整理, 並在附錄中對某些函式給出了helloworld式的測試程式碼, 方便查詢.

IPV4地址結構

in_addr

這個結構內部以網路位元組序的32位無符號整數表示的ipv4地址, 在後續介紹的地址結構中會用到這個結構. 之所以一個整數也要放在一個結構體中來代表地址, 是歷史原因. 一開始這個結構內部不僅僅是一個整數, 後來演化成只有一個整數, 但是這個結構體保留了. 所以要引用一個IPV4地址, 可以傳該結構體, 也可以傳結構體中的整數, 這需要根據具體介面的要求正確使用.

struct in_addr { 
in_addr_t s_addr; /* 32-bit IPv4 address */ 
                 /* network byte ordered */ 
};

sockaddr_in

這個是一個常見的IPV4地址結構, 註釋中給出了各個欄位的含義與大小. 其中sin_family, sin_port, 以及sin_addr是POSIX規範要求的三個欄位, 其他的為額外新增欄位. 在POSIX型別中, in_addr_t必須至少是一個32位無符號整數, in_port_t至少是一個16位無符號整數型別, sa_family_t則可以是任意無符合整數型別.

可以看到, POSIX定義了規範定義了各個結構必須有的欄位, 以及欄位的大小.

struct sockaddr_in {
 uint8_t sin_len; /* length of structure (16) */
 sa_family_t sin_family; /* AF_INET */
 in_port_t sin_port; /* 16-bit TCP or UDP port number */
 /* network byte ordered */
 struct in_addr sin_addr; /* 32-bit IPv4 address */
 /* network byte ordered */
char sin_zero[8]; /* unused */ };

sockaddr

通用套接字結構, 起來void*的作用, 在定義這個結構之前, ANSI C還沒有出來, 還沒有void*, 這個結構是為了能夠處理任意格式的地址引數, 通過指標的方式來傳遞, 對於socket程式設計而言, 地址結構作為引數傳遞的時候一般是通過指標進行的, 而不是直接傳遞結構體. 結構體在本機內部使用, 而不會在機器直接傳遞.有了這個結構體, 在介面編寫的時候, 要求的引數是struct sockaddr*型別, 到了函式內部通過型別轉換變成具體的地址結構型別.

struct sockaddr {
 uint8_t sa_len;
 sa_family_t sa_family; /* address family: AF_xxx value */
 char sa_data[14]; /* protocol-specific address */
};

IPV6地址結構

in6_addr

這個結構使用128bit的空間直接儲存一個ipv6的地址.

struct in6_addr {
 uint8_t s6_addr[16]; /* 128-bit IPv6 address */
 /* network byte ordered */
};

sockaddr_in6

實際使用的IPV6地址.

#define SIN6_LEN /* required for compile-time tests */
struct sockaddr_in6 {
 uint8_t sin6_len; /* length of this struct (28) */
 sa_family_t sin6_family; /* AF_INET6 */
 in_port_t sin6_port; /* transport layer port# */
 /* network byte ordered */
 uint32_t sin6_flowinfo; /* flow information, undefined */
 struct in6_addr sin6_addr; /* IPv6 address */
 /* network byte ordered */
 uint32_t sin6_scope_id; /* set of interfaces for a scope */
};

sockaddr_storage

這個結構和sockaddr的區別在於, 足夠大, 能夠儲存任何系統支援的地址結構,且滿足任意的對齊要求, 使用的時候, 需要強制型別轉化到特定需要的地址結構. 也是通過傳指標的方式來使用的.

struct sockaddr_storage {
 uint8_t ss_len; /* length of this struct (implementation dependent) */
 sa_family_t ss_family; /* address family: AF_xxx value */
 /* implementation-dependent elements to provide:
 * a) alignment sufficient to fulfill the alignment requirements of
 * all socket address types that the system supports.
 * b) enough storage to hold any type of socket address that the
 * system supports.
 */
};

addrinfo

這個結構可以通過函式getaddrinfo來獲取,一方面可以作為getaddrinfo函式呼叫時候的過濾器, 另一方面可以裝getaddrinfo返回的結果(通過引數列表中的指標返回). 可以看到, 其是一個連結串列結構, 其結構成員如下.

struct addrinfo {
    int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
    int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
    int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
    int ai_protocol; // use 0 for "any"
    size_t ai_addrlen; // size of ai_addr in bytes
    struct sockaddr *ai_addr; // struct sockaddr_in or _in6
    char *ai_canonname; // full canonical hostname
    struct addrinfo *ai_next; // linked list, next node
};

我們接下來通過getaddrinfo函式來了解這個結構.

首先, 該函式結合了gethostbyname(3) and getservbyname(3)兩個函式的功能,也就是說, 該函式支援從名字到地址, 以及從名字到服務埠的轉換, 其支援IPV6並且是可重入(reentrant)的.而前兩個函式只支援IPV4且不可重入.

這裡補充介紹兩個過時的函式:
+ gethostbyname 把主機名字對映成Ipv4地址, 返回地址通過hostent結構體來獲取, 是一個已經廢除的函式, 其函式頭如下:
struct hostent *gethostbyname(const char *name);

  • getserverbyname 可以獲取服務名字到地址的對映.這個對映通常儲存在/etc/services中. 常見的服務如mysql, ftp等.通過名字可以知道這個服務的總體資訊

更多關於上面這兩個函式的使用, 可以看附錄中的測試程式碼.

我們開始介紹getaddrinfo函式:

  • getaddrinfo
int getaddrinfo(const char *hostname, const char *service,
                       const struct addrinfo *hints,
                       struct addrinfo **res);

我們先看引數列表, hostname代表主機名, 也可以傳字串型別的ipv4或者ipv6地址, hints用於對返回什麼型別的結果做一個提示(過濾),比如限制只查詢IPV4或者只查詢IPV6等, 然後res是一個返回結果. 從上面的引數列表, 我們可以看出這個函式的一般使用使用策略:

  • 該函式的引數service執行使用字串的服務名和字串型別的埠名, 比如”mysql”或者”3307”, 這樣我們通過指定引數, 就可以獲得對應的addrinfo結構, 這個結構中的struct sockaddr *ai_addr可以被很多socket相關的介面使用. 該引數可以是NULL.
  • 該函式的hostname引數, 支援字串結構的地址或者主機名, 比如”www.baidu.com”或者”127.0.0.1”, 然後也會返回addrinfo結構. 該引數可以是NULL.
  • 和gethostbyname以及getservbyname類似, getaddrinfo函式也支援對查詢操作進行控制(過濾), 這通過傳入hints引數來完成, 我們通過hints引數, 可以對需要什麼型別的結果做比較細的定製.這個引數也可以是NULL

  • 最後的res用於接收查詢的結果. 改函式返回值是0代表執行成功, 否則代表執行失敗.

從addrinfo的各個欄位的作用來解釋這個過濾的機制:

欄位 可能的值
int ai_family AF_INET, AF_INET6, AF_UNSPEC, 分別表示返回的地址限制IPV4,IPV6或者不限制
int ai_socktype SOCK_STREAM, SOCK_DGRAM,0 分別表示返回地址是流模式,資料包模式,以及任意socket型別
int ai_protocol 一般使用0, 表示返回地址可以是任意協議
int ai_flags 當這個標誌是AI_PASSIVE且hostname是NULL的時候, 返回的地址可以用於server端,用於bind和accept, 並且 接受指向本機任意地址的連線. 如果AI_PASSIVE沒有被設定, 那麼這個返回的地址可以用於client端, 用於connect和sendto等函式, 此時如果hostname是NULL, 則預設是迴環地址(127.0.0.1). 如果設定了AI_CANONNAME, 則返回的連結串列中的第一個節點指向一個名字.
socklen_t ai_addrlen 設定為0
struct sockaddr *ai_addr 設定為NULL
char *ai_canonname 設定為NULL
struct addrinfo *ai_next 設定為NULL

可以看到, addrinfo有兩個作用, 其中部分的成員用於過濾返回型別, 還有部分成員用於裝返回地址.對於getaddrinfo函式, 其作用是, 分配連結串列空間, 並在找到所有和提供的service以及hostname匹配專案構造addrinfo, 並且根據hints中的選項做進一步的篩選, 然後通過引數列表中的res返回一個連結串列頭,連結串列中的每個節點可以看出對之前介紹的地址型別的封裝. 函式返回多個addrinfo結構可能有如下原因:

  • 多個網絡卡
  • 單個網絡卡支援多種協議AF_INET and AF_INET6
  • 同一個服務有基於 SOCK_DGRAM的, 也有基於 SOCK_STREAM的

需要注意的是, 返回的res結構使用完畢以後, 需要通過freeaddrinfo函式使用資源.

補充說明

關於位元組序

上面說到網路位元組序的問題, 所謂位元組序, 就是一個多位元組的變數在記憶體中放置的順序. 比如一個int佔有32個位元組, 這個型別的存放方式有兩種, 一種是低位存低地址, 叫little endian, 一種是低位存高地址, 叫big endian. 不同的機子對於單位元組資料的讀取是一致的, 因為它們都以8個bits作為一個byte, 但是對於多位元組的資料, 位元組序在不同的機子上並沒有約定. 對於一個四位元組的整數, big endian認為第一個位元組位於地址最大的位置, little endian認為第一個位元組位於地址最小的位置, 所以網路傳輸的時候, 需要規定網路位元組序,現行的規定是big endian(傳輸是由低地址到高地址傳). 由於不同機器的位元組序可能不一樣, 所以在進行網路傳輸的時候, 需要使用ntohxx,htonxx系列的函式進行位元組序的轉換.在ubuntu下, 可以使用命令lscpu 來檢視自己的機器是哪種位元組序.

附錄程式

gethostbyname

#include <stdio.h>
#include <netdb.h>
void printHostent(struct hostent *h){
    //輸出canonical 名字
    printf("%s\n",h->h_name);
    switch(h->h_addrtype){
        case AF_INET:{
            //連結串列的形式給出一系列的IP地址
            char **pptr = h->h_addr_list;
            for(;*pptr!=NULL;pptr++){
                printf("%d.%d.%d.%d\n",(int)pptr[0][0],pptr[0][1],pptr[0][2],pptr[0][3]);
            }
        }
        default:
            break;
    }
}

int main(){
    struct hostent *h;
    //這裡引數是主機名
    h=gethostbyname("www.baidu.com");
    printHostent(h);

    h=gethostbyname("localhost");
    printHostent(h);
    return 0;
}

getservbyname

#include <stdio.h>
#include <netdb.h>


//struct servent {
//    char  *s_name;       /* official service name */
//    char **s_aliases;    /* alias list */
//    int    s_port;       /* port number */
//    char  *s_proto;      /* protocol to use */
//}

void printserver(struct servent * s){
    if(s==NULL) return;
    printf("port=%d\n",ntohs(s->s_port));
}

int main(){
    struct servent * s;
    s = getservbyname("domain","udp");
    printserver(s);
    s = getservbyname("ftp","tcp");
    printserver(s);
    s = getservbyname("mysql",NULL);
    printserver(s);

    return 0;
}

getaddrinfo

下面程式實現了從服務到ip地址的轉換, 以及從域名到ip地址的轉換.

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>

#define BUF_SIZE 500

void printInfo(struct addrinfo *result){
    struct addrinfo *rp;
    char buf[BUF_SIZE];
    int i=0;
    for (rp = result; rp != NULL; rp = rp->ai_next) {
       struct sockaddr_in *in = (struct sockaddr_in*)(rp->ai_addr);
       printf("port = %d :",ntohs(in->sin_port));
       inet_ntop(AF_INET, &(in->sin_addr), buf, INET_ADDRSTRLEN);
       printf("ip = %s ",buf);
       printf("name = %s \n",rp->ai_canonname);

    }
}
int main(){
   struct addrinfo hints;
   struct addrinfo *result;
   int sfd, s;
   struct sockaddr_storage peer_addr;
   socklen_t peer_addr_len;
   ssize_t nread;

   //初始化hints, 通過hints來對查詢進行提示.
   memset(&hints, 0, sizeof(struct addrinfo));
   //同時執行IPV4和IPV6
   hints.ai_family = AF_UNSPEC;
   hints.ai_socktype = 0;
   hints.ai_flags = AI_PASSIVE|AI_CANONNAME;
   //任意的協議族
   hints.ai_protocol = 0;
   hints.ai_canonname = NULL;
   hints.ai_addr = NULL;
   hints.ai_next = NULL;

   s = getaddrinfo("localhost", "mysql", &hints, &result);
   if (s != 0) {
       fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
       exit(EXIT_FAILURE);
   }
    printInfo(result);

   s = getaddrinfo("localhost", "ftp", &hints, &result);
   if (s != 0) {
       fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
       exit(EXIT_FAILURE);
   }
   printInfo(result);

   s = getaddrinfo("www.baidu.com", NULL, &hints, &result);
   if (s != 0) {
       fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
       exit(EXIT_FAILURE);
   }
   printInfo(result);

   freeaddrinfo(result);
}

來自man文件的client/server程式例子:

//server端
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>

#define BUF_SIZE 500

int
main(int argc, char *argv[]){
   struct addrinfo hints;
   struct addrinfo *result, *rp;
   int sfd, s;
   struct sockaddr_storage peer_addr;
   socklen_t peer_addr_len;
   ssize_t nread;
   char buf[BUF_SIZE];

   if (argc != 2) {
       fprintf(stderr, "Usage: %s port\n", argv[0]);
       exit(EXIT_FAILURE);
   }

   memset(&hints, 0, sizeof(struct addrinfo));
   hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
   hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
   hints.ai_flags = AI_PASSIVE;    /* For wildcard IP address */
   hints.ai_protocol = 0;          /* Any protocol */
   hints.ai_canonname = NULL;
   hints.ai_addr = NULL;
   hints.ai_next = NULL;

   s = getaddrinfo(NULL, argv[1], &hints, &result);
   if (s != 0) {
       fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
       exit(EXIT_FAILURE);
   }

   /* getaddrinfo() returns a list of address structures.
      Try each address until we successfully bind(2).
      If socket(2) (or bind(2)) fails, we (close the socket
      and) try the next address. */

   for (rp = result; rp != NULL; rp = rp->ai_next) {
       sfd = socket(rp->ai_family, rp->ai_socktype,
               rp->ai_protocol);
       if (sfd == -1)
           continue;

       if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
           break;                  /* Success */

       close(sfd);
   }

   if (rp == NULL) {               /* No address succeeded */
       fprintf(stderr, "Could not bind\n");
       exit(EXIT_FAILURE);
   }

   freeaddrinfo(result);           /* No longer needed */

   /* Read datagrams and echo them back to sender */

   for (;;) {
       peer_addr_len = sizeof(struct sockaddr_storage);
       nread = recvfrom(sfd, buf, BUF_SIZE, 0,
               (struct sockaddr *) &peer_addr, &peer_addr_len);
       if (nread == -1)
           continue;               /* Ignore failed request */

       char host[NI_MAXHOST], service[NI_MAXSERV];

       s = getnameinfo((struct sockaddr *) &peer_addr,
                       peer_addr_len, host, NI_MAXHOST,
                       service, NI_MAXSERV, NI_NUMERICSERV);
      if (s == 0)
           printf("Received %zd bytes from %s:%s\n",
                   nread, host, service);
       else
           fprintf(stderr, "getnameinfo: %s\n", gai_strerror(s));

       if (sendto(sfd, buf, nread, 0,
                   (struct sockaddr *) &peer_addr,
                   peer_addr_len) != nread)
           fprintf(stderr, "Error sending response\n");
   }
}
//client端
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define BUF_SIZE 500

int
main(int argc, char *argv[])
{
   struct addrinfo hints;
   struct addrinfo *result, *rp;
   int sfd, s, j;
   size_t len;
   ssize_t nread;
   char buf[BUF_SIZE];

   if (argc < 3) {
       fprintf(stderr, "Usage: %s host port msg...\n", argv[0]);
       exit(EXIT_FAILURE);
   }

   /* Obtain address(es) matching host/port */

   memset(&hints, 0, sizeof(struct addrinfo));
   hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
   hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
   hints.ai_flags = 0;
   hints.ai_protocol = 0;          /* Any protocol */

   s = getaddrinfo(argv[1], argv[2], &hints, &result);
   if (s != 0) {
       fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
       exit(EXIT_FAILURE);
   }

   /* getaddrinfo() returns a list of address structures.
      Try each address until we successfully connect(2).
      If socket(2) (or connect(2)) fails, we (close the socket
      and) try the next address. */

   for (rp = result; rp != NULL; rp = rp->ai_next) {
       sfd = socket(rp->ai_family, rp->ai_socktype,
                    rp->ai_protocol);
       if (sfd == -1)
           continue;

       if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1)
           break;                  /* Success */

       close(sfd);
   }

   if (rp == NULL) {               /* No address succeeded */
       fprintf(stderr, "Could not connect\n");
       exit(EXIT_FAILURE);
   }

   freeaddrinfo(result);           /* No longer needed */

   /* Send remaining command-line arguments as separate
      datagrams, and read responses from server */

   for (j = 3; j < argc; j++) {
       len = strlen(argv[j]) + 1;
               /* +1 for terminating null byte */

       if (len + 1 > BUF_SIZE) {
           fprintf(stderr,
                   "Ignoring long message in argument %d\n", j);
           continue;
       }

       if (write(sfd, argv[j], len) != len) {
           fprintf(stderr, "partial/failed write\n");
           exit(EXIT_FAILURE);
       }

       nread = read(sfd, buf, BUF_SIZE);
       if (nread == -1) {
           perror("read");
           exit(EXIT_FAILURE);
       }

       printf("Received %zd bytes: %s\n", nread, buf);
   }

   exit(EXIT_SUCCESS);
}

原始連結:yiwenshao.github.io/2016/12/18/linux網路程式設計系列-地址結構-1/

文章作者:Yiwen Shao

許可協議: Attribution-NonCommercial 4.0

轉載請保留以上資訊, 謝謝!