以下内容引用于《图解HTTP》
当我们在网页浏览器(Web browser)的地址栏中输入 URL 时,Web 页面是如何呈现的?
Web 页面当然不能凭空显示出来。根据 Web 浏览器地址栏中指定的 URL,Web 浏览器从 Web 服务器端获取文件资源(resource)等信息,从而显示出 Web 页面。
像这种通过发送请求获取服务器资源
的 Web 浏览器等,都可称为客户端(client)。
Web 使用一种名为 HTTP(HyperText Transfer Protocol,超文本传输协议
)的协议作为规范
,完成从客户端到服务器端等一系列运作流程。而协议是指规则的约定。可以说,Web 是建立在 HTTP 协议上通信的。
设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法
HTTP有多个版本,目前广泛使用的是HTTP/1.1版本
URL(Uniform Resource Location)又叫统一资源定位符
,俗称网址,URL格式如下
通过域名解析得到服务器IP地址
默认使用80
端口,HTTPS默认使用443端口
/代表web根目录
,不是服务器的根目录让网页直接滑动到指定位置
像/ ? :
等这样的字符, 已经被url当做特殊意义
理解了. 因此这些字符不能随意出现.
某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
例如我们在百度搜索C++的时候,C++就被转义成了C%2B%2B "+" 被转义成了 "%2B"
把原始内容转义的过程叫做urlencode
把转义后的内容转回原始内容的过程叫做urldecode
urldecode就是urlencode的逆过程
urlencode工具
HTTP协议是基于TCP协议之上的,虽然TCP协议是面向连接
的,但是HTTP在通信时并不关心底层是如何通信
的,TCP在底层已经建立好链接
了,所以HTTP在通信时并不需要建立连接
。TCP建立连接与HTTP无关,HTTP直接向对方发送 HTTP请求即可。
HTTP本身是无状态的,并不会记录用户的任何信息
HTTP 协议自身不对请求和响应之间的通信状态进行保存
。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理
使用 HTTP 协议,每当有新的请求发送时,就会有对应的新响应产生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务
,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的
可是,随着 Web 的不断发展,因无状态而导致业务处理变得棘手的情况增多了
。比如,用户登录到一家购物网站,即使他跳转到该站的其他页面后,也需要能继续保持登录状态。针对这个实例,网站为了能够掌握是谁送出的请求,需要保存用户的状态
HTTP/1.1 虽然是无状态协议,但为了实现期望的保持状态功能,于是引入了 Cookie 技术
。有了 Cookie 再用 HTTP 协议通信,就可以管理状态了。
1.0版本相较于0.9版本,主要规范了协议格式
,支持了更多功能以及数据传输方式
1.1版本相较于1.0版本,主要在于性能上的改进
,以及其他一些特殊功能的添加
缓存控制:一些资源在没有改变的情况下不需要重新传输
缓存是指代理服务器或客户端本地磁盘内保存的资源副本。利用缓存可减少对源服务器的访问
,因此也就节省了通信流量和通信时间
。
长连接的改进:
以当年的通信情况来说,因为都是些容量很小的文本传输,所以即使这样也没有多大问题。可随着 HTTP 的普及,文档中包含大量图片的情况多了起来。比如,使用浏览器浏览一个包含多张图片的 HTML页面时,在发送请求访问 HTML 页面资源的同时,也会请求该 HTML 页面里包含的其他资源。因此,每次的请求都会造成无谓的 TCP 连接建立和断开,增加通信量的开销。
在一次连接中可以进行多次请求
,并且1.1版本的长连接具有管线化管理思想长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态
持久连接的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销
,减轻了服务器端的负载
。另外,减少开销的那部分时间
,使 HTTP 请求和响应能够更早地结束,这样 Web 页面的显示速度也就相应提高了。在 HTTP/1.1 中,所有的连接默认都是持久连接,但在 HTTP/1.0 内并未标准化
。虽然有一部分服务器通过非标准的手段实现了持久连接,但服务器端不一定能够支持持久连接。毫无疑问,除了服务器端,客户端也需要支持持久连接。
管线化:
长连接使得多数请求以管线化(pipelining)方式发送成为可能。从前发送请求后需等待并收到响应
,才能发送下一个请求
。管线化技术出现后,不用等待响应亦可直接发送下一个请求
。这样就能够做到同时并行发送多个请求,而不需要一个接一个地等待响应了
。
比如,当请求一个包含 10 张图片的 HTML Web 页面,与挨个连接相比,用持久连接可以让请求更快结束。而管线化技术则比持久连接还要快。请求数越多,时间差就越明显。
用于 HTTP 协议交互的信息被称为 HTTP 报文。请求端(客户端)的 HTTP 报文叫做请求报文
,响应端(服务器端)的叫做响应报文
。HTTP 报文本身是由多行(用 CR+LF 也就是回车换行)数据构成的字符串文本。HTTP 报文大致可分为报文首部和报文主体两块。两者由最初出现的空行(CR+LF)来划分。通常,并不一定要有报文主体。
用Fidder进行抓包,随便找一个请求和响应
报文结构分为以下:
[请求方法] + [url] + [协议版本]
请求的属性
, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束正文
,空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;报文首部包括首行和Header
其中最常用的就是GET方法和POST方法
响应报头的响应行中就有状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
keep-alive 是长连接,close是短连接
)HTTP 是无状态协议,它不对之前发生过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求处理。
假设要求登录认证的 Web 页面本身无法进行状态的管理(不记录已登录的状态),那么每次跳转新页面不是要再次登录,就是要在每次请求报文中附加参数来管理登录状态。
不可否认,无状态协议当然也有它的优点。由于不必保存状态,自然可减少服务器的 CPU 及内存资源的消耗。从另一侧面来说,也正是因为 HTTP 协议本身是非常简单的,所以才会被应用在各种场景里。
保留无状态协议这个特征的同时又要解决类似的矛盾问题,于是引入了 Cookie 技术
。Cookie 技术通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。
Cookie 会根据从服务器端发送的响应报文内的一个叫做Set-Cookie 的首部字段信息
,通知客户端保存 Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去
。
服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。
Cookie信息被保存到浏览器安装目录的Cookie文件中,而用户的电脑本身是不安全的,要是被黑客攻击,攻击者拿到了用户的Cookie信息,那么Cookie信息中的敏感信息就会泄漏。
为了解决Cookie泄漏敏感信息的问题,需要把敏感信息保存到服务器端,于是就有了Session
Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器
中,而Session保存在服务器
上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。
每个用户访问服务器都会建立一个session,服务器为了标识用户的唯一身份,用户与服务器建立连接的同时,服务器会自动为其分配一个Session ID。
这时浏览器端,也就是用户端的Cookie文件中存的是一个Session ID
,若用户再次被攻击,Cookie信息泄漏,那么攻击者拿到的Cookie信息里只有Cookie ID
,攻击者只能通过服务器的认证访问服务器,并不能拿到用户的敏感信息
。
HTTP是基于TCP协议的,而HTTP服务器其实就是在收到请求的时候,给用户返回响应信息
,我们自己构建一个简单的响应报头,返回给客户端,实现简单的HTTP通信
HttpServer.hpp
#ifndef __HTTP_SERVER_H__
#define __HTTP_SERVER_H__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 5
using namespace std;
class HttpServer{
private:
int port;
int lsock;
public:
HttpServer(int _port = 8080):port(_port),lsock(-1)
{}
void initServer(){
//设置SIGCHLD的默认递达方式为忽略
signal(SIGCHLD,SIG_IGN);
//创建监听套接字
lsock = socket(AF_INET,SOCK_STREAM,0);
if(lsock < 0){
cerr<<"sock error"<<endl;
exit(2);
}
//填充协议族/ip/端口号到sockaddr_in
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
//绑定端口号
if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0){
cerr<<"bind error"<<endl;
exit(3);
}
//开始监听
if(listen(lsock,BACKLOG) < 0 ){
cerr<<"listen error"<<endl;
exit(4);
}
}
void Start(){
cout << "HttpServer start"<<endl;
struct sockaddr_in peer;
while(true){
socklen_t len = sizeof(peer);
int sock = accept(lsock,(struct sockaddr*)&peer,&len);
if(sock < 0){
cerr << "accept error" <<endl;
continue;
}
cout<< "Get a new link"<<endl;
//创建子进程处理任务
if(fork() == 0){
close(lsock);
EchoHttp(sock);
exit(0);
}
close(sock);
}
}
void EchoHttp(int sock){
char request[2048];
ssize_t s =recv(sock,&request,sizeof(request)-1,0);
if(s > 0){
request[s] = 0;
cout << request <<endl;
//构建响应报头
string response = "HTTP/1.0 200 OK \r\n";
response += "Client-type : text/html\r\n";
response += "\r\n";
response += "\
\
\
\
HTTPTEST \
\
\
HTTP TEST
\
Hello World!
\
\
\
";
//响应信息发送给客户端
send(sock,response.c_str(),response.size(),0);
}
close(sock);
}
~HttpServer(){
if(lsock != -1){
close(lsock);
}
}
};
#endif
HttpServer.cc
#include"HttpServer.hpp"
void Usage(string proc){
cout<<"Usage\n \t";
cout<<proc<<" port"<<endl;
}
int main(int argc, char* argv[]){
if(argc != 2){
Usage(argv[0]);
exit(0);
}
HttpServer* hp = new HttpServer(atoi(argv[1]));
hp->initServer();
hp->Start();
delete hp;
return 0;
}
效果演示
可以看到浏览器的请求报头