CSAPP 第十一章 网络编程

网络应用由一个服务器进程和一个或者多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为客户端提供服务。

1. 网络模型

物理上而言,网络是一个按照地理远近组成的层次系统。最低层次是LAN(本地局域网),LAN最流行的技术是以太网

早期,多个主机通过双绞线和集线器相连,和同一个集线器相连的主机被称为以太网段。集线器是物理层设备,作用是将端口中收到的数据帧不加分辨地广播到其余所有端口。

在集线器的基础上,一个网桥可以将两个以太网段互联成较大的局域网,称为桥接以太网(bridged Ethernet)。相比于集线器,网桥属于链路层设备,其内动态维护了端口转发表。

事实上,目前集线器和网桥均已被淘汰。广泛使用的是交换机

在局域网的基础上,路由器将多个不兼容的局域网互连起来,组成一个internet(互联网络)。

internet指一般概念上的互联网络,开头大写字母的Internet指的是全球IP因特网。

为了解决数据从源主机跨网络发送数据到目标主机的问题,在主机和路由器上运行的协议软件主要解决下面两个问题

  • 命名机制: 不同的局域网技术有不同的主机地址分配方式。互联网络协议通过定义一种统一的主机地址格式消除这种差异。
  • 传送机制: 互联网络协议通过定义一种把数据分片打包为包头和有效载荷的统一方式消除不同局域网络的差异。

2. 全球IP因特网

IP地址

IP地址就是一个32位的无符号整数,按大端法传送。

1
2
3
4
5
6
7
8
9
10
struct in_addr {
    uint32_t s_addr;
}

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

IP地址通常使用点分十进制表示,inet_ptoninet_ntop可以用于IP地址和点分十进制串之间的转换。

1
2
3
4
5
#include <arpa/inet.h>
int inet_pton(AF_INET, const char *src, void *dst);
// Returns: 1 if OK, 0 if src is invalid dotted decimal, −1 on error
const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);
// Returns: pointer to a dotted-decimal string if OK, NULL on error

因特网域名

域名是一串用句点分割的单词。域名集合构成了一个层次结构,每个域名编码了它在这个层次中的位置。

因特网定义了域名集合和IP地址集合间的映射。目前这些映射由分布在世界上的DNS(Domain Name System)数据库维护。

因特网连接

客户端和服务器间的连接是点对点、全双工的。连接的一个端点是一个套接字,每个套接字都有相应的套接字地址。

  • 套接字地址: (地址:端口)
    • 因特网地址
    • 16位的整数端口
  • 连接: (cliaddr:cliport, servaddr:servport)

3. 套接字接口

套接字地址

因特网的套接字地址存放在sockaddr_in的16字节结构中。

注意: 可能还有其它互联网络的套接字地址,这些套接字地址在被传送给connect, bind, accept函数前都需要先强制转换成通用的sockaddr类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* IP socket address structure */
struct sockaddr_in {
uint16_t sin_family; /* Protocol family (always AF_INET) */
uint16_t 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) */
};

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

socket函数

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// Returns: nonnegative descriptor if OK, −1 on error

socket函数返回的clientfd描述符只是部分打开的,还不能用于读写。

例如,socket(AF_INET, SOCK_STREAM, 0);

  • AF_INET表示正在使用32位IP地址
  • SOCK_STREAM表示这个套接字是连接的一个端点

注意: socket的各个参数最好使用getaddrinfo函数自动生成。

connect函数

connect函数会一直阻塞直到连接成功建立或发生错误。

1
2
3
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
// Returns: 0 if OK, −1 on error

3.1. 服务器相关套接字函数

bind函数

bind告知内核将addr中的套接字地址和套接字描述符中的sockfd相互关联。请使用getaddrinfo为它提供参数。

listen函数

默认情况下使用socket()函数创建的是主动套接字(active socket),它被用于一个连接的客户端。listen()函数则将主动套接字转换为监听套接字(listening socket)。该套接字被服务器端使用。

accept函数

accept等待客户端请求到达listenfd,然后在addr中填写客户端的套接字地址,并返回一个已连接描述符(connected descriptor),可用于和客户端通信。

1
2
3
4
5
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int listenfd, struct sockaddr *addr, int *addrlen);

3.2. 主机和服务的转换

3.2.1. getaddrinfogetnameinfo

  • 这两个函数用于在(域名,地址,端口)字符串表示和socket地址结构间的转换。
  • 这两个函数是可重入的,可以用于线程程序中。

getaddrinfo函数将主机名、主机地址、服务名和端口号的字符串转化成套接字地址结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result);
// Returns: 0 if OK, nonzero error code on error
void freeaddrinfo(struct addrinfo *result);
// Returns: nothing
const char *gai_strerror(int errcode);
// Returns: error message

struct addrinfo {
    int ai_flags; /* Hints argument flags */
    int ai_family; /* First arg to socket function */
    int ai_socktype; /* Second arg to socket function */
    int ai_protocol; /* Third arg to socket function */
    char *ai_canonname; /* Canonical hostname */
    size_t ai_addrlen; /* Size of ai_addr struct */
    struct sockaddr *ai_addr; /* Ptr to socket address structure */
    struct addrinfo *ai_next; /* Ptr to next item in linked list */
};
  • host: 域名或者IP地址
  • service: 服务名(例如: http)或者端口号
  • hints: 一个addrinfo结构,仅能设置下面的参数,其它字段必须为0
    • ai_family: 默认返回IPv4和IPv6的套接字地址。设置本字段可以限制只返回IPv4或IPv6。
    • ai_socktype: 默认对于host关联的每个地址(可能关联多个),最多返回三个addrinfo结构,ai_socktype字段分别是。设置本字段可以限制返回的类型。
      • 连接
      • 数据报
      • 原始套接字
      • ..
    • ai_flags:
      • AI_ADDRCONFIG: 使用连接时建议使用,仅当本地主机配置为IPv4/IPv6时返回IPv4/IPv6地址。
      • AI_CANONNAME: host对应的官方域名会放在列表第一个addrinfo中。
      • AI_NUMBERICSERV: 强制service为端口号。
      • AI_PASSIVE: 默认返回主动套接字。设置这个,同时需要将host设置为NULL,则返回监听套接字。得到的addrinfo结构中的地址字段将会是通配符,表示可以接收所有IP地址的请求。
  • result
    • 返回一个指向addrinfo结构的链表
  • 注意:
    • addrinfo字段不是透明的,能够直接访问并用于传递给socket, connect, bind函数。
    • result中的列表使用完后需要使用freeaddrinfo()释放

getaddrinfo功能相反的是getnameinfo函数。用于从套接字地址sa中提取出主机和服务名字符串。

1
2
3
4
5
#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags);
// Returns: 0 if OK, nonzero error code on error
  • flags:
    • NI_NUMBERICHOST: 强制返回数字的地址字符串。
    • NI_NUMBERICSERV: 强制返回数字的端口号。

3.2.2. open_clientfdopen_listenfd

通过使用getaddrinfo能够提高程序可移植性,而不依赖任何特定版本的IP协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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;
}

4. Web 服务器

Web服务器和客户端通过HTTP协议交互,它是一个基于文本的应用级协议。

Web服务和常规的文件检索服务之间的区别主要在于Web内容可以使用一种叫做HTML(Hypertext Markup Language,超文本标记语言)的语言来编写。浏览器能够根据这种语言显示文本和图形对象。

但是,HTML真正强大之处在于一个页面可以包含指针(超链接)。

4.1. Web内容

Web内容是一种MIME(Multipurpose Internet Mail Extensions,多用途网际邮件扩充协议)类型的字节序列。

MIME类型 描述
text/html HTML页面
text/plain 无格式文本
application/postscript Postscript文档
image/gif GIF格式编码的二进制图像
image/png PNG格式编码的二进制图像
image/jpeg JPEG格式编码的二进制图像

Web服务器能够提供两种类型的内容

  • 静态内容: 将一个静态磁盘文件的内容返回给客户端。
  • 动态内容: 运行一个可执行文件,并将它的输出返回给客户端。

4.2. HTTP事务

HTTP事务由请求和响应组成。

  • HTTP请求
    • 请求行(request line): method URI version
      • method包括GET, POST, OPTIONS, HEAD, PUT, DELETE, TRACE
      • URI给出统一资源标识符,包括文件名和可选的参数
      • version给出HTTP的版本
    • 零个和多个请求报头(request header): header-name: header-data
      • 例如,Host: www.aol.com
    • 空的文本行
  • HTTP响应
    • 响应行(response line): version status-code status-message
      • 例如,HTTP/1.0 200 OK
    • 零个和多个响应报头(response header):
      • Date
      • Content-type
      • Content-Length
    • 终止报头的空文本行
    • 响应主体(response body)

4.3. 服务动态内容

为了服务动态内容,需要解决以下问题

  • 客户端如何传参给服务器
  • 服务器如何传参给子进程
  • 子进程将结果发给哪里

事实上,CGI标准(Common Gateway Interface,通用网关接口) 用于解决这些问题。

  • 客户端传参
    • GET请求: 通过URI传递
    • POST请求: 通过请求主体传递
  • 服务器传参给子进程
    • 服务器会调用fork来创建一个子进程,并使用execve执行cgi程序。在这之间,服务器可以通过环境变量传参。CGI定义了用于传参的环境变量,例如
      • QUERY_STRING
      • SERVER_PORT
      • REQUEST_METHOD
      • ..
  • 子进程将输出发送到哪里
    • 服务器通过dup2在执行cgi程序之前,将输出重定向到和客户端相连的已连接描述符