网络编程(15)名字与地址转换

socket网络编程 专栏收录该内容
20 篇文章 1 订阅

万字长文警告。。。。。。。


前面有关套接字的UDP和TCP网络编程中,都是使用点分十进制、的ip地址字符串(例如”192.168.0.100”)来表示主机,使用数值端口来表示服务器(例如80表示http服务器)。但是多数情况下,可以使用名字而不是数值来表示主机:名字容易记,ip地址可以变动但名字保持不变,ipv6地址过长不便于记忆和键入等。

本章主要介绍:主机名与数值地址转换(gethostbyname,gethostbyaddr)服务名与端口转换(getserverbyname,getserverbyaddr)。前面的4个函数方法仅支持IPv4相关协议,还介绍与协议无关的函数getaddrinfo、getnameinfo。另外还提到了与套接字地址转换类似关于可重入函数局域网IP地址获取的说明。



其他有关地址相关的博客

网络编程(5)套接字地址结构、地址转换

网络编程(9)获取socket地址信息



1、主机名与地址解析

在使用有关地址、名字相关的转换函数前,简单说明两个简单的概念。

(1) 主机名

为了方便记忆和使用,对某一台网络设备进行标识并能被其他网络发现而起的名字。这个名字可以是简单的字符串,例如”localhost”标志本机,”DESKTOP-UANHMRS”标识某台windows电脑;还可以是一个全域名,例如“www.baidu.com”,“www.microsoft.com”。

(2) 地址解析

主机名转换成地址时,需要用到地址解析。通常对于域名,需要DNS服务器进行解析,提供DNS解析的运营商维护域名和地址对应关系,当进行网络访问时会将实际的网络请求传递到实际的ip地址对应的物理主机进行处理。同样,也能使用静态的方式,例如在hosts文件中进行人为的指定域名(或简单名字)和地址对应关系(linux下/etc/hosts,windows下C:\Windows\System32\drivers\etc\hosts)。

2、主机名与地址转换

2.1 gethostname

#include <unistd.h>
int gethostname(char *hostname, size_t len);   // 返回:0成功, -1出错

函数getsockname获取本机的主机名。先在命令行中使用hostname获取本机名:
在这里插入图片描述
使用如下代码进行测试

#include <stdio.h>

#include <unistd.h>   // gethostname
void main()
{
  char host[32] = {0};
  if( gethostname(host, sizeof(host)) < 0){
    printf("gethostname failed. %s", strerror(errno));
  }
  printf("hostname: %s\n", host);
}

测试结果如下
在这里插入图片描述

2.2 gethostbyname

#include <netdb.h>

struct hostent{
  char *h_name;      /* 主机规范名.  */
  char **h_aliases;   /* 别名列表  */
  int h_addrtype;     /* 地址类型,固定AF_INET */
  int h_length;     /* 地址长度,固定4 */
  char **h_addr_list;   /* IP地址列表  */
};

struct hostent *gethostbyname (const char *hostname);
//返回:成功返回非空指针,出错返回NULL并设置h_errno

参数hostname是要查询的主机名,例如“localhost”,“www.baidu.com”,即可以是简单主机名或域名,也可能是点分十进制地址字符串,例如“192.168.0.1”。当发生错误时会修改全局变量h_errno,使用hstrerror函数输出错误说明。目前仅支持获取IPv4地址

#include <stdio.h>

#include <unistd.h>   // gethostname

#include <arpa/inet.h>      // sockaddr_in, inet_addr
#include <netdb.h>          // gethostby***,  getaddrinfo,   h_error enum

#include <errno.h>
#include <cstring>

int main()
{
  const char host[] = "www.baidu.com";

  printf("check host: %s\n\n", host);

  struct hostent *hptr;
  // 只能返回IPv4地址,host允许是点分十进制字串
  if( (hptr = gethostbyname(host)) == NULL) { 
    // 错误时,设置全局变量h_errno,取值在<netdb.h>中,
    // HOST_NOTFOUND,TRY_AGAIN, NO_RECOVERY, NO_DATA
    printf("gethostbyname error from \"%s\": %s\n", host, hstrerror(h_errno));
    return ;
  }

  printf("official name: %s\n", hptr->h_name);

  char **pptr;
  for(pptr = hptr->h_aliases; *pptr != NULL; pptr++){
    printf("  alias: %s\n", *pptr);
  }
  char str[16];
  for(pptr = hptr->h_addr_list; *pptr != NULL; pptr++){
    printf("address: %s\n", inet_ntop(hptr->h_addrtype, *pptr, str, 16));
  }
}

在这里插入图片描述
其他测试如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3 gethostbyaddr

#include <netdb.h>
struct hostent *gethostbyname (const char *addr, socklen_t len, int family);
//返回:成功返回非空指针,出错返回NULL并设置h_errno

gethostbyname用于由一个二进制的IP地址找到相应的主机名,感兴趣的是返回值hostent
中的h_host变量。实际中网络编程用的不多,主要用在内网中查询局域网中的网络设备名称。

测试代码段

   // char ip[] = "127.0.0.1" ;       // "localhost"      
   char ip[] = "192.168.250.100"; 

    in_addr addr;
    inet_aton(ip, &addr);      // ip地址转换为二进制地址
   // inet_pton(AF_INET, ip, &addr)

    if( (hptr = gethostbyaddr(&addr, sizeof(addr), AF_INET)) == NULL) { 
        printf("gethostbyaddr error from: %s\n", hstrerror(h_errno));
        return ;
    }

    printf("official name: %s\n", hptr->h_name);

    char **pptr;
    for(pptr = hptr->h_aliases; *pptr != NULL; pptr++){
        printf("  alias: %s\n", *pptr);
    }
    char str[16];
    for(pptr = hptr->h_addr_list; *pptr != NULL; pptr++){
        printf("address: %s\n", inet_ntop(hptr->h_addrtype, *pptr, str, 16));
    }

在这里插入图片描述
在这里插入图片描述 在这里插入图片描述

3、服务名与端口转换

同主机一样,服务也通常靠名字来认知。在程序代码中通过其名字而不是其端口来指代按一个服务,而且从服务名字到端口号的映射关系保存在一个文件中(通常是/etc/services)。

getserverbyname()函数用于根据名字查找相应的服务端口号,getserverbyaddr()函数用于根据指定端口号和可选协议查找对应的服务名

在端口服务映射文件/etc/services中,查看几个常见的的服务:

服务端口/协议服务别名说明
domain53/tcp# Domain Name Server
domain53/udp
http80/tcpwww# WorldWideWeb HTTP

服务domain使用端口号53,使用协议udp或tcp;服务http使用端口号80,仅使用tcp协议;服务tftp使用端口69,仅使用udp协议。

3.1 getservbyname

#include <netdb.h>

struct servent{
  char *s_name;         /* 标准服务名 */
  char **s_aliases;    /* 别名列表  */
  int s_port;           /* 端口号(网络序)  */
  char *s_proto;        /* 使用协议  */
};

struct servent *getservbyname (const char *servname, const char *protoname);
//返回:成功返回非空指针,出错返回NULL并设置h_errno

参数servname是要查询的服务名,必须指定。如果同时指定了协议即protoname非空,那么要查询的服务必须有匹配的协议,否则出错。如果未指定协议即传入protoname为NULL,而指定服务支持多个协议,那么返回的端口号是哪一个取决于实现。通常情况下,支持多个协议的服务使用相同的TCP和UDP端口号。

int main()
{
  servent *sptr = getservbyname("domain", NULL);
  if(sptr == NULL){
printf("getservbyname error. %s\n", hstrerror(h_errno)); 
return 0;
  }
  printf("service name: %s, port: %d, proto: %s", sptr->s_name, ntohs(sptr->s_port), sptr->s_proto);

  char **pptr;
  for(pptr = sptr->s_aliases; *pptr != NULL; pptr++){
      printf(", alias: %s", *pptr);
  }
}

下标中给出不同的测试结果,
在这里插入图片描述
实验结果情况如前函数说明一致。

例如domain服务支持tcp和udp,端口号都是53,当不指定协议时,默认返回是tcp协议,端口53;指定协议udp时,同样返回端口53。

又如http服务,仅支持tcp协议,使用端口80,当指定protoname为“udp”时无法解析;同样仅支持udp协议的tftp服务指定protoname为“tcp”时无法解析。

3.2 getservbyaddr

#include <netdb.h>
struct servent *getservbyaddr (int port, const char *protoname);
//返回:成功返回非空指针,出错返回NULL并设置h_errno

参数port要求是网络字节序,protoname参数要求同getservbyport函数。这里关心返回的服务名即servent结构的s_name。

给出如下实验表,
在这里插入图片描述
处理结果和上一节完全一致。

4、协议无关的转换

4.1 getaddrinfo

前一小节中,gethostbyaddr和gethostbyaddr这两个函数仅支持IPv4,是建议废弃的(实际上目前IPv4还不能完全被IPv4替代)。新的函数getaddrinfo是协议无关的,同时支持IPv4和IPv6地址,能够处理主机名以及服务到端口这两种转换,返回结果是sockaddr结构列表而不是一个点分十进制的ip地址列表,sockaddr结构可以由套接字函数bind、listen、connect、recvfrom、sendto等直接使用。

4.1.1 相关函数说明

#include <netdb.h>

struct addrinfo{
  int ai_flags;             /* 标志,一个或多个AI_xxx值 */
  int ai_family;            /* 套接字协议族 AF_xxx  */
  int ai_socktype;          /* 套接字类型 SOCK_xxx  */
  int ai_protocol;          /* IP协议类型  IPPROTO_xxx */
  socklen_t ai_addrlen;	     /* 套接字地址结构的长度 */
  struct sockaddr *ai_addr;  /* 套接字地址结构  */
  char *ai_canonname;	     /* 规范主机名,仅第一个结构体有值  */
  struct addrinfo *ai_next;   /* 下一个addrinfo 地址,其ai_canonname为空*/
};

//返回:成功返回0,出错返回非0
int getaddrinfo (const char *hostname,      /* 主机名或IPv4和IPv6点分十进制地址 */
const char *service,         /* 服务名或对应的十进制端口号字串  */
const struct addrinfo *hints,  /* 期望返回的信息类型的暗示 */
struct addrinfo **res);       /*  成功时,指向addrinfo结构列表的指针*/

参数hostname可以是主机名(包括如”localhost”简单主机名,可以是如“www.baidu.com的域名”),也可以是IPv4或IPv6的点分十进制地址数串。

参数service是需要查询的服务名或十进制服务端口数串,例如“domain”/”53”,“ssh”/”22”等。服务名或端口号的传递会受到hints参数ai_flag的设置影响。

参数hints可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:指定的服务既可支持TCP也可支持UDP,所以调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。

在这里插入图片描述

如果hint是一个空指针时,实际是设置hints是参数ai_flagsai_socktypeai_protocol为0,ai_famalyAF_UNSPEC。即代码块

struct addrinfo     hints, *res
bzero(&hints, sizeof(hints));
// hints.ai_family = AF_UNSPEC;  // 可省略,AF_UNSPEC值为0
getaddrinfo(hostname, service, &hints, &res)

getaddrinfo(hostname, service, NULL, &res) 结果相同。

函数调用成功返回0,那么res参数指向的变量已经填入一个指针,它指的是由其中的ai_next成员穿起来的addrinfo结构链表。如果出错返回非零值,使用const char * gai_strerror(int error);返回一个指向对应的出错信息字符串

4.1.2 使用示例与说明

这里说明hostname、service传递参数不同情况,先直接给出以hostname为“www.baidu.com”service为NULL,仅设置ai_flag为AI_CANONNAME的示例代码。

#include <stdio.h>
#include <arpa/inet.h>  // sockaddr_in, inet_addr
#include <netdb.h>     // gethostby***, getaddrinfo, h_error, gai_ strerror …
#include <cstring>

void main()
{
  char host[] = "www.baidu.com";
  char service[] = "http"; //http/80, ssh/22, domain/53 …
  struct addrinfo hints, *res;
  bzero(&hints, sizeof(hints));
  // hints.ai_flags = AI_PASSIVE;
  hints.ai_flags |= AI_CANONNAME;
  // hints.ai_flags |= AI_PASSIVE;
  // hints.ai_flags |= AI_V4MAPPED | AI_ALL;
  // hints.ai_family = AF_UNSPEC;
  // hints.ai_flags |= AI_NUMERICSERV;
  // hints.ai_socktype = SOCK_STREAM;
  // hints.ai_socktype = SOCK_DGRAM;

  int ret;
  if ((ret = getaddrinfo(host, NULL, &hints, &res)) < 0){
    printf("get addrinfo error: %s\n", gai_strerror(ret)); // getaddrinfo error
    return;
  }

  // 返回成功且ai_flags设置AI_CANONNAME,有且仅有第一个addrinfo中规范主机名不为空
  printf("canonname: %s\n", res->ai_canonname); 

  // 打印输出
  for (; res != NULL; res = res->ai_next){
    char family[16] = {0};
    char socktype[16] = {0};
    char protocal[16] = {0};
    char ip[INET6_ADDRSTRLEN] = {0};
    int port; // 不指定服务即参数service传递NULL时,返回端口为0

    if (res->ai_family == AF_INET){
      memcpy(family, "AF_INET", strlen("AF_INET"));

      sockaddr_in *addr = (sockaddr_in *)res->ai_addr;
      inet_ntop(res->ai_family, &addr->sin_addr, ip, res->ai_addrlen);
      port = ntohs(addr->sin_port);
    }
    else if (res->ai_family == AF_INET6){
      memcpy(family, "AF_INET6", strlen("AF_INET6"));

      sockaddr_in6 *addr = (sockaddr_in6 *)res->ai_addr;
      inet_ntop(res->ai_family, &addr->sin6_addr, ip, res->ai_addrlen);
      port = ntohs(addr->sin6_port);
    }
    else{
      printf("unknow famaly %d\n", res->ai_family);
      continue;
    }

    switch (res->ai_socktype){
      case SOCK_DGRAM: memcpy(socktype, "SOCK_DGRAM", strlen("SOCK_DGRAM")); break;
      case SOCK_STREAM:memcpy(socktype, "SOCK_STREAM", strlen("SOCK_STREAM")); break;
      case SOCK_RAW: memcpy(socktype, "SOCK_RAW", strlen("SOCK_RAW")); break;
      default:
        printf("\nunknow socktype %d\n", res->ai_socktype);
        memcpy(socktype, "UNKNOW", strlen("UNKNOW"));
    }

    switch (res->ai_protocol){
      case IPPROTO_IP: memcpy(protocal, "IPPROTO_IP", strlen("IPPROTO_IP"));break;
      case IPPROTO_UDP: memcpy(protocal, "IPPROTO_UDP", strlen("IPPROTO_UDP")); break;
      case IPPROTO_TCP: memcpy(protocal, "IPPROTO_TCP", strlen("IPPROTO_TCP"));break;
      default:
        printf("unknow protocal %d\n", res->ai_protocol);
        memcpy(protocal, "UNKNOW", strlen("UNKNOW"));
    }

    printf(
      "\nai_family: %s\n"
      "ai_socktype: %s\n"
      "ai_protocol: %s\n"
      "ip addres: %s %d\n",
      family, socktype, protocal, ip, port);
  }

  freeaddrinfo(res); // 使用getaddrinfo后必须调用以释放动态分配的res内存
}

上述代码的运行结果如下左侧截图,代码中设置了参数hintai_flag仅为了获取规范名字,并且仅有第一个地址结构中的规范名字段ai_canonname 有数据。遍历地址链表的结果实际与getaddrinfo(hostname, NULL, NULL, &res)相同。

在前面的gethostbyname中已经知道主机名“www.baidu.com” 关联了两个ip地址,这里返回了两个ip地址相关适应每个地址族的每个地址对应的一个结构。简单来说,当不设置service、hints时,将返回所有不同服务、不同ip地址对应的主机名的地址结构。这里没有指定任何服务,所有地址中的端口号都为0。

在这里插入图片描述 在这里插入图片描述

即如果中。修改代码,仅将service参数设置为“http”或“80”时,会从上述左侧地址中查找满足当前”http服务”查询的结果,返回结果如上右侧截图,端口号数为80。

注意,由于结果指针res指向的链表内存是动态分配的,调用完成后需要使用特定函数freeaddrinfo(res)进行内存释放



下面介绍传递参数的四类组合情况的对比

(1) service NULL,hints NULL

这种情况在上面的例子中说明,功能和getaddrbyname()类似。查询主机名www.baidu.com的地址时,getaddrbyname()仅返回了两个ip地址的结果,共2个结果;getaddrinfo()也返回同样两个ip地址的结果,但是将关联这两个ip地址的所有协议服务等分条列出,共6个结果。

查询主机名”localhost”的地址,getaddrbyname和getaddrinfod结果分别如下:

在这里插入图片描述在这里插入图片描述
(2) service 非空,hints NULL

这种情况下,相当于从 (1)中的的处理结果中,在对查询的主机名,筛选返回满足指定服务的地址信息。在上面的函数说明中已经给出了示例,这里给出调用getaddrinfo("localhost", NULL, NULL, &res)getaddrinfo("localhost ", "http", NULL, &res)的运行结果,当指定”http”服务(SOCK_STREAM、IPPROTO_TCP)仅返回满足条件的信息。分别如下图。

在这里插入图片描述 在这里插入图片描述
(3) service NULL,hints 非空

这种情况下,这种情况下,相当于从(1)的处理结果(6个)中,在对查询的主机名,筛选返回满足hints条件的地址信息。

例如,我们查询主机名“”www.baicu.com”, hints设置套接字类型为SOCK_STREAM,再将套接字类型为SOCK_DGRAM。两次处理修改代码和结果如下

struct addrinfo hints, *res;
bzero(&hints, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;

getaddrinfo("www.baidu.com", NULL, &hints, &res);

在这里插入图片描述

getaddrinfo("www.baidu.com", NULL, &hints, &res)struct addrinfo hints, *res;
bzero(&hints, sizeof(hints));
hints.ai_socktype = SOCK_DGRAM;

getaddrinfo("www.baidu.com", NULL, &hints, &res);

在这里插入图片描述
(4) service 非空,hints 非空

相当于在(3)service NULL,hints 非空 基础上再一次筛选满足服务的地址信息。这里我们以主机名”www.taobao.com”为例,其关联多个ip主机且支持IPv6协议。
示例代码和结果

char host[] = "www.taobao.com";

struct addrinfo hints, *res;
bzero(&hints, sizeof(hints));
hints.ai_flags |= AI_CANONNAME;   // 规范名
hints.ai_flags |= AI_NUMERICSERV; // 服务名必须使用十进制端口号数串
hints.ai_family = AF_INET6;         // IPv6协议
hints.ai_socktype = SOCK_STREAM;  // 流式套接字

char service[] = "80";   // 设置NUMERICSERV标志,不能使用“http” 

getaddrinfo(host, service, &hints, &res)

在这里插入图片描述
通常,在使用getaddrinfo()函数时一般通过参数hints和参数service,以返回我们期望的地址信息结果:

  • 指定hostname和service

    用于TCP或UDP客户端调用getaddrinfo()的常规输入。TCP客户调用返回后,遍历循环每一个返回的ip地址,逐一调用socket、connect,直到有一个连接成功,或者所有地址尝试一遍。

    UDP客户由getaddrinfo()填入的套接字地址结构调用sento或connect。如果客户能判断第一个地址不工作(已连接套机字上收到出错消息,或者在未连接的套接字上接受消息超时),可以尝试其余的地址。

    如果客户清楚只处理一种类型套接字,应该把hints结构的ai_socktype成员设置成SOCK_STREAM或SOCK_DGRAM。

  • 只指定service,不指定hostname

    典型的服务器进程使用情况,仅指定service而不指定hostname,同时在hints中指定AI_PASSIVE标志,返回的套接字地址结构中应含有INADDR_ANY(对与IPv4)

    或IN6ANND_ANY_INIT(对于IPv6)的IP地址。TCP服务器随后调用socket、bind和listen,UDP服务器将调用socket、bind和recvfrom。

    与典型的客户一样,如果服务器清楚自己只处理一种类型套接字,应该把hints结构的ai_socktype成员设置成SOCK_STREAM或SOCK_DGRAM,避免返回多个结构,其中可能出现错误的ai_socktype值。

    调用getaddrinfo(NULL, “http”, NULL, &res);的结果
    在这里插入图片描述

    然而实际使用中,这种获取回环地址127.0.0.1的方式只能在本机上使用,在局域网中需要获取局域网的ip地址,相关内容在第5.3节中说明。

4.1.3 不同应用场景举例

这里对使用getaddrinfo()函数在不同应用场景下的举例,给出演示代码

(1)Host server

struct addrinfo *
Host_serv(const char *host, const char *serv, int family, int socktype)
{
  int n;
  struct addrinfo hints, *res;

  bzero(&hints, sizeof(struct addrinfo));
  hints.ai_flags = AI_CANONNAME; /* always return canonical name */
  hints.ai_family = family;      /* 0, AF_INET, AF_INET6, etc. */
  hints.ai_socktype = socktype;  /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */

  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
    err_quit("host_serv error for %s, %s: %s",
             (host == NULL) ? "(no hostname)" : host,
             (serv == NULL) ? "(no service name)" : serv,
             gai_strerror(n));

  return (res); /* return pointer to first on linked list */
}

(2)Tcp connect

调用getaddrinfo一次,指定地址族为AF_UNSPEC,套接字类型为SOCK_STREAM。函数返回之后,对每个ip地址进行调用socket和connect。当返回地址有IPv6地址而主机不支持IPv6时,调用socke失败时就应该退出函数。

int tcp_connect(const char *host, const char *serv)
{
    int sockfd, n;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
        err_quit("tcp_connect error for %s, %s: %s",
                 host, serv, gai_strerror(n));
    ressave = res;

    do{
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sockfd < 0)
            continue; /* ignore this one */

        if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
            break; /* success */

        Close(sockfd); /* ignore this one */
    } while ((res = res->ai_next) != NULL);

    if (res == NULL) /* errno set from final connect() */
        err_sys("tcp_connect error for %s, %s", host, serv);

    freeaddrinfo(ressave);

    return (sockfd);
}

(3)Tcp listen

创建一个TCP套接字,捆绑到一个总所周知的端口,并允许接收外来的连接请求。初始化addrinfo结构提供如下暗示信息:AI_PASSIVE(本函数供服务器使用)、AF_UNSPEC(也能明确的指定地址族为AF_INT或AF_INET6)、SOCK_STREAM。调用socket和bind函数,如果任一个调用失败就尝试下一个IP地址。对于TCP服务器,总是设置SO_REUSEADDR套接字选项。调用bind成功后继续调用listen,使的当前套接字变成监听套接字以接收外来连接请求。

int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
    int listenfd, n;
    const int on = 1;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
        err_quit("tcp_listen error for %s, %s: %s",
                 host, serv, gai_strerror(n));
    ressave = res;

    do{
        listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (listenfd < 0)
            continue; /* error, try next one */

        Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
        if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
            break; /* success */

        Close(listenfd); /* bind error, close and try next one */
    } while ((res = res->ai_next) != NULL);

    if (res == NULL) /* errno from final socket() or bind() */
        err_sys("tcp_listen error for %s, %s", host, serv);

    Listen(listenfd, LISTENQ);

    if (addrlenp)
        *addrlenp = res->ai_addrlen; /* return size of protocol address */

    freeaddrinfo(ressave);

    return (listenfd);
}

(4)Udp clinet

创建一个未连接的UDP套接字。输入的是目的ip地址和端口服务,返回目标套接字、目标套接字地址结构和长度,便于直接利用在稍后的sendto函数。传递的参数saptr和lenp的空间要足够保存目标地址结构数据。

int udp_client(const char *host, const char *serv, sockaddr **saptr, socklen_t *lenp)
{
  int sockfd, n;
  struct addrinfo hints, *res, *ressave;

  bzero(&hints, sizeof(struct addrinfo));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_DGRAM;

  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
    err_quit("udp_client error for %s, %s: %s",
             host, serv, gai_strerror(n));
  ressave = res;

  do{
    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (sockfd >= 0)
      break; /* success */
  } while ((res = res->ai_next) != NULL);

  if (res == NULL) /* errno set from final socket() */
    err_sys("udp_client error for %s, %s", host, serv);

  *saptr = Malloc(res->ai_addrlen);
  memcpy(*saptr, res->ai_addr, res->ai_addrlen);
  *lenp = res->ai_addrlen;

  freeaddrinfo(ressave);

  return (sockfd);
}

(5)udp connect

创建一个已连接的UDP套接字,不需要同udp client中保存目标地址套接字结构,将sendto改用为write。

int udp_connect(const char *host, const char *serv)
{
  int sockfd, n;
  struct addrinfo hints, *res, *ressave;

  bzero(&hints, sizeof(struct addrinfo));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_DGRAM;

  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
    err_quit("udp_connect error for %s, %s: %s",
             host, serv, gai_strerror(n));
  ressave = res;

  do{
    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (sockfd < 0)
      continue; /* ignore this one */

    if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
      break; /* success */

    Close(sockfd); /* ignore this one */
  } while ((res = res->ai_next) != NULL);

  if (res == NULL) /* errno set from final connect() */
    err_sys("udp_connect error for %s, %s", host, serv);

  freeaddrinfo(ressave);

  return (sockfd);
}

(6)udp server

除了没有调用listen函数,代码几乎等同于tcp_listen。这里设置地址族为AF_UNSPEC,以支持IPv4和IPv6,并且明确指出了套接字类型为SOCK_DGRAM。对于UDP服务端套接字不设置SO_REUSEADDR,因为UDP套接字没有TCP的TIME_WAIT状态,也就没有必要在启动服务器时设置该选项。

int udp_server(const char *host, const char *serv, socklen_t *addrlenp)
{
  int sockfd, n;
  struct addrinfo hints, *res, *ressave;

  bzero(&hints, sizeof(struct addrinfo));
  hints.ai_flags = AI_PASSIVE;
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_DGRAM;

  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
    err_quit("udp_server error for %s, %s: %s",
             host, serv, gai_strerror(n));
  ressave = res;

  do{
    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (sockfd < 0)
      continue; /* error - try next one */

    if (bind(sockfd, res->ai_addr, res->ai_addrlen) == 0)
      break; /* success */

    Close(sockfd); /* bind error - close and try next one */
  } while ((res = res->ai_next) != NULL);

  if (res == NULL) /* errno from final socket() or bind() */
    err_sys("udp_server error for %s, %s", host, serv);

  if (addrlenp)
    *addrlenp = res->ai_addrlen; /* return size of protocol address */

  freeaddrinfo(ressave);

  return (sockfd);
}

在能明确套接字类型时,尽量给出其ai_socktype标志值。设置地址族标志ai_family为AF_UNSPEC时,创建的socket应用是协议无关的,同时支持IPv4和IPv6。

注意,尽管getaddrinfo比gethostbyname和getserverbyname要“好”(便于编写协议无关的程序代码,能够同时处理主机名和服务,返回信息都是动态分配的)。使用时必须先分配一个hint结构,清零后填写需要的字段,再调用getaddrinfo,然后遍历返回结果链表中的每一个地址。

4.2 getnameinfo

函数getaddrinfo解决把主机名和服务名转换为套接字地址结构的问题。getnamerinfo函数则相反,它把套接字地址结构转换成主机名和服务名。

#include <netdb.h>

# define NI_NUMERICHOST  1	/* 以数串格式返回主机字符串 */
# define NI_NUMERICSERV   2	/* 以数串格式返回服务字符串 */
# define NI_NOFQDN       4	/* 只返回QFDN的主机名部分 */
# define NI_NAMEREQD     8	/* 若不能从地址解析出名字则返回错误 */
# define NI_DGRAM        16	/* 数据报服务  */

//返回:成功返回0,出错返回非0
int getnameinfo(struct sockaddr *sockaddr, socklen_t addrlen   /* 套接字地址结构和长度 */
char *host, socklen_t hostlen          /* 主机字符串 */
char *serv, socklen_t servlen          /* 服务字符串 */
int flags);                            /* 标志  */

参数sockaddr指向套接字地址结构,addrlen为该套接字地址结构长度,通常由accept、recvfrom、getsockname或getpeername返回。字符串host和serv用于接收主机名和服务,当对应参数长度hostlen、servlen设置为0表示不想返回对应字符串。例如,仅希望返回主机名,可以调用getnameinfo((struct sockaddr *)&addr, sizeof(addr), hbuf, hbuflen, NULL, 0, flags);。类似gethostbyaddr函数使用,给出一个示例代码如下:

void main()
{
  char host[] = "127.0.0.1";

  char hbuf[NI_MAXHOST]; 
  char sbuf[NI_MAXSERV];

  int hbuflen = sizeof(hbuf);
  int sbuflen = sizeof(sbuf);

  sockaddr_in addr;
  addr.sin_family = AF_INET;
  inet_pton(AF_INET, host, &addr.sin_addr);
  addr.sin_port = htons(8080);

  
  int flags = 0;  
  flags |= NI_DGRAM;

  int ret = getnameinfo((struct sockaddr *)&addr, sizeof(addr),
                        hbuf, hbuflen, sbuf, sbuflen, flags);

  if (ret < 0){
    printf("getnameinfo error: %s\n", gai_strerror(ret)); // getnameinfo error
    return;
  }

  printf("hbuf: %s, sbuf: %s\n", hbuf, sbuf);
}

结果为
在这里插入图片描述

5 其他

5.1 可重入

本章节中使用到的函数gethostbyname、gethostbyaddr、getservbyname和getservbyport这四个函数都是不可重入的,由于他们都在内部返回一个静态结构的指针。

getaddrinfo和getnameinfo可重入的前提是由它调用的函数都可以重入,因为动态分配是内部完成。

同样的,在套接字函数中的错误信息errno变量也存在类似问题,每个进程有一个该变量的副本,在多线程中使用错误混乱。通常先将全局erron的值保存,调用相关函数后再回复其值。

地址转换中,inet_pton和inet_ntop总是可重入的,而inet_ntoa是不可重入的。

本章节中,提供了一些可重入的版本函数,函数名增加“_r”后缀,例如gethostbyname_r、gethostbyaddr_r、getservbyname_r和getservbyport_r。不同的平台、不同操作系统版本,可能提供的实现方式不同。这里以WSL下ubuntu16.04为例,说明gethostbyname_r函数:

//返回:成功返回0,出错返回非0
int gethostbyname_r (const void *hostname,   // 主机名
			    struct hostent *hostbuf,     // 地址结构缓冲区
			    char *buf, size_t buflen,    // 缓冲区地址和长度
			    struct hostent ** res,       // 地址结构链表
			    int * h_errnop);             // 错误指针

相对于 struct hostent *gethostbyname (const char *hostname), 参数增加了多个,主要是需要预先分配保存hostent结构链表的缓冲区。

void main()
{
  char host[] = "www.taobao.com";
  printf("check host: %s\n\n", host);

  struct hostent hostbuf, *res = NULL;
  char buf[8192] = {0};
  int err = 0;

  if( gethostbyname_r(host, &hostbuf, buf, sizeof(buf), &res, &err) < 0){
    printf("gethostbyname_r error from \"%s\": %s\n", host, hstrerror(err));
    return;
  }

  printf("official name: %s\n", res->h_name);

  char **pptr;
  for (pptr = res->h_aliases; *pptr != NULL; pptr++){
    printf("  alias: %s\n", *pptr);
  }
  char str[16];
  for (pptr = res->h_addr_list; *pptr != NULL; pptr++){
    printf("address: %s\n", inet_ntop(res->h_addrtype, *pptr, str, 16));
  }
}

运行结果完全同 gethostbyaddr()函数。

5.2 作废的ipv6解析

gethostbyname(const char* hostname)仅支持IPv4地址的解析,存在支持IPv6的地址解析函数,在函数名后增加了”2”的后缀,例如gethostbyname2(const char* hostname, int af)

其他函数在这里不做说明。建议直接使用getaddrinfo函数。

5.3 获取本机的局域网地址

前面的方法获取本机地址多为回环地址127.0.0.1。我们在需要外其他机器提供服务时,就必须需要知道本机在局域网中的ip地址。

有以下方法:

(1)创建socket,和外部UDP或TCP服务端建立连接,之后使用getsockname获取本机ip地址

(2)使用ioctl()函数以及结构体 struct ifreq和结构体struct ifconf来获取网络接口的各种信息。给出指定网卡名称eth0,返回其绑定的ip。

void main()
{
   // https://www.cnblogs.com/baiduboy/p/7287026.html
  int inet_sock;  
  ifreq ifr;  
  char ip[46]={};  

  inet_sock = socket(AF_INET, SOCK_DGRAM, 0);  
  strcpy(ifr.ifr_name, "eth0");     // 指定网卡名称
  ioctl(inet_sock, SIOCGIFADDR, &ifr);  
  inet_ntop(AF_INET, &(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr), ip, sizeof(sockaddr_in));
    
  printf("%s IP Address %s\n", ifr.ifr_name, ip);
}

在这里插入图片描述

(3)使用getifaddrs() 函数遍历网卡绑定的ip地址

#include <stdio.h>
#include <errno.h>
#include <cstring>
#include <ifaddrs.h>
void main()
{
  ifaddrs *ifAddrStruct = NULL;
  
  if (getifaddrs(&ifAddrStruct) < 0){
    printf("getifaddrs failed. %s", strerror(errno));
  }

  for (ifaddrs *ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
    if (!ifa->ifa_addr) {
      continue;
    }
    if (ifa->ifa_addr->sa_family == AF_INET) { // check it is IP4
      // is a valid IP4 Address
      void* tmpAddrPtr= &((sockaddr_in *)ifa->ifa_addr)->sin_addr;
      char addressBuffer[INET_ADDRSTRLEN];
      inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN);
      printf("%s IP Address %s\n", ifa->ifa_name, addressBuffer); 
    } else if (ifa->ifa_addr->sa_family == AF_INET6) { // check it is IP6
      // is a valid IP6 Address
      void* tmpAddrPtr = &((sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;
      char addressBuffer[INET6_ADDRSTRLEN];
      inet_ntop(AF_INET6, tmpAddrPtr, addressBuffer, INET6_ADDRSTRLEN);
      printf("%s IP Address %s\n", ifa->ifa_name, addressBuffer); 
    } 
  }
 
  freeifaddrs(ifAddrStruct);
}

先使用命令行ip addr查询本机的所有网卡ip地址信息:
在这里插入图片描述
运行结果如下,同样可以在循环中根据网口名称、地址族协议,获取指定的IP数据。
在这里插入图片描述

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值