UNP 学习笔记 #11 名字与地址转换

11.1 概述

用IP地址数值表示的主机, 端口标识服务器, 不容易记住, 而用名字表示比较容易记住. 另外, 数值地址可以变动, 而名字保持不变, 而且手工键入IP地址也很容易出错. 这就引入了域名系统 - 名字与地址间的转换.
名字和数值地址间进行转换的函数, 也是本章主要内容:
gethostbyname, gethostbyaddr 在主机名字和IPv4地址之间进行转换;
getservbyname, getservbyport 在服务名字和端口号之间进行转换;

2个协议无关的转换函数:
getaddrinfo, getnameinfo 分别用于主机名字和IP地址之间以及服务名字和端口号之间的转换.


11.2 域名系统

域名系统 Domain Name System, DNS, 主要用于主机名字与IP地址之间的映射.
主机名可以是简单名字(simple name), 如solaris, 或全限定域名(Fully Qualified Domain Name, FQDN), 如solaris.unpbook.com

11.2.1 资源记录

DNS条目称为资源记录(resource record, RR). 感兴趣的RR类型:

  • A
    A记录把一个主机名映射成一个32bit IPv4地址.
    i.g. freedsb的4个DNS记录中, 第1个是A记录:
freebsd IN A      12.106.32.254
        IN AAAA   3ffe:b80:1f8d:1:a00:20ff:fea7:686b
        IN MX     5  freebsd.unpbook.com.
        IN MX     10 mailhost.unpbook.com.
  • AAAA "四A"
    四A(quad A)记录把一个主机名映射成一个128bit的IPv6地址. 选择称呼"四A"是由于128bit是32bit的4倍.

  • PTR "指针记录"
    指针记录(pointer record)的PTR记录把IP地址映射成主机名.
    对于IPv4地址, 32bit地址的4byte先反转顺序, 每个byte都转换成各自的十进制ASCII值(0~255)后, 再添加"in-addr.arpa", 结果字符串用于PTR查询.
    对于IPv6地址, 128bit中每4bit一组 (共32组) 先反转顺序, 然后每4bit组转换成对应十六进制ASCII值(0~f)后, 再添加"ip6.arpa".
    i.g. A记录freebsd IN A 12.106.32.254, 对应PTR记录为254.32.106.12.in-addr.arpa
    AAA记录 IN AAAA 3ffe:b80:1f8d:1:a00:20ff:fea7:686b, 对应PTR记录为b.6.8.6.7.a.e.f.f.f.0.2.0.0.a.0.1.0.0.0.d.8.f.1.0.8.b.0.e.f.f.3.ip6.arpa

  • MX
    MX记录把一个主机指定作为给定主机的"邮件交换器(mail exchanger)". 上面A记录下面2个MX记录, 优先级分别5, 10, 值越小优先级越高. 存在多个MX记录时, 按优先级顺序使用.

  • CNAME
    CNAME代表"canonical name(规范名字)", 常用于为常用服务(如ftp, www)指定CNAME记录. 如果使用服务名而非真实主机名, 那么相应服务挪到另外一个主机时, 也不必知道.
    i.g. 名为linux的主机有2个CNAME记录:

ftp  IN  CNAME  linux.unpbook.com
www  IN  CNAME  linux.unpbook.com

11.2.2 解析器和名字服务器

每个组织机构都会运行一个或多个名字服务器, 即BIND(Berkeley Internet Name Domain简称)的程序. 书中的客户和服务器等应用程序通过调用称为解析器(resolver)的函数库函数接触DNS服务器.
常用的解析器中的函数有诸如gethostbyname, gethostbyaddr等.

应用进程、解析器和名字服务器之间典型关系:

解析器读取系统相关配置文件, 确定本组织机构的名字服务器的所在位置, 处于可靠性和冗余目的, 通常会设置多个名字服务器.
文件/etc/resolv.conf通常包含本地名字服务器主机的IP地址

$ cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0
search DHCP HOST

如何从其他名字服务器得知服务器的名字?
解析器使用UDP向本地名字服务器发出查询, 如果本地名字服务器不知道答案, 通常就会使用UDP在整个因特网上查询其他名字服务器. 如果答案太长, 超出UDP消息承载能力, 本地名字服务器和解析器就会自动切换到TCP.

11.2.3 DNS替代方法

不使用DNS获取名字+地址信息, 常用替代方法: 静态主机(/etc/hosts 文件)、网络信息系统(Network Information System, NIS)及轻权目录访问协议(Lightweight Directory Access Protocol, LDAP).
缺点: 系统管理员如何配置一个主机以使用不同类型的名字服务是实现相关的.
不过, 这些差异对应用程序开发人员来说, 通常是透明的, 只需要调用诸如gethostbyname, gethostbyaddr这样的解析器函数.


11.3 gethostbyname

查找主机的所有IPv4地址, 正式名称/规范名字(official name), 别名(alias name). 只能返回IPv4地址, 不能返回IPv6地址.
而处理IPv6需要用getaddrinfo, 随着IPv6的普及, 因此推荐使用getaddrinfo, 不推荐使用gethostbyname.

#include <netdb.h>
extern int h_errno;

struct hostent *gethostbyname(const char *name);
  • 返回值
    成功, 返回非空指针; 出错为NULL且设置h_errno

返回非空指针指向如下hostent结构:

struct hostent {
   char  *h_name;            /* official name of host */
   char **h_aliases;         /* alias list */
   int    h_addrtype;        /* host address type */
   int    h_length;          /* length of address */
   char **h_addr_list;       /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */

gethostbyname执行的是对A记录的查询, 因而只能返回IPv4地址.
这些字段中, 所查询主机的正式主机名(official host)和所有别名(alias)都是以空字符结尾的C字符串.

返回的h_name 就是所查询主机的规范名字(canonical name).

  • 错误处理
    发生错误时, gethostbyname 不会设置errno变量, 而是置全局整型变量h_errno为<netdb.h>中定义的值之一:
    HOST_NOT_FOUND;
    TRY_AGAIN;
    NO_RECOVERY;
    NO_DATA (<=> NO_ADDRESS);

NO_DATA表示指定的名字有效, 但没有A记录. 用hstrerror函数, 可以将h_errno值作为参数, 返回调用的错误信息.

if ((hptr = gethostbyname(ptr)) == NULL) {
    // 这里不要使用perror, 因为gethostbyname不会设置errno值

    fprintf(stderr, "gethostbyname error: %s\n", hstrerror(h_errno));
}
  • 例子
    命令行任意参数作为主机名, 调用gethostbyname 显示主机对应IPv4地址信息
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

extern int h_errno;

int main(int argc, char *argv[])
{
    char *ptr, **pptr;
    char str[INET_ADDRSTRLEN]; /* 存放IPv4字符串, 最少16个byte */
    struct hostent *hptr;

    while(--argc > 0) {
        ptr = *++argv;
        if ((hptr = gethostbyname(ptr)) == NULL) {
            // perror("gethostbyname error");
            fprintf(stderr, "gethostbyname errnor: %s\n", 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:
            fprintf(stderr, "unknown address type\n");
            break;
        }
    }

    return 0;
}

运行结果, 参数是主机名, 我的电脑名称是martin-thinkpad-t480

$ ./a.out martin
gethostbyname errnor: Unknown host
$ ./a.out martin-thinkpad-t480 localhost
official hostname: martin-ThinkPad-T480
	address: 127.0.1.1
official hostname: localhost
	address: 127.0.0.1
$ ./a.out baidu
official hostname: baidu.HOST
	address: 66.206.3.38
$ ./a.out 163
official hostname: 163
	address: 0.0.0.163
./a.out 163.com
official hostname: 163.com
	address: 123.58.180.8
	address: 123.58.180.7
$ ./a.out linux.unpbook.com
official hostname: linux.unpbook.com.HOST
	address: 136.243.78.216

11.4 gethostbyaddr

试图由一个二进制IP地址找到相应的主机名, 与gethostbyname行为相反

#include <netdb.h>
#include <sys/socket.h>       /* for AF_INET */

struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
  • 功能
    按DNS, gethostbyaddr在in_addr.arpa域中向一个名字服务器查询PTR记录

  • 参数
    addr 实际上是指向存放IPv4地址的某个in_addr 结构的指针
    len 是addr结构的大小, 对于IPv4地址 len = 4
    type 应为AF_INET

  • 返回值
    成功, 返回非空指针; 出错为NULL且设置h_errno


11.5 getservbyname, getservbyport

getservbyname
getservbyname根据给定名字查找相应服务.
服务也可以靠名字来认知, 如果在程序中通过其名字而非端口号指代一个服务, 而且从名字到端口号的映射关系保存在一个文件中(通常为/etc/services), 那么即使端口号变动, 我们仅需要修改/etc/services文件中某一行, 而不必重新build程序.

#include <netdb.h>

struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
  • 参数
    servname 服务名, 必须指定. 如果同时指定协议(proto), 那么要求指定服务必须有匹配的协议, 否则查找失败. 如果未指定协议(proto = NULL), 而servname指定服务支持多个协议, 那么返回哪个端口号取决于具体实现. 不过, 这种情况选择无关紧要, 因为支持多个协议的服务往往使用相同的TCP端口号和UDP端口号. (这点没有保证)
    proto 协议

  • 返回值
    成功, 返回非空指针; 出错返回NULL.
    返回的非空指针指向如下servent结构:

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

servent结构中, 最关心的主要字段是端口号.
典型调用如下:

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, tftp仅支持UDP.

本地查看方式:

$ cat /etc/services

查询某个服务名对应端口号, 如查看telnet:

$ grep  ^telnet /etc/services 
telnet		23/tcp
telnets		992/tcp				# Telnet over SSL

脱字符(^)表示文本行首, 参见 Shell编程中脱字符(^)的用法 | CSDN. 有时也用grep扩展命令$ grep -e ...


getservbyport
getservbyport根据指定端口号和可选协议, 查找相应的服务.

#include <netdb.h>

struct servent *getservbyport(int port, const char *proto);
  • 参数
    port 端口号, 必须为网络字节序(大端). 如果不是, 则可调用htons转换

  • 返回值
    成功, 返回非空指针; 出错, 返回NULL

典型调用:

struct servent *sptr;

sptr = gethostbyport(htons(53), "udp");  /* DNS using UDP */
sptr = gethostbyport(htons(21), "tcp");  /* FTP using TCP */
sptr = gethostbyport(htons(21), NULL);   /* FTP using TCP */
sptr = gethostbyport(htons(21), "udp");  /* this call will fail */

注: UDP上没有服务使用端口21, 最后一个调用失败

  • shell命令查看指定端口号的服务
$ grep 514 /etc/services 
shell		514/tcp		cmd		# no passwords used
syslog		514/udp
syslog-tls	6514/tcp			# Syslog over TLS [RFC5425]

11.6 getaddrinfo

getaddrinfo 能处理名字到地址, 服务到端口, 这两种转换. 支持IPv4, IPv6, 可替换只支持IPv4的 gethostbyname, gethostbyaddr.
getaddrinfo返回的是一个sockaddr结构, 而非地址列表. sockaddr结构随后可由socket函数直接使用, 如此一来, getaddrinfo 函数就把协议相关性完全隐藏在该库函数内部. 而应用程序只需要处理由getaddrinfo返回的套接字地址结构.

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
  • 参数
    node hostname主机名, 与service一起标识一个Internet主机和一个服务
    service 服务名
    hints 可以为NULL, 也可以是一个指向addrinfo结构链表的指针, 调用者在该结构中填入关于期望返回的信息类型的暗示. i.e. 如果指定服务既支持TCP, 也支持UDP, 那么调用者可设置hints.ai_socktype = SOCK_DGRAM, 表明只想要数据报套接字的信息.
    addrinfo结构定义:
struct addrinfo {
   int              ai_flags;      /* AI_PASSIVE, AI_CANONNAME */
   int              ai_family;     /* AF_xxx */
   int              ai_socktype;   /* SOCK_xxx */
   int              ai_protocol;   /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
   socklen_t        ai_addrlen;    /* length of ai_addr */
   struct sockaddr *ai_addr;       /* ptr to socket address structure */
   char            *ai_canonname;  /* ptr to canonical name for host  */
   struct addrinfo *ai_next;       /* ptr to next structure in linked list */
};

hints结构中调用者可以设置的成员:

调用者可设置hints成员 描述
ai_flags 零个或多个在一起的AI_xxx值;
ai_family 某个AF_xxx值;
ai_socktype 某个SOCK_xxx值;
ai_protocol 0或针对IPv4/IPv6的协议IPPROTO_xxx

对于ai_flags 可用标志值及含义:

ai_flags标志值 含义
AI_PASSIVE 套接字将用于被动打开,node为null,返回的套接字能作bind/accept(作为服务器);没有指定该标志,返回的套接字可以connect/sendto/sendmsg等(作为客户)
AI_CANONNAME 告知getaddrinfo函数返回主机的规范名字
AI_NUMERICHOST 防止任何类型的名字到地址映射, hostname参数必须是一个地址串
AI_NUMERICSERV 防止任何类型的名字到服务映射, service参数必须是一个十进制端口号数串
AI_V4MAPPED 如果同时指定ai_family成员的值为AF_INET6, 那么如果没有可用的AAAA记录, 就返回与A记录对应的IPv4映射的IPv6地址
AI_ALL 如果同时指定AI_V4MAPPED标志, 那么除了返回与AAAA记录对应IPv6地址外, 还返回与A记录对应的IPv4映射的IPv6地址
AI_ADDRCONFIG 按照所在主机的配置选择返回地址类型, 也就是只查找与所在主机回馈接口以外的网络接口配置的IP地址版本一致的地址

如果hints = NULL, 本函数就假设ai_flag, ai_socktype和ai_protocol的值均为0, ai_family = AF_UNSPEC.
如果本函数成功成功(0), 那么结果参数result指向的变量已被填入一个指针, 它指向的是由其中的ai_next成员串接起来的addrinfo结构链表. 可导致返回多个addrinfo结构的情形有以下2个:

  1. 如果与hostname参数关联的地址有多个(多宿主机), 那么适用于所请求的地址族(<= hints.ai_family)的每个地址都返回一个对应的结构;
  2. 如果service参数指定的服务支持多个套接字类型(如同时支持UDP/TCP/SCTP), 那么每个套接字类型都可能返回一个对应的结构, 具体取决于hints结构的ai_socktype成员;

注意:

  1. 当有多个addrinfo结构返回时, 先后顺序没有保证, 不能假定TCP服务总先于UDP服务;
  2. addrinfo返回的信息, 可直接用于socket调用, i.e. 可适用于客户的connect/sendto调用, or 服务器的bind调用;

如果hints.ai_flags = AI_CANONNAME, 那么本函数返回第一个addrinfo结构的ai_canonname成员指向所查找主机的规范名字(FQDN). 这样即使用户给定的是一个简单名字或别名, 也能搞清真正的名字.
获取指定主机规范名字的典型调用:

struct addrinfo hints, *res;

bzero(&hints, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_INET;

getaddrinfo("freebsd4", "domain", &hints, &res);

根据指定的service服务名(也可以是十进制端口号数串)和hints.ai_socktype(暗示信息), 为每个主机名查找获得的IP地址返回addrinfo结构的数目:

ai_socktype
暗示
仅TCP 仅UDP 仅SCTP TCP和UDP TCP和SCTP TCP、UDP
和SCTP
服务以端
口号标识
0 1 1 1 2 2 3 错误
SOCK_STREAM 1 错误 1 1 2 2 2
SOCK_DGRAM 错误 1 错误 1 错误 1 1
SOCK_SEQPACKET 错误 错误 1 错误 1 1 1

11.7 gai_strerror

gai_strerror将getaddrinfo返回的非0错误号转化为错误信息串.

#include <netdb.h>

const char *gai_strerror(int errcode);

getaddrinfo返回的非0错误常值:

常值 说明
EAI_AGAIN 名字解析中临时无效
EAI_BADFLAGS ai_flags的值无效
EAI_FAIL 名字解析中不可恢复地失败
EAI_FAMILY 不支持ai_family
EAI_MEMORY 内存分配失败
EAI_NONAME hostname或service未提供, 或者不可知
EAI_OVERFLOW 用户参数缓冲区溢出(仅限getnameinfo()函数)
EAI_SERVICE 不支持ai_socktype类型的service
EAI_SOCKTYPE 不支持ai_socktype
EAI_SYSTEM 在errno变量中有系统错误返回

11.8 freeaddrinfo

由getaddrinfo返回的所有存储空间都是动态获取的, 如malloc调用, 包括addrinfo结构、ai_addr结构、ai_canonname字符串. freeaddrinfo用于释放这些存储空间.

#include <netdb.h>

void freeaddrinfo(struct addrinfo *res);
  • 功能
    释放链表中所有的结构, 以及由它们指向的任何动态存储空间.

  • 参数
    res 应指向getaddrinfo返回的第一个addrinfo结构


11.9 getaddrinfo : IPv6

getaddrinfo在处理两个不同的输入: 套接字地址结构类型, 调用者期待返回的地址结构符合该类型; 资源记录型, 在DNS或其他数据块中执行的查找符合该类型.
调用者指定地址族hints.ai_family:
1)AF_INET getaddrinfo就不能返回任何sockaddr_in6结构;
2)AF_INET6 getaddrinfo就不能返回任何sockaddr_in结构;
3)AF_UNSPEC getaddrinfo适用于指定主机名和服务名且适合任意协议族的地址. 意味着, AAAA记录 -> sockaddr_in6结构返回, A记录 -> sockaddr_in结构返回.

调用者指定ai_flags = AI_PASSIVE但没有指定主机名, 那么IPv6通配地址(IN6ADDR_ANY_INIT或0::0) -> sockaddr_in6结构返回, IPv4通配地址(INADDR_ANY或0.0.0.0) -> sockaddr_in结构返回.


11.10 getaddrinfo: 例子


11.11 host_serv 自定义函数

自定义函数host_serv用于获取第一个链表节点(包含主机规范名称). 不要求调用者分配并设置hints结构, 而是只用关心地址族和套接字类型2个字段.
既想获取主机, 又想获取服务信息, 又想自己建立连接, 而又不想关注struct addrinfo和getaddrinfo细节时, 可以调用host_serv.

struct addrinfo *host_serv(const char *hostname, const char *service, int family, int socktype);

实现源码

struct addrinfo *host_serv(const char *hostname, const char *service, int family, int socktype)
{
    int n;
    struct addrinfo hints, *res;

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

    if ((n = getaddrinfo(hostname, service, &hints, &res) != 0)) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(n));
        return NULL;
    }
    
    return res;
}

11.12 tcp_connect 自定义函数

自定义函数tcp_connect 使用getaddrinfo, 创建一个TCP套接字并连接到服务器.
客户想通过服务器名称和服务名, 查找并连接服务器指定服务时, 可调用tcp_connect.

int tcp_connect(const char *hostname, const char *service);

实现源码

int tcp_connect(const char *hostname, const char *service)
{
    int sockfd, n;

    struct addrinfo hints, *res, *resave;

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

    if ((n = getaddrinfo(hostname, service, &hints, &res)) != 0) {
        fprintf(stderr, "tcp_connect error for %s, %s: %s\n", hostname, service, gai_strerror(n));
        exit(1);
    }
    resave = res;

    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sockfd < 0)
            continue;

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

        if (close(sockfd) < 0) {
            perror("close error");
        }
    }while ((res = res->ai_next) != NULL);

    if (res == NULL) {
        fprintf(stderr, "tcp_connect error for %s, %s\n", hostname, service);
    }

    freeaddrinfo(resave);

    return sockfd;
}

11.13 tcp_listen 自定义函数

自定义函数tcp_listen, 用getaddrinfo, 创建socket, 并进行bind, listen操作.
服务器端调用, 捆绑本地通配地址, 取得多宿主机所有IP地址, 并尝试依次创建并bind地址, 直到有一个成功, 然后设置监听队列.

int tcp_listen(const char *hostname, const char *service, sockelen_t *addrlenp);

实现源码
addrinfo结构提供函数信息:
AI_PASSIVE 因为本函数供服务器使用;
AF_UNSPEC 地址族, 不限制IP地址类型;
SOCK_STREAM 适用于TCP;

AI_PASSIVE, AF_UNSPEC将会返回2个套接字地址结构: 第一个IPv6的, 第二个IPv4的.
设置REUSEADDR选项, 目的在于避免一次bind失败后, socket不可重用(socket状态可能会被bind改变).

#define LISTENQ       10

int tcp_listen(const char *hostname, const char *service, socklen_t *addrlenp)
{
    int listenfd, n;
    int ret;
    const int on = 1;
    struct addrinfo hints, *res, *resave;

    /* set protocol to find out */
    bzero(&hints, sizeof(hints));
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = AF_UNSPEC;     /* IPv4, IPv6 */
    hints.ai_socktype = SOCK_STREAM; /* TCP */
    
    /* get addrinfo linked list */
    if ((n = getaddrinfo(hostname, service, &hints, &res)) != 0) {
        fprintf(stderr, "tcp_listen error for %s, %s: %s\n", hostname, service, gai_strerror(n));
        exit(1);
    }   
    resave = res;


    /* traverse the linked list until success to create a socket for bind, listen */
    do {
        listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (listenfd < 0)
            continue;

        /* set REUSEADDR option to re-bind using the same socket fd */
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

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

        close(listenfd);
    } while((res = res->ai_next) != NULL);

    if (res == NULL) {
        fprintf(stderr, "tcp_listen error for %s, %s\n", hostname, service);
        exit(1);
    }   
    
    ret = listen(listenfd, LISTENQ);
    if (ret < 0) {
        perror("listen error");
        exit(1);
    }   

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

    /* free addrinfo linked list */
    freeaddrinfo(resave);

    return listenfd;
}

11.14 udp_client 自定义函数

自定义函数udp_client, 用于创建未连接UDP套接字.
client调用udp_client函数, 创建未连接UDP socket. 创建完成socket后, 可以直接调用sendto, recvfrom跟server交互.
注意:

  1. 只有upd sever才会bind地址, client创建未连接socket不会调用connect.
  2. 客户需要保存利用getaddrinfo得到的host地址;
int udp_client(const char *hostname, const char *service, struct sockaddr **saptr, socklen_t *lenp);
typedef struct sockaddr SA;

int
udp_client(const char *hostname, const char *service, SA **saptr, socklen_t *lenp)
{
    int sockfd;
    int n;
    struct addrinfo hints, *res, *resave;

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

    if ((n = getaddrinfo(hostname, service, &hints, &res)) != 0) {
        fprintf(stderr, "udp_client error for %s, %s: %s\n", hostname, service, gai_strerror(n));
        exit(1);
    }
    resave = 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) {
        fprintf(stderr, "udp_client error for %s, %s\n", hostname, service);
    }
    
    *saptr = malloc(res->ai_addrlen);
    memcpy(*saptr, res->ai_addr, res->ai_addrlen);
    *lenp = res->ai_addrlen;

    freeaddrinfo(resave);

    return sockfd;
}

11.15 udp_connect 自定义函数

自定义函数udp_connect, 创建已连接(服务端)的udp套接字.
客户端调用该函数, 用于创建已连接服务端的udp套接字, 套接字会绑定该地址, 并且只能与绑定了此对端的服务端地址进行通信.

注意:

  1. 需要调用connect与服务端进行连接, 不过不会真的进行三次握手, 只绑定套接字与对端地址, 以及检查是否存在可知错误;
  2. 利用连接套接字, 客户只能与已绑定的对端进行通信;
int udp_connect(const char *hostname, const char *service);

实现源码:

/**
 * udp客户创建连接套接字, 如果能根据主机名+服务名成功查找到对端地址, 函数会绑定服务器地址
 * @return 成功, 返回创建的套接字;失败, 返回错误码
 * - 0 成功
 * - < 0 失败
 * @note 连接套接字只能与绑定地址的对端通信
 */
int 
udp_connect(const char *hostname, const char *service)
{
    int sockfd, n;
    struct addrinfo hints, *res, *resave;

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

    if ((n = getaddrinfo(hostname, service, &hints, &res)) != 0) {
        fprintf(stderr, "udp_connect error for %s, %s: %s", hostname, service, gai_strerror(n));
        exit(1);
    }
    
    resave = res;

    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sockfd < 0)
            continue;

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

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

    if (res == NULL) {
        /* no one sockfd has been created successfully and errno set from final connect() */
        fprintf(stderr, "udp_connect error for %s, %s: %s", hostname, service, strerror(errno));
    }

    freeaddrinfo(resave);

    return sockfd;
}

11.17 getnameinfo

getnameinfo是getaddrinfo的互补函数, 以套接字地址为参数, 返回描述其中的主机的一个字符串和描述其中的服务的另一个字符串. 函数以协议无关的方式, 提供这些信息, i.e. 调用者不必关心存放在套接字地址结构中的协议地址的类型, 因为这些细节由本函数自行处理.

#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *addr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags);
  • 参数
    sockaddr 指向一个套接字地址结构, 包含协议地址、端口信息;
    addrlen sockaddr地址结构的长度, 该结构及长度通常由accept, recvfrom, getsockname 或getpeername返回;
    待返回的2个字符串host, serv由调用者预先分配存储空间, 也由调用者负责释放:
    host hostlen 指定主机字符串. hostlen为0, 表示调用者不想关心主机字符串;
    serv servlen 指定服务字符串. servlen为0, 表示调用者不想关心服务字符串;
    flags 标志值, 可用于改变getnameinfo的操作
常值 说明
NI_DGRAM 数据报服务
NI_NAMEREQD 若不能从地址解析出名字, 则返回错误
NI_NOFQDN 只返回FQDN的主机名部分
NI_NUMERICHOST 以数串格式返回主机字符串
NI_NUMERICSCOPE 以数串格式返回范围标识字符串
NI_NUMERICSERV 以数串格式返回服务字符串

sock_ntop和getnameinfo的差别在于: 前者不涉及DNS (sock_ntop利用sockaddr.sa_family区分不同协议, inet_ntop实现地址值到地址字符串的转换 如IPv4 点分十进制, 而进行自定义的函数), 只返回IP地址和端口号的一个可显示版本; 后者通常尝试获取主机和服务的名字.


11.18 可重入函数

之前这篇文章提到过可重入的概念: 对线程安全, 可重入函数, 异步安全的理解
函数的可重入是指,

只有当线程安全函数也可能被信号处理程序调用, 如果信号处理程序的调用也是安全的, 此时, 才能说函数是异步信号安全的(可重入).

可重入 = 异步安全 = 线程安全 + 信号处理函数调用安全

gethostbyname等解析器函数的可重入性
而gethostbyname, gethostbyaddr都访问一个静态数据结构:host, 其定义是

static struct hostent host;  /* result stored here */

struct hostent *
gethostbyname(const char *hostname)
{
    ...
    return &host;
}

如果进程在执行gethostbyname, 产生并捕获一个信号, 在信号处理函数中如果也访问了host变量, 很可能会导致信号处理函数执行完毕回到gethostbyname时, host发生变化, 产生不安全行为.
因此, gethostbyname不是信号安全的, 即不是可重入的. 类似的, 还有gethostbyaddr, getservbyname, getservbyport 这3个函数.
类似的问题, 还有errno (全局变量, 一个进程一个), 系统调用出错时, 会直接设置errno, 那么这样的系统调用是不可重入的.


11.19 gethostbyname_r和gethostbyaddr_r函数

解决gethostbyname和gethostbyaddr不可冲入的办法: 提供_r版本函数, 是可重入的.

不可重入版本函数 可重入版本函数
gethostbyname gethostbyname_r
gethostbyaddr gethostbyaddr_r
getservbyname getservbyname_r
getservbyport getservbyport_r

将诸如gethostbyname类不可重入函数改为可重入函数的方法:

  1. 将不可重入函数填写并返回静态结构的做法, 改为由调用者分配再由可重入函数填写结构.
struct hostent *
gethostbyname_r1(const char *hostname, struct hostent *host)
{
    
    // fill host
    ...
    return host;
}
  1. 由可重入函数调用malloc以动态分配内存空间, 由调用者负责释放动态内存.
    类似于getaddrinfo, 动态分配res内存(struct addrinfo), 由调用者负责freeaddrinfo.
struct hostent *
gethostbyname_r2(const char *hostname )
{
    host = (struct hostent *)malloc(sizeof(struct hostent));
    // fill host
    ...
    return host;
}

void caller_func()
{
    struct hostent *host = gethostbyname_r2();
    free(host);
}
posted @ 2021-06-13 15:57  明明1109  阅读(298)  评论(0编辑  收藏  举报