HTTP协议叫做超文本传输协议, 是一种用作获取诸如HTML文档这类资源的协议。它是 Web 上进行任何数据交换的基础,同时,也是一种客户端—服务器(client-server)协议,也就是说,请求是由接受方——通常是浏览器——发起的。一个完整网页文档是由获取到的不同文档组件像是文本、布局描述、图片、视频、脚本等——重新构建出来的。
client和server在进行网络通信的时候,client可以把自己的“东西"给别人,client也可以把别人的“东西"拿到自己的本地,站在系统层面这个过程叫做IO,站在网络层面这个过程叫做request/response。在http协议中这些"东西" 可以是:网页文本,图片,视频,音频,我们把它们统称为资源。http协议叫做超文本传输协议,即它可以进行网络资源的互相传递。
URL代表着是统一资源定位符(Uniform Resource Locator),平时我们俗称的 “网址” 其实就是说的 URL。
一个URL的构成:
(1) 协议方案名
http://
中的http
代表协议名称,它表明了浏览器必须使用何种协议。它通常都是HTTP 协议或是HTTP协议的安全版,即 HTTPS。
(2) 登录信息
user:pass
表示登录认证信息,包含了用户的用户名和密码。现在绝大多数的URL这个字段是被省略的
(3) 服务器地址
要访问服务器我们必须要知道服务器的ip地址+端口号。
www.xxxx
表示域名,域名的本质:就是IP地址,用来表示唯一主机。因为直接使用IP地址不方便,所以它不经常在网络上使用。域名会被解析成IP地址,域名解析服务是由域名解析服务器完成的。平时用的浏览器和APP的服务端其实已经内置域名解析服务器对应的IP地址和端口。
使用ping指令就能查看到域名解析
(4) 服务器端口号
http
协议默认端口号是80,https
默认端口号是443(5) 带层次的文件路径
/dir/index.htm
表示要访问的资源所在的路径/
代表web根目录(6) 查询字符串
?
是区分URL左侧和右侧的分隔符,?
后面所跟的uid=1
代表参数,这些参数是用&
符号分隔的键/值对列表
(7) 片段标识符
#
后面的ch1
表示片段标识符,一般用来定位网页上的内容,比如点击页面上的一个链接,跳到当前页面的某一个位置。
像 /
、 ?
和 :
等这样的字符, 已经被url当做特殊意义理解了。因此这些字符不能随意出现。
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%
,编成%XY
例如搜索C++,“+
” 被转义成了 “%2B
”,同时对汉字也会进行编码。这个过程是浏览器或某种客户端自动做的,称为url
的encode
当服务器受到请求时,要对特殊符号和汉字进行解码,这个过程称为url
的decode
。
我们可以使用相关的在线工具验证一下。
HTTP请求由4部分构成:
KV
形式的键值对);每组属性之间使用\r\n分隔Content-Length
属性来标识其长度HTTP响应由4部分构成:
KV
形式的键值对);每组属性之间使用\r\n分隔html/css/js
,图片,视频,音频等资源,允许为空字符串。如果有效载荷存在, 则在报头中会有一个Content-Length
属性来标识其长度HTTP是怎么做到报头和有效载荷分离呢?
根据特殊字符\r\n
,按行来读取,一旦读到空行,报头就读取完了。如果有效载荷存在, 则在报头中会有一个Content-Length
属性来标识其长度,,Content-Type
属性来标识其数据类型(text/html等)
HTTP是怎么做到序列化和反序列化的呢?
\r\n
,把多行请求累加就变成了一个大字符串\r\n
作为分隔符,切分成各种结构浏览器本身就可以构建HTTP请求,所以以浏览器作为HTTP的客户端,自己来实现一个HTTP的服务端
完整代码:lesson37 · 遇健/Linux - 码云 - 开源中国 (gitee.com)
HttpServer.hpp
#pragma once
#include"Sock.hpp"
#include
#include
static const uint16_t defaultport = 8888;
class HttpServer;
using func_t =function<string(string&)>;
class ThreadData
{
public:
ThreadData(int sock, string ip, uint16_t port, HttpServer *tsvrp)
: _sock(sock), _ip(ip), _port(port), _tsvrp(tsvrp)
{
}
~ThreadData()
{
}
public:
int _sock;
HttpServer *_tsvrp;
string _ip;
uint16_t _port;
};
class HttpServer
{
public:
HttpServer(func_t f, uint16_t port = defaultport)
:func(f)
,_port(port)
{}
void InitServer()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
}
void HanlderHttpRequest(int sock)
{
char buffer[4096];
string request;
ssize_t s=recv(sock,&buffer,sizeof(buffer)-1,0); // 我们认为我们一次读完了
if(s>0)
{
buffer[s]=0;
request=buffer;
string response=func(request);
send(sock,response.c_str(),response.size(),0);
}
else
{
logMessage(Info, "client quit...");
}
}
static void*threadRoutine(void*args)
{
pthread_detach(pthread_self());
ThreadData*td=static_cast<ThreadData*>(args);
td->_tsvrp->HanlderHttpRequest(td->_sock);
close(td->_sock);
delete td;
return nullptr;
}
void Start()
{
while(true)
{
string clientip;
uint16_t clientport;
int sock=_listensock.Accept(&clientip,&clientport);
if(sock<0)
continue;
pthread_t tid;
ThreadData*td=new ThreadData(sock, clientip, clientport, this);
pthread_create(&tid,nullptr,threadRoutine,td);
}
}
~HttpServer()
{
}
private:
uint16_t _port;
Sock _listensock;
func_t func;
};
Main.cc
#include"HttpServer.hpp"
#include
string HandlerHttp(const string&request)
{
// 确信, request一定是一个完整的http请求报文
// 给别人返回的是一个http response
std::cout<<"-----------------------------"<<endl;
cout<<request<<endl;
return"";
}
// ./httpserver 8888
int main()
{
uint16_t port=8889;
std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp,port));
tsvr->InitServer();
tsvr->Start();
return 0;
}
启动服务端后,在浏览器上进行访问
由于代码中什么也没有,就会显示下面的信息
此时浏览器发送请求后,服务端就会收到浏览器发送来的请求
上面的URL是/
,此时浏览器会默认访问Web根目录。我们想要访问特定目录下的资源也是通过URL来体现的
Host
:被请求的目标服务器的ip地址和端口号
Connection
:表示链接模式,长短链接(后面详谈)
User-Agent
:这次请求的客户端信息
平时我们在下载软件的时候,好像浏览器就自动能够识别并适配我的系统,它是如何做到的呢?
比如我要下载微信,浏览器就会自动识别出我是Windows系统的。
我在搜索的本质是向百度服务器发送了http请求,我的http请求中携带一个User-Agent
字段,表明浏览器相关的详细信息,其中就包含有操作系统的信息,百度识别后就可以自动呈现搜索结果时,自动把电脑平台中符合操作系统的标签选中,自动下载它。
剩下的字段了解即可:
Cache-Insecure-Requests
:代表最大缓存机制,双方在通信时要建立缓存,默认为0表示不缓存
Accept-Encoding
:代表客户端所接收的编码和压缩类型
Accept-Language
:代表客户端可以接收的编码符号
这里介绍两种方法:
方法一:
我们用telnet工具来连接百度的服务器,发出http请求,百度就会给我们http响应
方法二:
使用Postman软件
用Postman获取出来的百度首页其实和上面telnet获取出来的相同,只不过Postman做了一些排版便于我们查看,实际在网络传输中还是telnet上面的方式。
完整代码:lesson37/Http_v2 · 遇健/Linux - 码云 - 开源中国 (gitee.com)
Main.cc
#include"HttpServer.hpp"
#include
const string SEP="\r\n";
string HandlerHttp(const string&request)
{
// 确信, request一定是一个完整的http请求报文
// 给别人返回的是一个http response
std::cout<<"-----------------------------"<<endl;
cout<<request<<endl;
// 给你一个响应
string response="HTTP/1.0 200 OK"+SEP;
response += SEP;
response += "Hello Http"; // 有效载荷部分(正文)
return response;
}
// ./httpserver 8888
int main()
{
uint16_t port=8888;
std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp,port));
tsvr->InitServer();
tsvr->Start();
return 0;
}
启动服务端后,在浏览器上进行访问,就会出现Hello Http
的响应
服务端也收到了浏览器发送来的请求
可是我们一般的响应是一个网页,图片,视频,音频类的资源,同时它们不能硬编码到代码中。我们必须把它们放到文件里,所以我们要给这些资源维护一个自己的目录通过读取指定的文件来访问它们。
完整代码:lesson37/Http_v4 · 遇健/Linux - 码云 - 开源中国 (gitee.com)
Util.hpp
: 主要功能是读取文件。坑:一般对于网页文件都是文本的, 但是如果是图片, 视频, 音频是二进制的,所以我们要以二进制的形式读。
#pragma once
#include
#include
#include
#include
#include
#include
#include"log.hpp"
class Util
{
public:
// 坑: 一般对于网页文件, 都是文本的
// 但是如果是图片, 视频, 音频 -> 二进制的(中间可能存在0值)
static bool ReadFile(const std::string&path,std::string*fileContent)
{
// 1. 获取文件本身的大小
struct stat st;
int n=stat(path.c_str(),&st);
if(n<0)
return false;
int size=st.st_size;
// 2. 调整string的空间
fileContent->resize(size);
// 3. 读取
int fd=open(path.c_str(), O_RDONLY);
if(fd<0)
return false;
read(fd,(char*)fileContent->c_str(),size);
close(fd);
logMessage(Info,"read file %s done",path.c_str());
return true;
}
};
Main.cc
:
#include"HttpServer.hpp"
#include"Util.hpp"
#include
const string SEP="\r\n";
const string path="./wwwroot/index.html";
string HandlerHttp(const string&request)
{
// 确信, request一定是一个完整的http请求报文
// 给别人返回的是一个http response
std::cout<<"-----------------------------"<<endl;
cout<<request<<endl;
// 资源, 图片(.png, jpg...), 网页(.html, .htm), 视频, 音频 -> 文件! 都要有自己的后缀!
string body;
Util::ReadFile(path,&body);
// 给你一个响应
string response="HTTP/1.0 200 OK"+SEP; //状态行
response += "Content-Length: "+std::to_string(body.size()) + SEP; // 表明有效载荷的长度
response += "Content-Type: text/html" + SEP;
response += SEP;
response += body; // 有效载荷部分(正文)
return response;
}
// ./httpserver 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp,port));
tsvr->InitServer();
tsvr->Start();
return 0;
}
启动服务端后,在浏览器上进行访问,就会出现刚才写的html
文件
下面要做的工作:
wwwroot
(Web根目录)下的不同文件html
中特定的标签被浏览器解释,重新发起http
请求完整代码:lesson37/Http_v5 · 遇健/Linux - 码云 - 开源中国 (gitee.com)
Util.hpp
:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include"log.hpp"
class Util
{
public:
// 坑: 一般对于网页文件, 都是文本的
// 但是如果是图片, 视频, 音频 -> 二进制的(中间可能存在0值)
static bool ReadFile(const std::string&path,std::string*fileContent)
{
// 1. 获取文件本身的大小
struct stat st;
int n=stat(path.c_str(),&st);
if(n<0)
return false;
int size=st.st_size;
// 2. 调整string的空间
fileContent->resize(size);
// 3. 读取
int fd=open(path.c_str(), O_RDONLY);
if(fd<0)
return false;
read(fd,(char*)fileContent->c_str(),size);
close(fd);
logMessage(Info,"read file %s done",path.c_str());
return true;
}
static string ReadOneline(string&message,const string&sep)
{
auto pos=message.find(sep);
if(pos==string::npos)
return"";
string s=message.substr(0,pos);
message.erase(0,pos+sep.size());
return s;
}
// GET /favicon.ico HTTP/1.1
// GET /a/b/c/d/e/f/g/h/i/g/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z HTTP/1.1
static bool ParseRequestLine(const string&line, string*method, string*url, string*httpVersion)
{
// 1个字符串变3个字符串
stringstream ss(line);
ss>>*method>>*url>>*httpVersion;
return true;
}
};
Main.cc
:
#include"HttpServer.hpp"
#include"Util.hpp"
#include
#include
const string SEP="\r\n";
// 一般一个webservser, 不做特殊说明, 如果用户直接默认访问'/', 我们绝对不能把整站给对方
// 需要添加默认首页!! 而且, 不能让用户访问wwwroot里面的任何一个目录本身,也可以给每一个目录都带上默认首页
const string defaultHomePage="index.html"; // web根目录
const string webRoot="./wwwroot"; // web根目录
// const string path="./wwwroot/index.html";
class HttpRequest
{
public:
HttpRequest()
:path_(webRoot)
{}
~HttpRequest()
{}
void Print()
{
logMessage(Info, "method: %s, url: %s, version: %s", method_.c_str(), url_.c_str(),
httpVersion_.c_str());
// for(const auto&line: body_)
// logMessage(Debug, "-%s",line.c_str());
logMessage(Debug, "path: %s", path_.c_str());
}
public:
std::string method_;
std::string url_;
std::string httpVersion_;
std::vector<std::string> body_;
std::string path_;
std::string suffix_; // 要访问资源的后缀
};
HttpRequest Deserialize(string&message)
{
HttpRequest req;
string line=Util::ReadOneline(message,SEP);
Util::ParseRequestLine(line,&req.method_,&req.url_,&req.httpVersion_);
while(!message.empty())
{
line=Util::ReadOneline(message,SEP);
req.body_.push_back(line);
}
req.path_+=req.url_; // "wwwroot/a/b/c.html" , "./wwwroot"
if(req.path_[req.path_.size()-1]=='/')
req.path_+=defaultHomePage;
auto pos=req.path_.rfind(".");
if(pos==string::npos)
req.suffix_=".html";
else
req.suffix_=req.path_.substr(pos);
return req;
}
string GetContentType(string&suffix)
{
string content_type="Content-Type: ";
if(suffix==".html" || suffix==".htm")
content_type + "text/html";
else if (suffix == ".css")
content_type += "text/css";
else if (suffix == ".js")
content_type += "application/x-javascript";
else if (suffix == ".png")
content_type += "image/png";
else if (suffix == ".jpg")
content_type += "image/jpeg";
else
{
}
return content_type + SEP;
}
string HandlerHttp(string&message)
{
// 1. 读取请求
// 确信, request一定是一个完整的http请求报文
// 给别人返回的是一个http response
std::cout<<"-----------------------------"<<endl;
// 资源, 图片(.png, jpg...), 网页(.html, .htm), 视频, 音频 -> 文件! 都要有自己的后缀!
// 2. 反序列化和分析请求
HttpRequest req= Deserialize(message);
req.Print();
// 3. 使用请求
string body;
Util::ReadFile(req.path_,&body);
// 给你一个响应
string response="HTTP/1.0 200 OK"+SEP; //状态行
response += "Content-Length: "+std::to_string(body.size()) + SEP; // 表明有效载荷的长度
response += GetContentType(req.suffix_);
response += SEP;
response += body; // 有效载荷部分(正文)
return response;
}
// ./httpserver 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp,port));
tsvr->InitServer();
tsvr->Start();
return 0;
}
启动服务端,观察收到的请求
支持网页间的跳转
网页添加图片
其中最常用的就是GET方法和POST方法。
HTTP方法是由浏览器客户端发起的,它会构建一个http request,携带的方法GET/POST。在服务端和浏览器交互时,浏览器是通过html的表单,采用不同的方法进行资源请求提交的。
总结:
GET方法,POST方法的差别和它们各自的应用场景
注:私密 != 安全,两种方法都不安全,因为没有加密
所以所有的登录注册支付等行为,都要使用POST方法提参(不是只用)
最常见的状态码, 比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway)
下面就是一个重定向过程,你去西门吃麻辣烫却看到门口写着:移至东门跑去东门吃
你后来每次去麻辣烫前先去西门,看西门的店是否开门,没开去东门。这就是一个临时重定向的过程。对应的状态码有:302, 307
半年后,你先去西门的麻辣烫发现没开门,并且在门外写着往后三个月之内本店将永久搬到东门,你后来就直接东门,不再跑到西门。这就是一个永久重定向的过程。对应的状态码有:301
发现在上面两次重定向中都携带了新的麻辣烫地址
重定向就是就是通过各种方法将各种网络请求重新定个方向转到其它位置。重定向操作由服务器向请求发送特殊的重定向响应而触发。重定向响应包含以 3
开头的状态码以及 Location
标头,其保存着重定向的URL。
下面演示一下临时重定向
将HTTP响应当中的状态码改为302,加上对应的状态码描述,此外还需要在HTTP响应报头当中添加Location字段,这个Location后面跟的就是你需要重定向到的网页,比如我这里将其设置为腾讯网
此时当浏览器访问我的服务器时,就会立马跳转到腾讯网的首页
临时重定向:618,双11期间,你打开某些软件就会自动跳转到金主爸爸(淘宝,京东)的网站
永久重定向:迁移到新的域名。例如,公司改名后,你希望用户在搜索旧名称的时候,依然可以访问到应用了新名称的站点。
Content-Type
: 数据类型(text/html
等)Content-Length
: Body(正文)的长度Host
: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;User-Agent
: 声明用户的操作系统和浏览器版本信息;referer
: 当前页面是从哪个页面跳转过来的;location
: 搭配3xx
状态码使用, 告诉客户端接下来要去哪里访问;Cookie
: 用于在客户端存储少量信息。通常用于实现会话(session)的功能;
Keep-Alive
Keep-Alive
:长连接,一张网页中有很多资源,基于一条建立好的连接上的很多http请求,对方收到后对这些请求依次处理,基于同样的连接返回一些响应。即不用每次访问资源都建立连接并断开连接,基于一条连接就能保证我们把资源传输完毕。
在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。而从HTTP/1.1起,默认使用长连接,用以保持连接特性
favicon.ico
使用chrome测试我们的服务器时, 可以看到服务器打出的请求中还有一个 GET /favicon.ico HTTP/1.1
这样的请求。其中favicon.ico
:代表网页上面的图标
http本身是一种无状态协议,在同一个连接中,两个执行成功的请求之间是没有关系的。这就带来了一个问题,用户没有办法在同一个网站中进行连贯的交互,比如在电商网站中使用购物车功能。
可是我们在使用浏览器时发现并不是这样的。
比如,我们打开bilibili网站,登录账号,你登录一次之后,这个登录状态就会维持很久,即使你关闭网站甚至关掉浏览器,再次打开此网站我们还是登录状态的。
上面的工作就是通过Cookie和session实现的,称为会话保持。http并不直接做这个工作,而是浏览器为了满足用户需求,提供了会话保持功能。
Cookie是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器。
用户想要访问某个VIP资源时,网站会要求此用户输入账号密码登录后进行认证。认证通过后,服务器会通过一些http的选项把我的私人信息携带到http响应中,当浏览器收到响应后,浏览器会将响应中的Cookie信息在本地进行保存。
Cookie信息分为内存级和文件级。
从此往后,继续在访问相同的网站,浏览器构建的http请求中都会携带Cookie属性,会把曾经保存的历史Cookie信息携带上。不用用户手动操作,这个是浏览器自动做的。同时client在访问每一个资源时会自动进行认证,不用用户再输入账号和密码了。
我们登录某网站后,将把该网站的Cookie信息删除,删除之后,用户就不是登录状态了,需要进行重新登录
Cookie存在的问题:
我们今天在上网,登录了QQ, 微信, 淘宝,bilibili等,于是浏览器客户端就会存在大量的Cookie信息,如果信息被不法分子盗取,他就可以自己的浏览器直接使用用户的Cookie信息访问服务器,服务器就会误认为是用户在访问服务端。
Session是一种可以维持服务器端的数据存储技术,Session对象存储特定用户会话所需的属性及配置信息。
session解决的问题:
虽然仍然存在Cookie信息被盗取的问题,但是Cookie中保存的是session id,并没有泄露用户的账号和密码,用户的私人信息已经被服务器维护起来的。这就解决了泄露用户私人信息的问题。
黑客盗取了用户的session id后仍可以非法登录,只能靠服务端的安全策略保障安全,例如账号被异地登录了,服务端察觉后只要让session id失效即可,这样异地登录将会使用户重新验证账号密码,一定程度上保障了信息的安全。
<1> cookie与session区别
<2> session的生命周期
Session保存在服务器端,为了获取更高的存取速度,服务器一般会把Session放在内存里面,每个用户都会有一个独立的Session。如果Session里面的内容太过复杂,当大量的用户访问服务器时,可能会导致内存溢出,所以我们的session内容应当适当的精简。当我们第一次访问服务器时,服务器会给我们自动创建一个Session,生成session后,只要用户继续访问,服务器就会更新session的最后访问时间,并且维护这个session。当用户访问服务器一次,无论是否读写了session,服务器都会认定这个session活跃(active)了一次。当越来越多的用户访问我们的服务器时,因此我们的session会越来越多。为了防止内存溢出,服务器会把长时间没有活跃的Session删除。这个时间就是session的超时时间,过了超时时间,我们的session就会自动失效。
在服务器的响应报头当中添加上一个Set-Cookie
字段,看看浏览器第二次发起HTTP请求时是否会带上这个Set-Cookie
字段
客户端第二次请求就已经携带cookie信息
往后,每次http请求,都会自动携带曾经设置好的所有cookie,帮助服务器进行鉴权行为,这就是http的会话保持的功能。