csapp家庭作业

254 阅读6分钟

第 11 章 网络编程

11.6

A:原样返回每一个请求行和请求报头
比较简单,没必要返回打印出来看看就行。
B:将Tiny输出到一个文件
比较简单,打印出来看看就行。需要调试的话就用管道输出到文件 C:确定浏览器的HTTP版本
chrome是92.0.4515.107版本。是HTTP/1.1。
D: 参考RFC2616标准,确定报头含义。
可以看标准,但是现在HTTP/1.1有新的标准文档了,可以搜索下。但是全英文一般也不会看。可以参考《图解HTTP协议》来查看。
在看首部字段之前,先看一下首部字段的分类 有两种分类方式。

  1. 端到端/逐跳 http包也会经过多层http代理或者网关。有的首部字段在每一跳中都会更改,这些首部字段就是针对这些代理的。而另一些字段是端到端的,也就是说这些代理不许修改并且必须转发。 <图解http协议>中提到,仅有8个字段是逐条首部,而且必须提供connection首部
Connection /Keep-Alive /Proxy-Authenticate 
/Proxy-Authorization /Trailer /TE /Transfer-Encoding /Upgrade
  1. 通用/请求/响应/实体首部
    通用是请求报文和响应报文都用的。
    实体首部是用来限定请求体内容的,主要是文件的类型等。 以下是chrome给的报头
Request headers:
GET / HTTP/1.1    请求行  GET方法, uir是/ 协议版本是 HTTP 1.1
// Host
Host: x.x.xx.x:8000

Connection: keep-alive 

Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/926
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=09
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: buvid_fp=failed
  • Host 请求字段:http1.1唯一要求必须包含的字段。其指定了主机名和端口号。主要用于支持单台主机多个域名的情况,此时两个服务器用来区分究竟要取哪个域名的内容。

  • Connection 通用字段,两个作用:

  1. 控制不再转发给代理的首部字段。例如:客户端发送:Connection: Upgrade 表示代理服务器收到后,不再转发给下一个服务器
  2. 管理持久连接:HTTP/1.1默认都是持久连接。若服务器明确想断开,则指定Connection为Close。用于服务器不支持持久连接。此外若客户端(往往比较新)想兼容旧版本HTTP协议,则用Connection:Keep-alive,来向旧版本服务器来请求长连接。
  • Cache-Control 通用字段,内容有很多种,分为客户端的和服务端的。 这里只介绍max-age=xxx[秒]

表示我只能接收xxx秒的缓存。如果你[指代理服务]的的内容的缓存时间要是超过这个数,对不起,你得去源服务器重拿。

  • Upgrade-Insecure-Requests: 跟https有关,参考这篇博客

  • User-Agent: 创建请求的浏览器和用户代理名称。如果是爬虫,可能会加上爬虫的作者邮箱。如果经过代理,中间也可能加上代理服务器的名称

  • Accept: 请求字段:通知服务器自己能处理的媒体类型和优先级.优先级通过q=来指定,用;分隔。若一次请求多个文件,则服务器优先返回优先级较高的内容。

  • Accept-Encoding: 请求字段:通知服务器自己能处理的编码格式。

  1. gzip是GNU zip支持循环校验。
  2. compress是Unix的文件压缩程序compress实现的压缩
  3. deflate 组合使用zlib格式和deflate压缩算法
  4. identity 不压缩
  • Accept-Language: 请求字段:告知服务器自己需要的自然语言集和优先级。浏览器可以一次指定多种语言集。q来表示优先级。不指定默认为1

  • Cookie:

客户端发送给服务器的Cookie,服务器用于维持该连接的状态。cookie是服务器发回来的,因此第一次请求时,客户端当然没有cookie,但是通常会发送客户端支持cookie的信息,也就是Cookie: status=enable。至于buvid_fp=failed是什么意思,我没有搜索到。

11.7 拓展Tiny,使他可以支持视频播放

这里有一些MP4视频链接,blog.csdn.net/weixin_3036…

用wget下载到本地,起名为home.mp4

get_filetype中添加文件类型

     } else if (strstr(filename, ".mp4")) {
         strcpy(filetype, "video/mp4");

启动后尝试请求即可

xxx.xxx.xxx.xxxx: xxxx/video.mp4

11.8 修改Tiny,使他用信号来回收子进程资源而不是显示等待。

书中原来有一个回收子进程资源的信号处理函数,我们把它抄过来

main函数
if (Signal(SIGCHLD, recycle_childs) == SIG_ERR) {
    unix_error("mask signal pipe error");
}
    
信号处理函数
void recycle_childs(int sig) {
    int olderrno = errno;
    while (waitpid(-1, NULL, 0) > 0) { 
        printf("child recycled!\n");
    }
    errno = olderrno;
}

11.9 使用malloc来分配内存,而不是使用mmap

server_static 函数
srcfd = Open(filename, O_RDONLY, 0);

srcp = (char *)Malloc(size);
lseek(srcfd, header->range_begin, SEEK_SET);    //这里是本人为了支持http协议的Range字段添加的
Rio_readn(srcfd, srcp, size);
Close(srcfd);
Rio_writen(fd, srcp, size);
free(srcp);

11.10 写一个html表单来支持cgi程序的接口

从别的地方抄来一个表单
<form action="cgi-add" method="GET">  //使用GET方法,之后为写后面的作业,会改成POST
First Number:<br>
<input type="text" name="FirstNumbervalue" value="">// 这里浏览器发送请求时,参数是 FirstNumbarValue=xxx。注意在CGI程序中解析
<br><br>
Second Number:<br>
<input type="text" name="SecondNumber" value="">
<br><br>
<input type="submit" value="Submit">
</form>

cgi程序修改:

if ((cgiargs = getenv("QUERY_STRING")) != NULL) {
    mid = strstr(cgiargs, "&");
    *mid = '\0';
    p = strstr(cgiargs, "=");  //解析参数抛弃=之前的name,只要后面的数字
    a1 = atoi(p + 1);
    p = strstr(mid + 1, "=");
    a2 = atoi(p + 1);
}

11.11 拓展Tiny支持Head方法

HEAD方法比较简单,其处理方式和GET是一样的,只是不返回报文主体部分,只返回响应。

为了方便处理,设置了如下数据结构

typedef enum {
    GET,
    POST,
    HEAD
} HttpMethod ;

typedef struct RequestHeader {
    HttpMethod method;
    int is_range;
    int range_begin;
    int range_end;
} RequestHeader;

在doit函数内填写改数据结构,然后

RequestHeader header;
memset(&header, 0, sizeof(header));
if (!strcasecmp(method, "GET")) {
    header.method = GET;
} else if (!strcasecmp(method, "POST")) {
    header.method = POST;
} else if (!strcasecmp(method, "HEAD")) {
    header.method = HEAD;
} else {
    clienterror(fd, method, "501", "Not implemented",
                 "Tiny does not implement this method");
    return ;
}
read_requesthdrs(&rio, &header);  //新增的填写数据结构函数

然后将该数据结构给server_static

server_static(fd, filename, sbuf.st_size, &header);

这样server_static中就能够获取到http方法信息,做相应的处理

serveer_static函数
if (header->method == HEAD) {  //在发送文件前加这样的判断即可
    printf("method HEAD do not send file\n");
    return ;
}

可以通过telnet链接,然后输入HEAD /home.mp4 HTTP/1.0 来测试。注意输完之后需要打两个enter才会有反应

11.12 用Post来获取cgi程序

11.11 已经实现了解析POST,与GET不同的是,POST方法的参数是放在报文主体中的,因此解析参数时会有问题。

// post 请求cgi的请求报文
POST /cgi-add HTTP/1.1 
Host: 47.108.235.60:8000 
Proxy-Connection: keep-alive 
Content-Length: 35 
Cache-Control: max-age=0 
Upgrade-Insecure-Requests: 1 
...

FirstNumbervalue=21&SecondNumber=36        //报文主体

因此,如果是post请求,首先默认调用server_dynamic,然后再次读取cgi参数,当然读取多少是需要解析的 因此先在 requestHeader中添加字段content_length

typedef struct RequestHeader {
    HttpMethod method;
    int is_range;
    int range_begin;
    int range_end;
    int content_length;  //添加字段
} RequestHeader;

再在readHeadrs中添加解析

read_ququesthdrs函数
} else if (!strcasecmp(buf, "Content-Length")) {
    header->content_length = atoi(p + 1);
}

然后再读取对应长度的字符到cgiargs字符串中

doit函数
is_static = parse_uri(uri, filename, cgiargs);    
if (header.method == POST) {   
  is_static = 0;             
  printf("read post args\n");                                                                                                                                                                           
  Rio_readnb(&rio, cgiargs, header.content_length);
}

11.13 处理SIG_PIPE信号和EPIPE错误

SIG_PIPE信号直接忽略

main函数
if (Signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
    unix_error("mask signal pipe error");
}

但是如果RIO包继续写会报EPIPE错误并直接终止程序

void Rio_writen(int fd, void *usrbuf, size_t n)
{
    int ret = rio_writen(fd, usrbuf, n);
    if (ret != n) {
        unix_error("Rio_writen error");  //终止程序
    }                                                               
}

所以直接修改RIO包

void Rio_writen(int fd, void *usrbuf, size_t n)
{
    int ret = rio_writen(fd, usrbuf, n);
    if (ret != n) {
        if (errno == EPIPE) {
            fprintf(stderr, "EPIPE: writen on closed socket\n"); 
            return ;
        }
        unix_error("Rio_writen error");  //终止程序
    }                                                               
}