CSAPP-11网络编程

Posted by SH on June 17, 2020

CSAPP-11网络编程

  • 互联网络思想的精髓,封装是关键。

抽象、封装。

客户端-服务器编程模型

服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务。

客户端-服务器模型中的基本操作是事务,一个客户端-服务器事务由以下四步组成:

  • 1)当一个客户端需要服务时,它向服务器发送一个请求,发起一个事务。
  • 2)服务器收到请求后,解释它,并以适当的方式操作它的资源。
  • 3)服务器给客户端发送一个响应,并等待下一个请求。
  • 4)客户端收到响应并处理它。

此事务非数据库事务,仅仅是客户端和服务器执行的一系列步骤。

网络

对主机而言,网络只是又一种I/O设备,是数据源和数据接收方。

一个插到I/O总线扩展槽的适配器提供了到网络的物理接口。从网络上接收到的数据从适配器经过I/O和内存总线复制到内存,通常是通过DMA传送。相似地,数据也能从内存复制到网络。

image-20210120170043813

局域网LAN -> 以太网(Ethernet)是最流行的局域网技术。

以太网段:包括一些电缆(双绞线)和一个叫做集线器的小盒子。以太网段通常跨越一些小的区域(房间或楼层)。集线器不加分辨地将从一个端口上收到的每个位复制到其他所有的端口上,因此每台主机都能看到每个位。

每个以太网适配器都有一个全球唯一的48位地址,存储在这个适配器的非易失性存储器上。一台主机可以发送一段位(称为帧frame,包含头部、有效载荷)到这个网段内的其他任何主机。每个主机适配器都能看到这个帧,但是只有目的主机实际读取它。

桥接以太网(bridged Ethernet):使用一些电缆和叫做网桥(bridge)的小盒子,多个以太网段可以连接成较大的局域网。跨域建筑物或小区。

image-20210120172646908

WAN广域网:多个不兼容的局域网可以通过路由器(router)的特殊计算机连接起来,组成一个internet(互联网络)

互联网至关重要的特性是,它能由采用完全不同和不兼容技术的各种局域网和广域网组成。协议软件消除了不同网络之间的差异。

  • 命名机制
  • 传送机制

互联网络思想的精髓,封装是关键。

image-20210120170508319

全球IP因特网

image-20210120235935466

每台因特网主机都运行实现TCP/IP协议(Transmission Control Protocol/Internet Protocol,传输控制协议/互联网络协议)的软件。

因特网的客户端和服务器混合使用套接字接口函数Unix I/O函数来进行通信。

TCP/IP实际是一个协议族,其中每一个都提供不同的功能:

  • IP协议:提供基本的命名方法和递送机制,数据报,不可靠的。
  • UDP(Unreliable Datagram Protocol,不可靠数据报协议):稍微扩展了IP协议,包可以在进程间而不是在主机间传送。
  • TCP:是一个构建在IP之上的复杂协议,提供了进程间可靠的全双工(双向的)连接。

IP地址

一个32位无符号整数。

因特网域名

域名分层:一级域名、二级域名。

域名集合和IP地址集合之间的映射:直到1988年,都是通过一个叫做HOSTS.TXT的文本文件来手工维护的。从那以后,这个映射是通过分布在世界范围内的数据库(DNS,Domain Name System)来维护的。

因特网连接

连接:点对点、全双工的、可靠的。

一个套接字是连接的一个端点。每个套接字都有相应得套接字地址,由一个因特网地址和一个16位的整数端口组成的。

一个连接是由它两端的套接字地址唯一确定的。这对套接字地址叫做套接字对(socket pair)

套接字接口

套接字接口(socket interface)是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用。

image-20210121002333811

套接字接口是加州大学伯克利分校的研究人员在20世纪80年代早期提出的。也经常被叫做伯克利套接字。

套接字地址结构:

  • 从Linux内核的角度来看,一个套接字就是通信的一个端点
  • 从Linux程序的角度来看,套接字就是一个有相应描述符的打开文件

函数:

  • socket:创建一个套接字描述符;
  • connect:建立和服务器的连接;
  • bind:告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来;
  • listen:转化为监听套接字,可以接受来自客户端的连接请求;
  • accept:等待来自客户端的连接请求;

image-20210121095359419

Web服务器

HTTP。

TINY Web服务器

  • main:无限循环,监听在命令行中传递来的端口上的连接请求,执行事务,关闭连接。
  • doit:处理一个HTTP事务,解析请求,判断解析方式。
  • clienterror:发送一个HTTP响应到客户端,响应行中包含相应的状态码和状态信息,相应主题包含一个html文件,向浏览器用户解释这个错误。
  • read_requesthdrs:读取请求头,终止请求报头的空文本行是由回车和换行对组成的 \r\n。
  • parse_uri:解析URI,假设静态内容的主目录就是当前目录,可执行文件的主目录是./cgi-bin。
  • serve_static:发送一个HTTP响应,主体包含一个本地文件的内容。
  • serve_dynamic:通过派生一个子进程并在子进程的上下文中运行一个CGI程序,来提供各种类型的动态内容。
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/*
 * tiny.c - A simple, iterative HTTP/1.0 Web server that uses the 
 *     GET method to serve static and dynamic content.
 *
 * Updated 11/2019 droh 
 *   - Fixed sprintf() aliasing issue in serve_static(), and clienterror().
 */
#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 - handle one HTTP request/response transaction
 */
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);
    if (!Rio_readlineb(&rio, buf, MAXLINE))
        return;
    printf("%s", buf);
    sscanf(buf, "%s %s %s", method, uri, version);
    // 只支持get请求
    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 */
    // 将URI解析为一个文件名和一个可能为空的CGI参数字符串
    // 设置标志,表明请求是静态内容还是动态内容
    is_static = parse_uri(uri, filename, cgiargs);
    // 如果文件在磁盘上不存在,返回404
    if (stat(filename, &sbuf) < 0)
    {
        clienterror(fd, filename, "404", "Not found",
                    "Tiny couldn't find this file");
        return;
    }

    /* Serve static content */ // 静态内容
    if (is_static)
    {
        // 验证文件是一个普通文件,而且要有读权限
        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);
    }
    /* Serve dynamic content */ // 动态内容
    else
    {
        // 验证文件是可执行的
        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); //line:netp:doit:servedynamic
    }
}

/*
 * read_requesthdrs - read HTTP request headers
 */
void read_requesthdrs(rio_t *rp)
{
    char buf[MAXLINE];

    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
    // 终止请求报头的空文本行是由回车和换行对组成的 \r\n
    while (strcmp(buf, "\r\n"))
    {
        Rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf);
    }
    return;
}

/*
 * parse_uri - parse URI into filename and CGI args
 *             return 0 if dynamic content, 1 if static
 */
// 解析URI,假设静态内容的主目录就是当前目录,可执行文件的主目录是./cgi-bin。
int parse_uri(char *uri, char *filename, char *cgiargs)
{
    char *ptr;

    /* Static content */ // 非cgi-bin目录,静态内容
    if (!strstr(uri, "cgi-bin"))
    {
        // 将cgi参数字符串设置为空
        strcpy(cgiargs, "");
        // 将文件名转化为Linux相对路径
        strcpy(filename, ".");
        strcat(filename, uri);
        // ‘/’ 默认主页 ./home.html
        if (uri[strlen(uri) - 1] == '/')
            strcat(filename, "home.html");
        return 1;
    }
    /* Dynamic content */ // 动态内容
    else
    {
        // 抽取所有cgi参数
        ptr = index(uri, '?');
        if (ptr)
        {
            strcpy(cgiargs, ptr + 1);
            *ptr = '\0';
        }
        else
            strcpy(cgiargs, "");
        // 将URI剩余部分转换为一个Linux相对文件名
        strcpy(filename, ".");
        strcat(filename, uri);
        return 0;
    }
}

/*
 * serve_static - copy a file back to the client 
 */
void serve_static(int fd, char *filename, int filesize)
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */ // 响应头
    // 通过后缀判断文件类型
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: Tiny Web Server\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-length: %d\r\n", filesize);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
    Rio_writen(fd, buf, strlen(buf));

    /* Send response body to client */ // 响应体
    // 将被请求文件的内容复制到已连接描述符fd来发送响应主体。
    // 以读方式打开filename,获得描述符
    srcfd = Open(filename, O_RDONLY, 0);
    // mmap将被请求文件映射到一个虚拟内存空间
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    // 一旦将文件映射到内存,就不需要它的描述符了,所以关闭文件
    Close(srcfd);
    // 复制从srcp开始的filesize个字节到客户端的已连接描述符
    Rio_writen(fd, srcp, filesize);
    // 释放虚拟内存区域,避免内存泄漏
    Munmap(srcp, filesize);
}

/*
 * get_filetype - derive file type from file name
 */
// 通过后缀判断文件类型
void get_filetype(char *filename, char *filetype)
{
    if (strstr(filename, ".html"))
        strcpy(filetype, "text/html");
    else if (strstr(filename, ".gif"))
        strcpy(filetype, "image/gif");
    else if (strstr(filename, ".png"))
        strcpy(filetype, "image/png");
    else if (strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpeg");
    else
        strcpy(filetype, "text/plain");
}

/*
 * serve_dynamic - run a CGI program on behalf of the client
 */
// 通过派生一个子进程并在子进程的上下文中运行一个CGI程序,来提供各种类型的动态内容
void serve_dynamic(int fd, char *filename, char *cgiargs)
{
    char buf[MAXLINE], *emptylist[] = {NULL};

    /* Return first part of HTTP response */
    // 向客户端发送一个表明成功的响应行,CGI程序负责发送响应的剩余部分
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: Tiny Web Server\r\n");
    Rio_writen(fd, buf, strlen(buf));

    // 不太健壮,没有考虑CGI程序会遇到某些错误的可能性
    if (Fork() == 0)
    {
        /* Real server would set all CGI vars here */
        // 将cgi参数放入环境变量QUERY_STRING
        setenv("QUERY_STRING", cgiargs, 1);
        // 子进程重定向它的标准输出到已连接文件描述符
        Dup2(fd, STDOUT_FILENO);              /* Redirect stdout to client */
        // 加载并运行CGI程序
        Execve(filename, emptylist, environ); /* Run CGI program */
    }
    // 父进程阻塞等待子进程终止,回收资源
    Wait(NULL); /* Parent waits for and reaps child */
}

/*
 * clienterror - returns an error message to the client
 */
// 发送一个HTTP响应到客户端,响应行中包含相应的状态码和状态信息,响应主体包含一个HTML文件,向浏览器用户解释这个错误。
void clienterror(int fd, char *cause, char *errnum,
                 char *shortmsg, char *longmsg)
{
    char buf[MAXLINE];

    /* Print the HTTP response headers */
    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html\r\n\r\n");
    Rio_writen(fd, buf, strlen(buf));

    /* Print the HTTP response body */
    sprintf(buf, "<html><title>Tiny Error</title>");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<body bgcolor="
                 "ffffff"
                 ">\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "%s: %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<p>%s: %s\r\n", longmsg, cause);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<hr><em>The Tiny Web server</em>\r\n");
    Rio_writen(fd, buf, strlen(buf));
}