CSAPP第十一章网络编程

网络应用随处可见。任何时候浏览 Web、发送 email 信息或是玩在线游戏,你就正在使用网络应用程序。有趣的是,所有的网络应用都是基于相同的基本编程模型,有着相似的整体逻辑结构,并且依赖相同的编程接口。

网络应用依赖于很多在系统研究中已经学习过的概念。例如,进程、信号、字节顺序、内存映射以及动态内存分配,都扮演着重要的角色。还有一些新概念要掌握。我们需要理解基本的客户端 - 服务器编程模型,以及如何编写使用因特网提供的服务的客户端—服务器程序。最后,我们将把所有这些概念结合起来,开发一个虽小但功能齐全的 Web 服务器,能够为真实的 Web 浏览器提供静态和动态的文本和图形内容。

客户端-服务器模型

每个网络应用都是基于客户端—服务器模型的。釆用这个模型,一个应用是由一个服务器进程和一个或者多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务。例如,一个 Web 服务器管理着一组磁盘文件,它会代表客户端进行检索和执行。一个 FTP 服务器管理着一组磁盘文件,它会为客户端进行存储和检索。相似地,一个电子邮件服务器管理着一些文件,它为客户端进行读和更新。

客户端—服务器模型中的基本操作是事务(transaction)

 一个客户端服务器事务 (1).png)

一个客户端—服务器事务由以下四步组成。

  1. 当一个客户端需要服务时,它向服务器发送一个请求,发起一个事务。例如,当 Web 浏览器需要一个文件时,它就发送一个请求给 Web 服务器。
  2. 服务器收到请求后,解释它,并以适当的方式操作它的资源。例如,当 Web 服务器收到浏览器发出的请求后,它就读一个磁盘文件。
  3. 服务器给客户端发送一个响应,并等待下一个请求。例如,Web 服务器将文件发送回客户端。
  4. 客户端收到响应并处理它。例如,当 Web 浏览器收到来自服务器的一页后,就在屏幕上显示此页。

认识到客户端和服务器是进程,而不是常提到的机器或者主机,这是很重要的。一台主机可以同时运行许多不同的客户端和服务器,而且一个客户端和服务器的事务可以在同一台或是不同的主机上。无论客户端和服务器是怎样映射到主机上的,客户端—服务器模型都是相同的。

网络

客户端和服务器通常运行在不同的主机上,并且通过计算机网络的硬件和软件资源来通信。网络是很复杂的系统,在这里我们只想了解一点皮毛。我们的目标是从程序员的角度给你一个切实可行的思维模型。

这一块可以去看我的服务器编程相关博客

全球IP因特网

全球 IP 因特网是最著名和最成功的互联网络实现。从 1969 年起,它就以这样或那样的形式存在了。虽然因特网的内部体系结构复杂而且不断变化,但是自从 20 世纪 80 年代早期以来,客户端 - 服务器应用的组织就一直保持着相当的稳定。

11-08 一个因特网应用程序的硬件和软件组织

每台因特网主机都运行实现 TCP/IP 协议(Transmission Control Protocol / Internet Protocol,传输控制协议/互联网络协议)的软件,几乎每个现代计算机系统都支持这个协议。因特网的客户端和服务器混合使用套接字接口函数和 Unix l/O 函数来进行通信(我们将在 11.4 节中介绍套接字接口)。通常将套接字函数实现为系统调用,这些系统调用会陷入内核,并调用各种内核模式的 TCP/IP 函数。

TCP/IP 实际是一个协议族,其中每一个都提供不同的功能。例如,IP 协议提供基本的命名方法和递送机制,这种递送机制能够从一台因特网主机往其他主机发送包,也叫做数据报(datagram)。IP 机制从某种意义上而言是不可靠的,因为,如果数据报在网络中丢失或者重复,它并不会试图恢复。UDP(Unreliable Datagram Protocol,不可靠数据报协议)稍微扩展了 IP 协议,这样一来,包可以在进程间而不是在主机间传送。TCP 是一个构建在 IP 之上的复杂协议,提供了进程间可靠的全双工(双向的)连接。为了简化讨论,我们将 TCP/IP 看做是一个单独的整体协议。我们将不讨论它的内部工作,只讨论 TCP 和 IP 为应用程序提供的某些基本功能。我们将不讨论 UDP。

从程序员的角度,我们可以把因特网看做一个世界范围的主机集合,满足以下特性:

  • 主机集合被映射为一组 32 位的 IP 地址
  • 这组 IP 地址被映射为一组称为因特网域名(Internet domain name)的标识符。
  • 因特网主机上的进程能够通过连接(connection)和任何其他因特网主机上的进程通信。

套接字接口

都是网络编程的接口介绍

Web服务器

TINY WEB服务器

我们通过开发一个虽小但功能齐全的称为 TINY 的 Web 服务器来结束对网络编程的讨论。TINY 是一个有趣的程序。在短短 250 行代码中,它结合了许多我们已经学习到的思想,例如进程控制、Unix I/O、套接字接口和 HTTP。虽然它缺乏一个实际服务器所具备的功能性、健壮性和安全性,但是它足够用来为实际的 Web 浏览器提供静态和动态的内容。我们鼓励你研究它,并且自己实现它。将一个实际的浏览器指向你自己的服务器,看着它显示一个复杂的带有文本和图片的 Web 页面,真是非常令人兴奋

main程序

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
/*
* tiny.c - A simple, iterative HTTP/1.0 Web server that uses the
* GET method to serve static and dynamic content
*/
#include "csapp.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg);

int main(int argc, char **argv)
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;

/* Check command-line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}

listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd);
Close(connfd);
}
}

doit函数

处理HTTP事务

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
41
42
43
44
45
46
void doit(int fd)
{
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;

/* Read request line and headers */
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE);
printf("Request headers:\n");
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) {
clienterror(fd, method, "501", "Not implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio);

/* Parse URI from GET request */
is_static = parse_uri(uri, filename, cgiargs);
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn’t find this file");
return;
}

if (is_static) { /* Serve static content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn’t read the file");
return;
}
serve_static(fd, filename, sbuf.st_size);
}
else { /* Serve dynamic content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn’t run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs);
}
}

首先,我们读和解析请求行(第 11 ~ 14 行)

TINY 只支持 GET 方法。如果客户端请求其他方法(比如 POST),我们发送给它一个错误信息,并返回到主程序(第 15 ~ 19 行),主程序随后关闭连接并等待下一个连接请求。否则,我们读并且(像我们将要看到的那样)忽略任何请求报头(第 20 行)。

然后,我们将 URI 解析为一个文件名和一个可能为空的 CGI 参数字符串,并且设置一个标志,表明请求的是静态内容还是动态内容(第 23 行)。如果文件在磁盘上不存在,我们立即发送一个错误信息给客户端并返回。

最后,如果请求的是静态内容,我们就验证该文件是一个普通文件,而我们是有读权限的(第 31 行)。如果是这样,我们就向客户端提供静态内容(第 36 行)。相似地,如果请求的是动态内容,我们就验证该文件是可执行文件(第 39 行),如果是这样,我们就继续,并且提供动态内容(第 44 行)。

小结

每个网络应用都是基于客户端—服务器模型的。根据这个模型,一个应用是由一个服务器和一个或多个客户端组成的。服务器管理资源,以某种方式操作资源,为它的客户端提供服务。客户端—服务器模型中的基本操作是客户端—服务器事务,它是由客户端请求和跟随其后的服务器响应组成的。

客户端和服务器通过因特网这个全球网络来通信。从程序员的观点来看,我们可以把因特网看成是一个全球范围的主机集合,具有以下几个属性:

  1. 每个因特网主机都有一个唯一的 32 位名字,称为它的 IP 地址。
  2. IP 地址的集合被映射为一个因特网域名的集合。
  3. 不同因特网主机上的进程能够通过连接互相通信。

客户端和服务器通过使用套接字接口建立连接。一个套接字是连接的一个端点,连接以文件描述符的形式提供给应用程序。套接字接口提供了打开和关闭套接字描述符的函数。客户端和服务器通过读写这些描述符来实现彼此间的通信。

Web 服务器使用 HTTP 协议和它们的客户端(例如浏览器)彼此通信。浏览器向服务器请求静态或者动态的内容。对静态内容的请求是通过从服务器磁盘取得文件并把它返回给客户端来服务的。对动态内容的请求是通过在服务器上一个子进程的上下文中运行一个程序并将它的输出返回给客户端来服务的。CGI 标准提供了一组规则,来管理客户端如何将程序参数传递给服务器,服务器如何将这些参数以及其他信息传递给子进程,以及子进程如何将它的输岀发送回客户端。只用几百行 C 代码就能实现一个简单但是有功效的 Web 服务器,它既可以提供静态内容,也可以提供动态内容。