# 11.4.8 套接字接口的辅助函数

初学时,getnameinfo 函数和套接字接口看上去有些可怕。用高级的辅助函数包装一下会方便很多,称为 open_clientfdopen_listenfd,客户端和服务器互相通信时可以使用这些函数。

# 1. open_clientfd 函数

客户端调用 open_clientfd 建立与服务器的连接。

#include "csapp.h"

int open_clientfd(char *hostname, char *port);

返回: 若成功则为描述符,若出错则为 -1。

open_clientfd 函数建立与服务器的连接,该服务器运行在主机 hostname 上,并在端口号 port 上监听连接请求。它返回一个打开的套接字描述符,该描述符准备好了,可以用 Unix I/O 函数做输入和输出。图 11-18 给出了 open_clientfd 的代码。

我们调用 getaddrinfo,它返回 addrinfo 结构的列表,每个结构指向一个套接字地址结构,可用于建立与服务器的连接,该服务器运行在 hostname 上并监听 port 端口。然后遍历该列表,依次尝试列表中的每个条目,直到调用 socketconnect 成功。如果 connect 失败,在尝试下一个条目之前,要小心地关闭套接字描述符。如果 connect 成功,我们会释放列表内存,并把套接字描述符返回给客户端,客户端可以立即开始用 Unix I/O 与服务器通信了。

注意,所有的代码都与任何版本的 IP 无关。socketconnect 的参数都是用 getaddrinfo 自动产生的,这使得我们的代码干净可移植。

# 2. open_listenfd 函数

调用 open_listenfd 函数,服务器创建一个监听描述符,准备好接收连接请求。

#include "csapp.h"

int open_listenfd(char *port);

返回: 若成功则为描述符,若出错则为 -1。

code/src/csapp.c

int open_clientfd(char *hostname, char *port) {
    int clientfd;
    struct addrinfo hints, *listp, *p;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;  /* Open a connection */
    hints.ai_flags = AI_NUMERICSERV;  /* ... using a numeric port arg. */
    hints.ai_flags |= AI_ADDRCONFIG;  /* Recommended for connections */
    Getaddrinfo(hostname, port, &hints, &listp);

    /* Walk the list for one that we can successfully connect to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol))
            < 0) continue; /* Socket failed, try the next */

        /* Connect to the server */
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
            break; /* Success */
        Close(clientfd); /* Connect failed, try another */
    }

    /* Clean up */
    Freeaddrinfo(listp);
    if (!p) /* All connects failed */
        return -1;
    else    /* The last connect succeeded */
        return clientfd;
}

图 11-18 open_clientfd:和服务器建立连接的辅助函数。它是可重入和与协议无关的

open_listenfd 函数打开和返回一个监听描述符,这个描述符准备好在端口 port 上接收连接请求。图 11-19 展示了 open_listenfd 的代码。

open_listenfd 的风格类似于 open_clientfd。调用 getaddrinfo,然后遍历结果列表,直到调用 socketbind 成功。注意,在第 20 行,我们使用 setsockopt 函数(本书中没有讲述)来配置服务器,使得服务器能够被终止、重启和立即开始接收连接请求。一个重启的服务器默认将在大约 30 秒内拒绝客户端的连接请求,这严重地阻碍了调试。

因为我们调用 getaddrinfo 时,使用了 AI_PASSIVE 标志并将 host 参数设置为 NULL,每个套接字地址结构中的地址字段会被设置为通配符地址,这告诉内核这个服务器会接收发送到本主机所有 IP 地址的请求。

code/src/csapp.c

int open_listenfd(char *port)
{
    struct addrinfo hints, *listp, *p;
    int listenfd, optval=1;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;             /* Accept connections */
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
    hints.ai_flags |= AI_NUMERICSERV;            /* ... using port number */
    Getaddrinfo(NULL, port, &hints, &listp);

    /* Walk the list for one that we can bind to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol))
            < 0) continue; /* Socket failed, try the next */

        /* Eliminates "Address already in use" error from bind */
        Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
                   (const void *)&optval, sizeof(int));

        /* Bind the descriptor to the address */
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break; /* Success */
        Close(listenfd); /* Bind failed, try the next */
    }

    /* Clean up */
    Freeaddrinfo(listp);
    if (!p) /* No address worked */
        return -1;

    /* Make it a listening socket ready to accept connection requests */
    if (listen(listenfd, LISTENQ) < 0) {
        Close(listenfd);
        return -1;
    }
    return listenfd;
}

图 11-19 open_listenfd:打开并返回监听描述符的辅助函数。它是可重入和与协议无关的

最后,我们调用 listen 函数,将 listenfd 转换为一个监听描述符,并返回给调用者。如果 listen 失败,我们要小心地避免内存泄漏,在返回前关闭描述符。