文章目录
学习Web开发,如果仅仅只是学习Web应用开发框架,那只是知其然不知其所以然。只有从最基础的原理开始学习,才能将知识都融会贯通,才能理解更深刻,走得更宽广。为此将自己的学习经验分享,写一个系列博客。
HTTP协议
HTTP即超文本传输协议(HyperText Transfer Protocol),是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。
设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。通过HTTP或者HTTPS协议请求的资源由统一资源标识符URI(Uniform Resource Identifiers)来标识。
HTTP的发展是由蒂姆·伯纳斯-李于1989年在欧洲核子研究组织(CERN)所发起。HTTP的标准制定由万维网协会(World Wide Web Consortium,W3C)和互联网工程任务组(Internet Engineering Task Force,IETF)进行协调,最终发布了一系列的RFC,其中最著名的是1999年6月公布的 RFC 2616,定义了HTTP协议中现今广泛使用的一个版本——HTTP 1.1。
2014年12月,互联网工程任务组(IETF)的Hypertext Transfer Protocol Bis(httpbis)工作小组将HTTP/2标准提议递交至IESG进行讨论,于2015年2月17日被批准。HTTP/2标准于2015年5月以RFC 7540正式发表,取代HTTP 1.1成为HTTP的实现标准。
1. 概述
HTTP是一个客户端(用户)和服务器端(网站)请求和应答的标准(TCP)。通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80)。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个“中间层”,比如代理服务器、网关或者隧道(tunnel)。
尽管TCP/IP协议是互联网上最流行的应用,HTTP协议中,并没有规定必须使用它或它支持的层。事实上,HTTP可以在任何互联网协议上,或其他网络上实现。HTTP假定其下层协议提供可靠的传输。因此,任何能够提供这种保证的协议都可以被其使用。因此也就是其在TCP/IP协议族使用TCP作为其传输层。
通常,由HTTP客户端发起一个请求,创建一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如"HTTP/1.1 200 OK",以及返回的内容,如请求的文件、错误消息、或者其它信息
2. 请求信息——Request
客户端发送一个HTTP请求到服务器,请求消息格式为如下四部分组成的一个字符串。
- 请求行(request line)
- 请求头(header)
- 空行
- 请求体
第一行请求行指定<方法>、<URL>、<协议版本>
以空格分隔,如下例
GET /hello HTTP/1.1
第二行起为请求头部,Host
指出请求的目的地(主机域名);User-Agent
是客户端的信息,它是检测浏览器类型的重要信息,由浏览器定义,并且在每个请求中自动发送,如下实例
Host: www.google.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
第三行为空行,放入\r\n
字符
第四行为请求体,可以添加任意的其它数据,一般POST请求使用
2.1 常用标准请求头字段
常用标准请求头字段 点我跳转
3. 响应信息——Response
服务器收到客户端的请求后,就会有一个
HTTP
的响应消息,HTTP响应也由四部分组成
- 状态行
状态行由协议版本号、状态码、状态消息组成 - 响应头
响应头是客户端可以使用的一些信息,如:Date
(生成响应的日期)、Content-Type
(MIME类型及编码格式)、Connection
(默认是长连接)等等 - 空行
第三行是空行 - 响应体
响应正文,服务器返回给客户端的文本信息
响应信息实例
HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
<html><head>Hello</head> <body>Hello,world </body> </html>
3.1 常用标准响应头字段
熟悉服务器的响应头字段,对于网络爬虫的编写至关重要
常用标准响应头字段 点我跳转
MIME Type
响应头的
Content-Type
字段的值总称为MIME Type
,每个值包括一级类型和二级类型,之间用斜杠分隔。除了预定义的类型,也可以自定义类型。它还可以在尾部使用分号,添加参数
Content-Type: text/html; charset=utf-8
- text/plain
- text/html
- text/css
- image/jpeg
- image/png
- image/svg+xml
- audio/mp4
- video/mp4
- application/javascript
- application/pdf
- application/zip
- application/atom+xml
4. 请求方法
即请求行中指定的请求方法。HTTP/1.1协议中共定义了八种方法(也叫“动作”)来以不同方式操作指定的资源。最常用的请求方法是GET和POST
请求方法 | 简述 |
---|---|
GET | 请求指定的页面信息,并返回实体主体,由于服务器的限制,对get提交通常有长度限制 |
POST | 用来传输实体的主体。虽然get也可以,但是一般是用post传输。在REST架构标准的网站中,post被用来创建资源 |
PUT | 用来传输文件。就像FTP协议的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求URI指定的位置中。在REST架构标准的网站中,会开放此方法用来更新资源。 |
DELETE | 用来删除文件,是与put相反的方法。REST架构标准的网站中,会开放此方法用来作为删除资源。 |
HEAD | 和get方法一样,只是不返回报文主体部分。用于确认uri的有效性及资源更新的时间等 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断 |
CONNECT | HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接 |
OPTIONS | 可使服务器传回该资源所支持的所有HTTP请求方法。用’*'来代替资源名称,向Web服务器发送OPTIONS请求,可以测试服务器功能是否正常运作 |
4.1 GET和POST的区别
-
GET提交的数据会放在URL之后,以
?
分割URL和传输数据,参数之间以&
相连,如EditPosts.aspx?name=test1&id=123456
。 POST方法是把提交的数据放在HTTP包的Body中。因此GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变 -
GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制。
-
GET方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值。
-
GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码。
5. 状态码
在响应信息的状态行中。状态码以3位数字组成,状态代码的第一个数字代表当前响应的类型
- 1xx消息——请求已被服务器接收,继续处理
- 2xx成功——请求已成功被服务器接收、理解、并接受
- 3xx重定向——需要后续操作才能完成这一请求
- 4xx请求错误——请求含有词法错误或者无法被执行
- 5xx服务器错误——服务器在处理某个正确请求时发生错误
常见示例
200 OK //客户端请求成功
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
状态码详细
6. 实现HTTP服务器
纸上得来终觉浅,绝知此事要躬行!学习HTTP协议最好的方法就是自己实现一个HTTP服务器。本来使用Python写一个简单的HTTP服务器是最简单的,但是为了加深理解,为了从更本质上理解HTTP服务器,仍然考虑使用C语言来写一个,毕竟C语言经常不用就沦为了摆设,经常使用C来写一写原理性的东西,对于理解概念和提高C语言都有莫大的好处。
我们这里参考 tinyhttpd 库进行部分代码改写和移植,该源码是solaris平台编写的,在Linux下亦无法直接编译运行,更不支持Windows平台,因此我们将它改造为同时支持Windows和Linux平台的版本。说实话,使用C从头开始造轮子写一个东西是比较累的,完全从头写亦脱离了我们学习HTTP的本意,所以我们就直接改造一个现有的极简Http服务器。只要我们一行一行的去敲出来,搞懂每一行代码,就相当于我们自己实现了一个简单HTTP服务器,同学们无需纠结。
首先给我们的最简HTTP服务器命名为zjhttp(完整代码已经提交GitHub,别忘了给颗小星星),并创建一个头文件 zjHttp.h
/* #define ENABLE_FEATURE_WIN32 1 */
#define SERVER_STRING "Server:zjhttp/0.1.0\r\n" /* server name */
#define BUF_SIZE 2048
#define HTTP_PORT 7749
#define CGI_ENVIRONMENT_SIZE 8192
#if ENABLE_FEATURE_WIN32
#define _ZJ_WIN32
#endif // ENABLE_FEATURE_WIN32
#ifdef _ZJ_WIN32
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_NONSTDC_NO_DEPRECATE
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") /* 加载 ws2_32.dll */
typedef SOCKET Client;
typedef struct cgi_env {
char buf[CGI_ENVIRONMENT_SIZE];
int len;
int st;
} CGI_ENV;
#else
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>
typedef int Client;
#endif /* _ZJ_WIN32 */
#include <stdio.h>
#include <sys/stat.h>
void *accept_request(void*);
void bad_request(Client);
void cat(Client, FILE *);
void cannot_execute(Client);
void error_die(const char *);
void execute_cgi(Client, char *, const char *, const char *);
int get_line(Client, char *, int);
void headers(Client, const char *);
void not_found(Client);
void serve_file(Client, const char *);
int startup(int);
void unimplemented(Client);
6.1 环境
如习惯在visual studio 中编写代码,请将第一行/* #define ENABLE_FEATURE_WIN32 1 */
注释的代码打开,表示编译Windows版本。
我仍然建议在Windows上的朋友使用GCC编译器,下载安装MinGW工具,获得一套Windows版的GCC工具链。如果单独下载MinGW工具太卡慢,则请下载Code::Blocks工具,因为Code::Blocks的下载速度更快。该工具是一个开源的全功能的跨平台C/C++集成开发环境,其自带了MinGW工具。
如使用MinGW或在Linux平台,则无需打开/* #define ENABLE_FEATURE_WIN32 1 */
这行注释,因为我们使用makefile脚本进行构建。
6.2 C语言预处理回顾
预处理是指在代码编译之前进行的处理
大多数预处理指令可分为三类
- 宏定义
使用#define
指令定义一个宏
使用#undef
指令删除一个宏 - 文件包含
使用#include
指令包含一个指定文件 - 条件编译
包含#if、#ifdef、ifndef
等,使预处理器可以根据条件确定是否将一段文本包含
简单宏
大多数时候可以把宏简单理解为编辑器里面的
Ctrl+R
,即全文中的字符串替换
#define 标识符 替换列表
#define PI 3.1514
宏的替换列表可以包括标识符、关键字、数值、字符串常量、操作符等。当预处理器遇到一个宏时,会做一个“标识符”代表“替换列表”的记录,在文件后面,不管标识符在哪出现,都会被替换列表的内容替换。有一点需要注意,定义一个宏时,替换列表允许为空。
带参的宏
也称函数式宏,宏函数。
#define 标识符(a,b,c,...,d) 替换列表
//实例
#define MAX(x,y) ((x)>(y)?(x):(y))
预处理器会在后面将所有的MAX(x,y)替换为后面替换列表的内容,其中x、y分别对应后面替换列表中的x、y
包含多条语句的宏
当创建的宏函数需要包含多条语句时,有一种通用的写法
#define ECHO(s) \
do{\
gets(s);\
puts(s);\
}while(0)
ECHO(str);
//宏展开后
do{gets(str);puts(str);}while(0);
条件编译
#if
和#endif
#define DEBUG 1 /* #if和#endif成对出现 #if后面跟常量表达式,0为false,反之true。当为0时,它们之间的代码在预处理时会被删去 */ #if DEBUG printf("this is debug!\n"); #endif
条件编译主要用于C语言的跨平台实现,通过定义不同的宏来决定面对不同的运行平台,哪些代码需要被包含,哪些代码应该被删除。如我们的zjhttp中的/* #define ENABLE_FEATURE_WIN32 1 */
,打开该行代码后,即会定义一个_ZJ_WIN32
宏,用于包含运行在Windows平台的代码
#if ENABLE_FEATURE_WIN32
#define _ZJ_WIN32
#endif // ENABLE_FEATURE_WIN32
-
#ifdef
和#ifndef
#ifdef
指令用于检测一个标识符是否已经被定义为宏,#ifndef
则相反,检测一个标识符是否未被定义为宏#ifdef 标识符 /* 它等价于以下指令 */ #if defined 标识符
-
#elif
和#else
这两个指令结合#if
使用,相当于C语言中的if…else if…else的用法。这两个指令还可以与#ifdef
或#ifndef
结合使用#if 表达式1 ... #elif 表达式2 ... #else ... #endif
6.3 函数简述
其中与CGI相关的函数暂不说明,等到下一章详细说明了CGI协议后再看
accept_request
处理从套接字上监听到的一个 HTTP 请求bad_request
返回客户端 400响应码,表示错误请求cat
读取服务器上某文件并写到 socket 中error_die
把错误信息写到 perrorget_line
读取一行HTTP报文headers
返回HTTP响应头not_found
返回找不到请求文件信息serve_file
调用 cat 把服务器文件内容返回给浏览器startup
开启服务,包括绑定端口,监听unimplemented
返回给浏览器该 HTTP 请求方法不被支持
6.4 代码实现
我们上面只是实现了zjhttp的头文件,接下来我们需要创建zjHttp.c
的具体实现,由于代码太长,摘要部分说明。结合上面讲的HTTP知识以及代码注释,基本可以理解这些代码的含义。
...
/* 接收客户端的连接,读取请求数据 */
void *accept_request(void* args){
Client client = (Client)args;
char *method = NULL;
char *url = NULL;
char buf[BUF_SIZE];
int numchars;
char path[512];
struct stat st;
int cgi = 0;
char *query_string = NULL;
memset(buf, 0, sizeof(buf));
/* 获取一行HTTP报文数据 */
if ((numchars = get_line(client, buf, sizeof(buf))) == 0) return NULL;
/* 获取Http请求行字段,格式为<method> <request-URL> <version> 每个字段以空白字符相连 */
method = strtok(buf, " "); /* 从请求行中分割出method字段 */
/* 本Demo仅实现GET请求和POST请求 */
if (StrCaseCmp(method, "GET") && StrCaseCmp(method, "POST")){
unimplemented(client);
return NULL;
}
/* 如果请求方法为POST,cgi标志位置1,开启cgi解析 */
if (StrCaseCmp(method, "POST") == 0) cgi = 1;
url = strtok(NULL, " "); /* 从请求行中分割出request-URL字段 */
if (StrCaseCmp(method, "GET") == 0){ /* GET请求,url可能带有"?",有查询参数 */
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?'){
cgi = 1; /* 如果带有查询参数,执行cgi解析参数,设置标志位为1 */
*query_string = '\0'; /* 将解析参数截取下来 */
query_string++;
}
}
/* url中的路径格式化到 path */
sprintf(path, "static%s", url);
/* 如果path只是一个目录,默认设置为首页 */
if (path[strlen(path) - 1] == '/') strcat(path, "index.html");
if (stat(path, &st) == -1) { /* 访问的网页不存在,则读取剩下的请求头信息并丢弃 */
while ((numchars > 0) && strcmp("\n", buf))
numchars = get_line(client, buf, sizeof(buf));
not_found(client);
}else{
/* 网页存在。路径如果是目录,显示主页 (S_IFDIR代表目录) */
if ((st.st_mode & S_IFMT) == S_IFDIR) strcat(path, "/index.html");
#ifdef _ZJ_WIN32
char *suffix = &path[strlen(path) - 4];
if (StrCaseCmp(suffix, ".cgi") == 0) cgi = 1;
#else
if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
cgi = 1;
#endif // _ZJ_WIN32
if (!cgi) serve_file(client, path); /* 将静态文件返回 */
else execute_cgi(client, path, method, query_string); /* 执行cgi动态解析 */
}
CloseSocket(client); /* 关闭套接字 */
return NULL;
}
...
void unimplemented(Client client){ /* 501 相应方法未实现 */
char buf[1024];
sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
void not_found(Client client){ /* 返回404 */
char buf[1024];
sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "your request because the resource specified\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
...
void bad_request(Client client){ /* 发送400 */
char buf[1024];
sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "<P>Your browser sent a bad request, ");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client, buf, sizeof(buf), 0);
}
void headers(Client client, const char *filename){ /* 发送HTTP头 */
char buf[1024];
(void)filename; /* could use filename to determine file type */
strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), 0);
}
/* 将请求的文件返回给浏览器客户端 */
void serve_file(Client client, const char *filename){
FILE *resource = NULL;
int numchars = 1;
char buf[1024];
buf[0] = 'A'; buf[1] = '\0';
while ((numchars > 0) && strcmp("\n", buf)) /* 读取HTTP请求头并丢弃 */
numchars = get_line(client, buf, sizeof(buf));
resource = fopen(filename, "r");
if (resource == NULL) not_found(client);
else{
headers(client, filename); /* 添加HTTP头 */
cat(client, resource); /* 发送文件内容 */
}
fclose(resource);
}
...
void cat(Client client, FILE *resource){
char buf[1024];
fgets(buf, sizeof(buf), resource); /* 读取文件到buf中 */
while (!feof(resource)){ /* 判断文件是否读取到末尾 */
send(client, buf, strlen(buf), 0); /* 读取并发送文件内容 */
fgets(buf, sizeof(buf), resource);
}
}
void error_die(const char *sc){
perror(sc);
exit(1);
}
int get_line(Client client, char *buf, int size){
int i = 0;
char c = '\0';
while ((i < size - 1) && (c != '\n')){
if (recv(client, &c, 1, 0) > 0) {
if (c == '\r') {
if ((recv(client, &c, 1, MSG_PEEK) > 0) && (c == '\n'))
recv(client, &c, 1, 0);
else c = '\n';
}
buf[i] = c;
i++;
}else c = '\n';
}
buf[i] = '\0';
return(i);
}
zjhttp
简单流程
从zjHttp.c
中的main
函数开始,首先调用了startup
函数,主要用于C语言的Socket创建,关于C语言的Socket其实和其他语言的也都大同小异,遵循基本的Unix中的Socket步骤,不清楚的可以查找一些资料学习,Socket编程是网络编程的基础,这里不在叙述。
创建Socket之后,在main
函数中还调用的了accept
函数,用于监听绑定的端口。当监听到客户端连接时,创建一个新的线程用于专门处理该客户端的请求。在新的线程中,将执行accept_request
函数,用于具体的解析并处理客户端请求。accept_request
函数基本包含了Http服务器的主要逻辑,其中清晰的展现了将客户端发送的二进制数据依照Http协议格式解析的过程。
关于HTTP相关部分代码先到这里,下一篇博客 接着讲解CGI相关知识。如需完整代码,请到我的GitHub上把 zjhttp 完整clone下来,并将对应的makefile_xxx脚本重命名后执行make指令进行编译(例如Windows平台,则将makefile_win32重命名为makefile),编译成功后运行程序,在浏览器输入http://localhost:7749 测试。
注意:如Windows上未成功安装MinGW,则将zjHttp.c和zjHttp.h拷贝至visual studio 新建的工程中,打开/* #define ENABLE_FEATURE_WIN32 1 */
这行注释编译即可。
*扩展部分:HTTP/2
2015年,HTTP/2 发布。它不叫 HTTP/2.0,是因为标准委员会不打算再发布子版本了,下一个新版本将是 HTTP/3。
二进制协议
HTTP/1.1 版的头信息肯定是文本(ASCII编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧"(frame):头信息帧和数据帧。
二进制协议的一个好处是,可以定义额外的帧。HTTP/2 定义了近十种帧,为将来的高级应用打好了基础。如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便得多。
多工
HTTP/2 复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了"队头堵塞"。
举例来说,在一个TCP连接里面,服务器同时收到了A请求和B请求,于是先回应A请求,结果发现处理过程非常耗时,于是就发送A请求已经处理好的部分, 接着回应B请求,完成后,再发送A请求剩下的部分。
这样双向的、实时的通信,就叫做多工(Multiplexing)。
数据流
因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。
HTTP/2 将每个请求或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。另外还规定,客户端发出的数据流,ID一律为奇数,服务器发出的,ID为偶数。
数据流发送到一半的时候,客户端和服务器都可以发送信号(
RST_STREAM
帧),取消这个数据流。1.1版取消数据流的唯一方法,就是关闭TCP连接。这就是说,HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。
客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。
头信息压缩
HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如
Cookie
和User Agent
,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。
HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用
gzip
或compress
压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
服务器推送
HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。
常见场景是客户端请求一个网页,这个网页里面包含很多静态资源。正常情况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发出静态资源请求。其实,服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了。
附录
2.1 常用标准请求头字段
- Accept 设置接受的内容类型
Accept: text/plain
- Accept-Charset 设置接受的字符编码
Accept-Charset: utf-8
- Accept-Encoding 设置接受的编码格式
Accept-Encoding: gzip, deflate
- Accept-Datetime 设置接受的版本时间
Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT
- Accept-Language 设置接受的语言
Accept-Language: en-US
- Authorization 设置HTTP身份验证的凭证
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
- Cache-Control 设置请求响应链上所有的缓存机制必须遵守的指令
Cache-Control: no-cache
- Connection 设置当前连接和hop-by-hop协议请求字段列表的控制选项
Connection: keep-alive
Connection: Upgrade
- Content-Length 设置请求体的字节长度
Content-Length: 348
- Content-MD5 设置基于MD5算法对请求体内容进行Base64二进制编码
Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
- Content-Type 设置请求体的MIME类型(适用POST和PUT请求)
Content-Type: application/x-www-form-urlencoded
- Cookie 设置服务器使用Set-Cookie发送的http cookie
Cookie: $Version=1; Skin=new;
- Date 设置消息发送的日期和时间
Date: Tue, 15 Nov 1994 08:12:31 GMT
- Expect 标识客户端需要的特殊浏览器行为
Expect: 100-continue
- Forwarded 披露客户端通过http代理连接web服务的源信息
Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
Forwarded: for=192.0.2.43, for=198.51.100.17
- From 设置发送请求的用户的email地址
From: [email protected]
- Host 设置服务器域名和TCP端口号,如果使用的是服务请求标准端口号,端口号可以省略
Host: en.wikipedia.org:8080
Host: en.wikipedia.org
- If-Match 设置客户端的ETag,当时客户端ETag和服务器生成的ETag一致才执行,适用于更新自从上次更新之后没有改变的资源
If-Match: "737060cd8c284d8af7ad3082f209582d
- If-Modified-Since 设置更新时间,从更新时间到服务端接受请求这段时间内如果资源没有改变,允许服务端返回304 Not Modified
If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
- If-None-Match 设置客户端ETag,如果和服务端接受请求生成的ETage相同,允许服务端返回304 Not Modified
If-None-Match: “737060cd8c284d8af7ad3082f209582d”
- If-Range 设置客户端ETag,如果和服务端接受请求生成的ETage相同,返回缺失的实体部分;否则返回整个新的实体
If-Range: “737060cd8c284d8af7ad3082f209582d”
- If-Unmodified-Since 设置更新时间,只有从更新时间到服务端接受请求这段时间内实体没有改变,服务端才会发送响应
If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT
- Max-Forwards 限制代理或网关转发消息的次数
Max-Forwards: 10
- Origin 标识跨域资源请求(请求服务端设置Access-Control-Allow-Origin响应字段)
Origin:
http://www.example-social-network.com
- Pragma 设置特殊实现字段,可能会对请求响应链有多种影响
Pragma: no-cache
- Proxy-Authorization 为连接代理授权认证信息
Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
- Range 请求部分实体,设置请求实体的字节数范围,具体可以参见HTTP/1.1中的Byte serving
Range: bytes=500-999
- Referer 设置前一个页面的地址,并且前一个页面中的连接指向当前请求,意思就是如果当前请求是在A页面中发送的,那么referer就是A页面的url地址
Referer:
http://en.wikipedia.org/wiki/Main_Page
- TE 设置用户代理期望接受的传输编码格式,和响应头中的Transfer-Encoding字段一样
TE: trailers, deflate
- Upgrade 请求服务端升级协议
Upgrade: HTTP/2.0, HTTPS/1.3, IRC/6.9, RTA/x11, websocket
- User-Agent 用户代理的字符串值
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0
- Via 通知服务器代理请求
Via: 1.0 fred, 1.1 example.com (Apache/1.1)
- Warning 实体可能会发生的问题的通用警告
Warning: 199 Miscellaneous warning
3.1 常用标准响应头字段
- Access-Control-Allow-Origin 指定哪些站点可以参与跨站资源共享
Access-Control-Allow-Origin: *
- Accept-Patch 指定服务器支持的补丁文档格式,适用于http的patch方法
Accept-Patch: text/example;charset=utf-8
- Accept-Ranges 服务器通过byte serving支持的部分内容范围类型
Accept-Ranges: bytes
- Age 对象在代理缓存中暂存的秒数
Age: 12
- Allow 设置特定资源的有效行为,不允许则返回405
Allow: GET, HEAD
- Alt-Svc 服务器使用"Alt-Svc"(Alternative Servicesde)头标识资源可以通过不同的网络位置或者不同的网络协议获取
Alt-Svc: h2=“http2.example.com:443”; ma=7200
- Cache-Control 告诉服务端到客户端所有的缓存机制是否可以缓存这个对象,单位是秒
Cache-Control: max-age=3600
Cache-Control: no-cache
- Connection 这个字段要求服务器不要关闭TCP连接,以便其他请求复用。服务器同样回应这个字段。客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接,客户端在最后一个请求时,发送
Connection: close
,明确要求服务器关闭TCP连接
Connection: keep-alive
Connection: close
- Content-Disposition 告诉客户端弹出一个文件下载框,并且可以指定下载文件名
Content-Disposition: attachment; filename=“fname.ext”
- Content-Encoding 设置数据使用的编码类型
Content-Encoding: gzip
- Content-Language 为封闭内容设置自然语言或者目标用户语言
Content-Language: en
- Content-Length 响应体的字节长度
Content-Length: 348
- Content-Location 设置返回数据的另一个位置
Content-Location: /index.htm
- Content-MD5 设置基于MD5算法对响应体内容进行Base64二进制编码
Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
- Content-Range 标识响应体内容属于完整消息体中的那一部分
Content-Range: bytes 21010-47021/47022
- Content-Type 设置响应体的MIME类型。关于字符的编码,1.0版规定,头信息必须是 ASCII 码,后面的数据可以是任何格式。因此,服务器回应的时候,必须告诉客户端,数据是什么格式,这就是该字段的作用
Content-Type: text/html; charset=utf-8
- Date 设置消息发送的日期和时间
Date: Tue, 15 Nov 1994 08:12:31 GMT
- ETag 特定版本资源的标识符,通常是消息摘要
ETag: “737060cd8c284d8af7ad3082f209582d”
- Expires 设置响应体的过期时间
Expires: Thu, 01 Dec 1994 16:00:00 GMT
- Last-Modified 设置请求对象最后一次的修改日期
Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
- Link 设置与其他资源的类型关系
Link: ; rel=“alternate”
- Location 在重定向中或者创建新资源时使用
Location:
http://www.w3.org/pub/WWW/People.html
- Pragma 设置特殊实现字段,可能会对请求响应链有多种影响
Pragma: no-cache
- Proxy-Authenticate 设置访问代理的请求权限
Proxy-Authenticate: Basic
- Public-Key-Pins 设置站点的授权TLS证书
Public-Key-Pins: max-age=2592000; pin-sha256=“E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=”;
- Refresh "重定向或者新资源创建时使用,在页面的头部有个扩展可以实现相似的功能,并且大部分浏览器都支持
<meta http-equiv="refresh" content="5; url=http://example.com/">
Refresh: 5; url=
http://www.w3.org/pub/WWW/People.html
- Retry-After 如果实体暂时不可用,可以设置这个值让客户端重试,可以使用时间段(单位是秒)或者HTTP时间
Example 1: Retry-After: 120
Example 2: Retry-After: Fri, 07 Nov 2014 23:59:59 GMT
- Server 服务器名称
Server: Apache/2.4.1 (Unix)
- Set-Cookie 设置HTTP Cookie
Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1
- Status 设置HTTP响应状态
Status: 200 OK
- Strict-Transport-Security 一种HSTS策略通知HTTP客户端缓存HTTPS策略多长时间以及是否应用到子域
Strict-Transport-Security: max-age=16070400; includeSubDomains
- Trailer 标识给定的header字段将展示在后续的chunked编码的消息中
Trailer: Max-Forwards
- Transfer-Encoding 设置传输实体的编码格式,目前支持的格式: chunked, compress, deflate, gzip, identity
Transfer-Encoding: chunked
- Upgrade 请求客户端升级协议
Upgrade: HTTP/2.0, HTTPS/1.3, IRC/6.9, RTA/x11, websocket
- Vary 通知下级代理如何匹配未来的请求头已让其决定缓存的响应是否可用而不是重新从源主机请求新的
Example 1: Vary: *
Example 2: Vary: Accept-Language
- Via 通知客户端代理,通过其要发送什么响应
Via: 1.0 fred, 1.1 example.com (Apache/1.1)
- Warning 实体可能会发生的问题的通用警告
Warning: 199 Miscellaneous warning
- WWW-Authenticate 标识访问请求实体的身份验证方案
WWW-Authenticate: Basic
- X-Frame-Options 点击劫持保护:
deny frame中不渲染
sameorigin 如果源不匹配不渲染
allow-from 允许指定位置访问
allowall 不标准,允许任意位置访问
X-Frame-Options: deny