第 11 章 网络编程
11.6
A:原样返回每一个请求行和请求报头
比较简单,没必要返回打印出来看看就行。
B:将Tiny输出到一个文件
比较简单,打印出来看看就行。需要调试的话就用管道输出到文件
C:确定浏览器的HTTP版本
chrome是92.0.4515.107版本。是HTTP/1.1。
D: 参考RFC2616标准,确定报头含义。
可以看标准,但是现在HTTP/1.1有新的标准文档了,可以搜索下。但是全英文一般也不会看。可以参考《图解HTTP协议》来查看。
在看首部字段之前,先看一下首部字段的分类
有两种分类方式。
- 端到端/逐跳 http包也会经过多层http代理或者网关。有的首部字段在每一跳中都会更改,这些首部字段就是针对这些代理的。而另一些字段是端到端的,也就是说这些代理不许修改并且必须转发。 <图解http协议>中提到,仅有8个字段是逐条首部,而且必须提供connection首部
Connection /Keep-Alive /Proxy-Authenticate
/Proxy-Authorization /Trailer /TE /Transfer-Encoding /Upgrade
- 通用/请求/响应/实体首部
通用是请求报文和响应报文都用的。
实体首部是用来限定请求体内容的,主要是文件的类型等。 以下是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 通用字段,两个作用:
- 控制不再转发给代理的首部字段。例如:客户端发送:Connection: Upgrade 表示代理服务器收到后,不再转发给下一个服务器
- 管理持久连接: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: 请求字段:通知服务器自己能处理的编码格式。
- gzip是GNU zip支持循环校验。
- compress是Unix的文件压缩程序compress实现的压缩
- deflate 组合使用zlib格式和deflate压缩算法
- 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"); //终止程序
}
}