套接字地址

IP地址是一个32bits无符号整数。网络程序将IP地址存放在IP地址结构中:

#include <netinet/in.h>

/* Internet address structure */
struct in_addr{
    unsigned int s_addr;  /* Network byte order (big-endian) */
};

因为因特网主机可以有不同的主机字节顺序,TCP/IP为任意整数数据项定义了统一的网络字节顺序(network byts order)(big-endian),Unix提供了以下函数用于在网络和主机字节顺序间实现转换:

#include <netinet/in.h>

// 返回按照网络字节顺序的值
unsigned long int htonl(unsigned long int hostlong); // 32bits IP地址
unsigned short int htons(unsigned short int hostshort); // 16bits 端口号

// 返回按照主机字节顺序的值
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netlong);

IP地址通常以点分十进制来表示,每个字节由它的十进制表示,并用句点和其他字节分开。因特网程序使用inet_atoninet_ntoa函数实现IP地址和点分十进制串之间的转换:

注意inet_ntoa以结构体为参数而不是指针

#include <arpa/inet.h>

// 成功返回1,失败返回0
// 点分十进制串cp --> struct in_addr *inp
int inet_aton(const char *cp, struct in_addr *inp);

// 成功返回指向点分十进制串的指针
char *inet_ntoa(struct in_addr in);

因特网定义了域名集合与IP地址集合之间的映射,这个映射通过DNS来维护。从概念上而言,DNS数据库有上百万个如下所示的主机条目结构(host entry structure)组成,其中每条定义了一组域名(一个官方名字和一组别名)和一组IP地址间的映射。

/* DNS host entry structure */
struct hostent {
    char *h_name; // Official domain name of host
    char **h_aliases; // Null-terminated arrar of domain names
    int h_addrtype; // Host address type (AF_INET)
    int h_length; // Length of an address, in bytes
    char **h_addr_list; // Null-terminated array of in_addr structs
}

注意其将IP地址结构体当做字符串,我猜这样做可能是因为需要按字节大小处理IP地址

因特网应用程序通过调用gethostbynamegethostbyaddr函数,从DNS数据库中检索任意的主机条目。

#include <netdb.h>

// 成功返回非NULL指针,若出错返回NULL指针,同时设置h_errno
struct hostent *gethostbyname(const char *name); // 注意 name 是域名,不是点分十进制串
struct hostent *gethostbyaddr(const char *addr, int len, 0);

从Unix程序的角度来看,套接字就是一个有相应描述符的打开文件。因特网的套接字地址存放在如下所示的类型为sockaddr_in的16bytes结构中。

#include <netinet/in.h>

/* Internet-style socket address structure */
struct sockaddr_in{
    unsigned short sin_family; // Address family (always AF_INET)
    unsigned short sin_port; // Port number in network byte order
    struct in_addr sin_addr; // IP address in network byte order
    unsigned char sin_zero[8]; // Pad to sizeof(struct sockaddr)
}

在作为参数传入一个套接字函数时,套接字地址结构总是以引用形式(指向该结构的指针)来传递的,但不同协议族的套接字地址结构类型可能不同。函数如何声明套接字地址结构的指针类型因此成为一个问题。为了解决这个问题,定义了通用套接字地址结构sockaddr

#include <sys/socket.h>

/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr{
    unsigned short sa_family; // Protocol family
    char sa_data[14]; // Address data
}

在ANSI C出现之后,可以定义通用的指针类型void *,但套接字函数是在ANSI C之前定义的

套接字接口

以下为一对TCP客户与服务器进行之间进行交互的典型流程:

Client process                 Server process

   socket                         socket
     |                              |
     |                              |
     |                              v
     |                             bind
     |                              |
     |                              |
     |                              v
     |                            listen
     |                              |
     |                              |<-----------------------------------------------+
     |                              v                                                ^
     |                           accept                                              |
     |                              |                                                |
     |                              v                                                |
     |                           一直阻塞到                                          |
     |                          客户端连接到达                                       |
     |                              |                                                |
     v       Connection request     |                                                |
  connect ------------------------->+                                                |
     |       (TCP三路握手)          |                                                |
     |                              |                                                |
     v     Client sends requset     v    Server processes request   ____________     |
   write ========================> read ==========================> | Resource |     |
     |                              |      and requset source       |__________|     |
     |                              |                               |__________|     |
     |                              |                               |__________|     |
     |                              |                              //                |
     |                              |        +====================+                  |
     v    Server sends respense     v       // get the source file                   |
   read <======================== write <==+                                         |
     |                              |                                                |
     |                              |                                                |
     v            EOF               v                                                |
   close ------------------------> read                                              |
                                    |                                                |
                                    v     Await connection request from next client  |
                                   close ------------------------------------------->+

创建套接字

客户端和服务器使用socket函数来创建一个套接字描述符(socket descriptor)。

#include <sys/socket.h>

// 成功返回非负描述符,失败返回-1
int socket(int family, int type, int protocol);
/*
 * 创建一个套接字描述符
 * AF_INET 表明我们正在使用IPv4协议
 * SOCK_STREAM 表明这个套接字是字节流套接字(TCP)
 * protocol是0表示选择给定family和type组合的系统默认值
 */

socketfd = socket(AF_INET, SOCK_STREAM, 0);

socket返回的套接字描述符仅是部分打开的,还不能用于读写。如何完成打开套接字的工作,取决于我们是客户端还是服务器。

connect(客户端)

客户端通过调用connect函数来建立和服务器的连接:

#include <sys/socket.h>

// 若成功返回0,出错返回-1
int connect(int sockfd, const struct sockaddr *serv_addr, int addrlen);

connect函数试图与套接字地址为serv_addr的服务器建立一个因特网连接,其中addrlensizeof(sockaddr_in)connect函数仅在连接成功建立或是发生错误时才返回。如果成功,sockfd描述符现在就准备好读写了,即使用Unix I/O函数与服务器通信。

connect失败则该套接字不可再用,必须关闭,不能对这样的套接字再次调用connect函数。

包装socketconnectopen_clientfd:

/*
 * 创建socket描述符并与运行在hostname上的服务器(知名端口)建立一个连接
 * 若成功则返回一个打开的套接字描述符
 * 若Unix出错则返回-1
 * 若DNS出错则返回-2
 */

typedef struct sockaddr SA;   // Generic socket address structure

int open_clienfd(char *hostname, int port)
{
    int clientfd;
    struct hostent *hp;
    struct sockaddr_in serveraddr;

    if ( (clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        return -1; // Check errno for cause of error
    }

    /* Fill in the server's IP address and port */
    if ( (hp = gethostbyname(hostname)) == NULL) {
        return -2; // Check h_errno for cause of error
    }
    bzero((char *)&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    bcopy((char *)hp->h_addr_list[0], (char *)&serveraddr.sin_addr.s_addr, hp->h_length);
    serveraddr.sin_port = htons(port);

    /* Establish a connection with the server */
    if (connect(clientfd, (SA *)&serveraddr, sizeof(sockaddr_in)) < 0) {
        return -1;
    }
    return clientfd;
}

bind、listen、accept(服务器)

服务器使用bindlistenaccept和客户端建立连接。

#include <sys/socket.h>

// 成功返回0,失败返回-1
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
int listen(int sockfd, int backlog);

bind函数告诉内核将my_addr中的服务器套接字地址与套接字描述符sockfd联系起来,参数addrlensizeof(sockaddr_in)

listen函数将sockfd从一个主动套接字转换为一个监听套接字(listening socket),该套接字可以接受来自客户端的连接请求。backlog参数规定了内核应该为相应套接字排队的最大连接个数。

包装socketbindlistenopen_listenfd:

/*
 * 打开和返回一个监听描述符,这个描述符准备好在知名端口port上接收连接请求
 * 若成功返回描述符
 * 若Unix出错返回-1
 */

#define LISTENQ 1024
typedef struct sockaddr SA;   // Generic socket address structure

int open_listenfd(int port){
    int listenfd, optval=1;
    struct sockaddr_in serveraddr;

    /* Create a socket descriptor */
    if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        return -1;
    }

    /*
     * Eliminates "Address already in use" error from bind
     * 使服务器可以立即终止和重启,默认地,一个重启的服务器将
     * 在大约30秒内拒绝客户端的连接请求
     */
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
                   (const void *)&optval, sizeof(int)) < 0) {
        return -1;
    }

    /* Listenfd will be an end point for all requsts to port
       on any IP address for this host */
    bzero((char *)&serveraddr, sizeof(serveraddr);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.saddr = htonl(INADDR_ANY);  // INADDR_ANY表示通配地址,即让内核选择IP地址(wildcard)
    serveraddr.sin_port = htons((unsigned short)port);
    if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0) {
        return -1;
    }

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

服务器通过调用accept函数来等待来自客户端的连接请求。

#include <sys/socket.h>>

// 成功则返回非负描述符,出错返回-1
int accept(int listenfd, struct sockaddr *cliaddr, int *addrlen);

accept函数等待来自客户端的连接请求到达监听描述符listenfd,然后在cliaddr中填写客户端的套接字地址,并返回一个已连接描述符(connected desctptor)(或者说:从已完成连接队列头返回下一个已完成连接)。这个描述符可被用来利用Unix I/O函数与客户端通信。

cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。addrlen为值-结果参数:调用前,我们将由addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。

accept返回的描述符为内核自动生成的一个全新描述符,而不是listenfd。一个服务器通常仅仅创建一个监听描述符,它在服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。

关闭套接字

通过调用Unix close关闭(连接)描述符:

#include <unisted.h>

// 若成功返回0,若出错则返回-1
int close(int sockfd);