本文参考: HTTP协议
HTTP协议(超文本传输协议HyperText Transfer Protocol),它是基于TCP协议的应用层传输协议,简单来说就是客户端和服务端进行数据传输的一种规则。
URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,URL是对从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址,互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎处理它。
一个URL大致由如下几部分构成(其中有些部分是可以省略的):
解释 :
http://
表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS,成熟的协议要和端口号一 一匹配,例如使用HTTP协议的服务器要采用80号端口提供服务,使用HTTPS协议的服务器要采用443号端口提供服务usr:pass
表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器。www.example.jp
表示的是服务器地址,也叫做域名,通过域名解析系统能够得到域名对应的IP地址。ping
命令获得www.baidu.com
域名解析后的IP地址为39.156.66.14
80
表示的是服务器端口号,这个字段一般是被省略的,因为协议和端口号是一一对应的。/dir/index.htm
表示的是要访问的资源所在的路径。访问服务器的目的是获取服务器上的某种资源,通过前面的域名和协议已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径。uid=1
表示的是请求时提供的额外的参数,这些参数是以键值对的形式,通过&符号分隔开的。你好世界
,此时可以看到URL中有很多参数,而在这众多的参数当中有一个参数wd(word),表示的就是我们搜索时的搜索关键字wd=你好世界。ch1
表示的是片段标识符,是对资源的部分补充。如果在搜索关键字当中出现了像/?:
这样的字符,由于这些字符已经被URL当作特殊意义理解了,因此URL在呈现时会对这些特殊字符进行转义。
转义的规则如下:
比如当我们搜索C++时,由于+
加号在URL当中也是特殊符号,经过转义以后变成了%2B
,因此一个+
就会被编码成一个%2B
。
说明: URL当中会对这些特殊符号做编码,中文也属于特殊字符。
我们浏览器看到的是中文,但是复制其url
时,就会发现其被转义了。
HTTP请求由以下四部分组成:
key: value
的形式按行陈列的。Content-Length
属性来标识请求正文的长度。前面三部分是一般是由HTTP协议自行设置的,而请求正文一般是用户提交的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。
为了证明上面的结论我们可以通过启动下面的代码将浏览器的发送的请求,打印出来。
// Sock.hpp
// 这是一个对socket相关接口的一个封装,大概了解其接口的作用就行
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
class Sock
{
public:
Sock()
:_sock(-1)
{}
void Socket()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
}
void Bind(uint16_t port)
{
struct sockaddr_in local;
socklen_t len = sizeof(local);
memset(&local, 0, len);
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
bind(_sock, (struct sockaddr*)&local, len);
}
void Listen(int backlog = 32)
{
listen(_sock, backlog);
}
int Accept(std::string* client_ip, uint16_t* client_port)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
memset(&client, 0, len);
int sockfd = accept(_sock, (struct sockaddr*)&client, &len);
return sockfd;
}
int Connect(const std::string& server_ip, uint16_t server_port)
{
struct sockaddr_in server;
socklen_t len = sizeof(server);
memset(&server, 0, len);
server.sin_family = AF_INET;
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
server.sin_port = htons(server_port);
return connect(_sock, (struct sockaddr*)&server, len);
}
int getFd()
{
return _sock;
}
~Sock()
{
if (_sock >= 0)
{
close(_sock);
}
}
private:
int _sock; // 对于服务端来说是监听套接字
};
// httpserver.hpp
// 这是一个对服务器相关接口的一个封装,大概了解其接口的作用就行。
// 注意这里使用了原生线程库pthread
#pragma once
#include
#include
#include
#include
#include
#include
#include "Sock.hpp"
class HttpServer
{
using func_t = std::function<std::string(const std::string&)>;
public:
HttpServer(func_t func, uint16_t server_port)
:_func(func), _server_port(server_port)
{}
void Init()
{
_listen_fd.Socket();
_listen_fd.Bind(_server_port);
_listen_fd.Listen();
}
void Start()
{
std::string client_ip;
uint16_t client_port;
while (true)
{
// 接收连接
int sockfd = _listen_fd.Accept(&client_ip, &client_port);
if (sockfd < 0)
{
continue;
}
// 多线程并发处理任务
pthread_t tid;
ThreadData* ptd = new ThreadData(sockfd, this, client_port, client_ip);
pthread_create(&tid, nullptr, ThreadRoutine, ptd);
}
}
private:
struct ThreadData
{
ThreadData(int sockfd, HttpServer* is, uint16_t client_port, std::string client_ip)
:_sockfd(sockfd), _is(is), _client_port(client_port), _client_ip(client_ip)
{}
~ThreadData()
{
if (_sockfd >= 0)
{
// logMessage(Info, "通讯完成,文件描述符关闭");
close(_sockfd);
}
}
public:
int _sockfd; // 通信套接字
HttpServer* _is; // 传递过来的this指针
uint16_t _client_port; // 客户端端口号
std::string _client_ip; // 客户端ip
};
static void* ThreadRoutine(void* args)
{
pthread_detach(pthread_self());
ThreadData* ptd = static_cast<ThreadData*>(args);
ptd->_is->HandleHttpRequest(ptd->_sockfd, ptd->_client_ip, ptd->_client_port);
delete ptd;
return nullptr;
}
// 这个必须是一个可重入函数
void HandleHttpRequest(int sockfd, const std::string& client_ip, uint16_t client_port)
{
char buf[4096];
// 1.假设一次能够读取完毕,并且只请求一次
ssize_t n = recv(sockfd, buf, sizeof(buf) - 1, 0);
buf[n] = '\0';
std::string request = buf;
// 2.业务处理
std::string response = _func(request);
// 3.结果返回
send(sockfd, response.c_str(), response.size(), 0);
}
private:
func_t _func; // 回调函数
uint16_t _server_port; // 服务器端口号
Sock _listen_fd; // 监听套接字
};
// httpserver.cpp
#include
#include
#include
#include
#include
#include "httpserver.hpp"
// 使用手册
static void Usage(std::string proc)
{
std::cout << "usage\n\t" << proc << " 端口" << std::endl;
}
// 处理http请求的函数,这里我们对于请求不作任何处理,只打印请求的内容
std::string Handler(const std::string& request)
{
// 打印请求的内容
std::cout << "------------------------------------" << std::endl;
std::cout << request << std::endl;
return "";
}
// 必须以 程序名 端口号 的方式启动
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t server_port = atoi(argv[1]);
std::unique_ptr<HttpServer> up(new HttpServer(Handler, server_port));
up->Init();
up->Start();
return 0;
}
编译运行我们的程序,这里我们的直接在浏览器的地址栏里面,输入我们的公网IP和端口号,格式:IP: 端口号
。
/
表示的是web根目录,这个web根目录可以是你的机器上的任何一个目录,由你自己指定的。HTTP响应由以下四部分组成:
为了证明上面的结论我们可以通过telnet
发送的请求,来得到响应,这里我们以百度的响应为例子。
我们先通过telnet
连接上百度,然后对百度发送一个请求,然后百度会将响应结果发送回来,这里我们对百度的Web根目录发起请求,它应该给我们显示百度前端界面的html
文件。
在响应报头里面有几个很重要的字段,这些字段不仅响应里面含有,请求里面也含有。
Content-Length
:其表示的是响应正文的长度,通过这个字段双方能够确定对方的请求/响应正文的长度是多少,方便正确的进行提取。Content-Type
: 表示正文中数据类型(例如html,png,jpg,MP3,MP4)。为什么HTTP请求和响应都要有交互版本?
HTTP请求当中的请求行和HTTP响应当中的状态行,当中都包含了http的版本信息。其中请求格式中HTTP的版本是客户端的HTTP版本,而响应格式的HTTP版本是服务器HTTP版本。
客户端和服务器双方在进行通信时会交互双方http版本,主要还是为了兼容性的问题。因为服务器和客户端使用的可能是不同的http版本,为了让不同版本的客户端都能享受到对应的服务,此时就要求通信双方需要进行版本协商。
客户端在发起HTTP请求时告诉服务器自己所使用的http版本,此时服务器就可以根据客户端使用的http版本,为客户端提供对应的服务,而不至于因为双方使用的http版本不同而导致无法正常通信。因此为了保证良好的兼容性,通信双方需要交互一下各自的版本信息是很有必要的。
HTTP常见的方法如下:
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获得报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议连接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINK | 断开连接关系 | 1.0 |
其中最常用的就是就是GET方法和POST方法,我们这里也只介绍这两种方法。
GET方法一般用于获取某种资源信息,但上传数据时可以使用GET方法。
GET方法是通过url传参的,这种传参的方式不够私密(注意:不是不够安全),而且因为url的长度是有限制的,所以如果要传递的数据很多,就不能够使用GET方法。
Postman软件演示GET传参
如果访问我们的服务器时使用的是GET方法,此时应该通过url进行传参,可以在Params下进行参数设置,因为Postman当中的Params就相当于url当中的参数,你在设置参数时可以看到对应的url也在随之变化。
我们的服务器收到的HTTP请求时,可以看到请求行中的url就携带上了我们刚才在Postman当中设置的参数。
POST方法一般用于将数据上传给服务器。
POST方法是通过正文传参的,因为理论上正文的长度是可以无限大的,所以POST方法就可以携带更多的数据。
此外POST方法传参更加私密,因为POST方法不会将你的参数回显到url当中,此时也就不会被别人轻易看到。
Postman软件演示GET传参
这里我们使用的是POST方法,此时就应该通过正文进行传参,可以在Body下进行参数设置,在设置时可以选中Postman当中的raw方式传参,表示原始传参,也就是你输入的参数是什么样的实际传递的参数就是什么样的。
此时服务器收到的HTTP请求的请求正文就不再是空字符串,而是我们通过正文传递的参数。
所以所有的登陆注册支付等行为,一般都要使用POST方法提参,但是POST方法和GET方法实际都不安全,要做到安全只能通过加密来完成。
HTTP的状态码如下:
类别 | 原因短语 | |
---|---|---|
1XX | Informational(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
最常见的状态码比如:200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect ,重定向),504(Bad Gateway)
这里我们重点介绍Redirect,重定向功能。
HTTP中的重定向就是当客户端访问一个服务器的资源后,服务器会返回一个响应包,响应包中带有另一个链接,然后浏览器会自动跳转到另一个链接,无需用户点击,是浏览器自动跳转的,此时这个服务器相当于提供了一个引路的服务。
重定向又可分为临时重定向和永久重定向,其中状态码301表示的就是永久重定向,而状态码302和307表示的是临时重定向。
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时你访问的直接就是重定向后的网站。而如果某个网站是临时重定向,那么每次访问该网站时如果需要进行重定向,都需要浏览器来帮我们完成重定向跳转到目标网站。
进行重定向时需要用到Location
字段,Location
字段是HTTP报头当中的一个属性信息,该字段表明了你所要重定向到的目标网站,其后面跟的就是重定向后的地址。
临时重定向
// - 1构建请求行
std::string response = "HTTP/1.0 302 Found" + tol::Sep;
// - 2构建响应报头
response += "Location: https://www.csdn.net" + tol::Sep;
// - 3构建空行
response += tol::Sep;
永久重定向
// - 1构建请求行
std::string response = "HTTP/1.0 301 Moved Permanently" + tol::Sep;
// - 2构建响应报头
response += "Location: https://www.csdn.net" + tol::Sep;
// - 3构建空行
response += tol::Sep;
实际应用
重定向可实现许多目标:
域名别称,扩大站点的用户覆盖面。
假如站点位于 www.example.com
域名下,那么通过 example.com
也应该可以访问到。这种情况下,可以建立从 example.com
的页面到www.example.com
的重定向。此外还可以提供你域名常见的同义词,或者该域名容易导致的拼写错误的别称来重定向到你的网站。
站点维护或停机期间的临时重定向。
Host字段表明了客户端要访问的服务的IP和端口,比如当浏览器访问我们的服务器时,浏览器发来的HTTP请求当中的Host字段填的就是我们的IP和端口。但客户端不就是要访问服务器吗?为什么客户端还要告诉服务器它要访问的服务对应的IP和端口?
因为有些服务器实际提供的是一种代理服务,也就是代替客户端向其他服务器发起请求,然后将请求得到的结果再返回给客户端。在这种情况下客户端就必须告诉代理服务器它要访问的服务对应的IP和端口,此时Host提供的信息就有效了。
User-Agent代表的是客户端对应的操作系统和浏览器的版本信息。
比如当我们用电脑下载某些软件时,它会自动向我们展示与我们操作系统相匹配的版本,这实际就是因为我们在向目标网站发起请求的时候,User-Agent字段当中包含了我们的主机信息,此时该网站就会向你推送相匹配的软件版本。
Referer代表的是你当前是从哪一个页面跳转过来的。Referer记录上一个页面的好处一方面是方便回退,另一方面可以知道我们当前页面与上一个页面之间的相关性。
Keep-Alive(长连接)
HTTP/1.0是通过request&response的方式来进行请求和响应的,HTTP/1.0常见的工作方式就是客户端和服务器先建立链接,然后客户端发起请求给服务器,服务器再对该请求进行响应,然后立马断开连接。
但如果一个连接建立后客户端和服务器只进行一次交互,就将连接关闭,就太浪费资源了,因此现在主流的HTTP/1.1是支持长连接的。所谓的长连接就是建立连接后,客户端可以不断的向服务器一次写入多个HTTP请求,而服务器在上层依次读取这些请求就行了,此时一条连接就可以传送大量的请求和响应,这就是长连接。
如果HTTP请求或响应报头当中的Connect字段对应的值是Keep-Alive,就代表支持长连接。
close
表明客户端或服务器想要关闭该网络连接,这是 HTTP/1.0 请求的默认值
HTTP 是一种无状态 (stateless) 协议, HTTP协议本身不会对发送过的请求和相应的通信状态进行持久化处理。这样做的目的是为了保持HTTP协议的简单性,从而能够快速处理大量的事务, 提高效率。
然而,在许多应用场景中,我们需要保持用户登录的状态或记录用户购物车中的商品。由于HTTP是无状态协议,所以必须引入一些技术来记录管理状态,例如Cookie。
Cookie的应用 :
当我们第一次登录某个网站时,需要输入我们的账号和密码进行身份认证,此时如果服务器经过数据比对后判定你是一个合法的用户,那么为了让你后续在进行某些网页请求时不用重新输入账号和密码,此时服务器就会进行Set-Cookie的设置。(Set-Cookie也是HTTP报头当中的一种属性信息)
当认证通过并在服务端进行Set-Cookie设置后,服务器在对浏览器进行HTTP响应时就会将这个Set-Cookie响应给浏览器。而浏览器收到响应后会自动提取出Set-Cookie的值,将其保存在浏览器的cookie文件当中,此时就相当于我们的账号和密码信息保存在本地浏览器的cookie文件当中。
从第一次登录认证之后,浏览器再向该网站发起的HTTP请求当中就会自动包含一个cookie字段,其中携带的就是我第一次的认证信息,此后对端服务器需要对你进行认证时就会直接提取出HTTP请求当中的cookie字段,而不会重新让你输入账号和密码了。
也就是在第一次认证登录后,后续所有的认证都变成了自动认证,这就是cookie技术。
Cookie 的类型 :
cookie
就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。cookie文件可以分为两种,一种是内存级别的cookie文件,另一种是文件级别的cookie文件。
将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的。
将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的。
Session的引入:
网络中有一些病毒或者钓鱼链接,它们可能盗取你的Cookie信息,如果你浏览器当中保存的cookie信息被非法用户盗取了,那么此时这个非法用户就可以用你的cookie信息,以你的身份去访问你曾经访问过的网站,所以单纯的使用cookie是非常不安全的,因为此时cookie文件当中就保存的是你的私密信息,一旦cookie文件泄漏你的隐私信息也就泄漏,所以当前主流的服务器还引入了Session
这样的概念。
Session的工作原理 :
当我们第一次登录某个网站输入账号和密码后,服务器认证成功后还会服务端创建一个Session对象(可以是内存级别也可以是文件级别),这个对象里面包含了我们用户的相关信息,此外生成Session对象都有一个唯一的SessionID,系统会将所有用户的SessionID值统一维护起来。
此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的SessionID值响应给浏览器。浏览器收到响应后会自动提取出SessionID的值,将其保存在浏览器的cookie文件当中(此时Cookie保存的只有SessionID,而没有用户的相关信息,相关信息在服务器中的Session对象中),后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个SessionID。
而服务器识别到HTTP请求当中包含了SessionID,就会提取出这个SessionID,然后再到对应的集合当中进行对比,对比成功就说明这个用户是曾经登录过的,此时也就自动就认证成功了,然后就会正常处理你发来的请求,这就是我们当前主流的工作方式。
正确的理解安全:
引入SessionID之后,浏览器当中的cookie文件保存的是SessionID,此时这个cookie文件同样可能被盗取。此时用户的账号和密码虽然不会泄漏了,但用户对应的SessionID是会泄漏的,非法用户仍然可以盗取我的SessionID去访问我曾经访问过的服务器,相当于还是存在刚才的问题。
这种方法虽然没有真正解决安全问题,但这种方法是相对安全的。互联网上是不存在绝对安全这样的概念的,任何安全都是相对的,就算你将发送到网络当中的信息进行加密,也有可能被别人破解。
不过在安全领域有一个准则:如果破解某个信息的成本已经远远大于破解之后获得的收益(说明做这个事是赔本的),那么就可以说这个信息是安全的。
引入SessionID后的好处 :
此时虽然SessionID可能被非法用户盗取,但服务器也可以使用各种各样的策略来保证用户账号的安全。
IP是有归类的,可以通过IP地址来判断登录用户所在的地址范围。如果一个账号在短时间内登录地址发送了巨大变化,此时服务器就会立马识别到这个账号发生异常了,进而在服务器当中清除对应的Session对象。这时当你或那个非法用户想要访问服务器时,就都需要重新输入账号和密码进行身份认证,而只有用户自己是知道自己的密码的,当你重新认证登录后服务器就可以将另一方识别为非法用户,进而对该非法用户进行对应的黑名单/白名单认证。
当操作者想要进行某些高权限的操作时,会要求操作者再次输入账号和密码信息,再次确认身份。就算你的账号被非法用户盗取了,但非法用户在改你密码时需要输入旧密码,这是非法用户在短时间内无法做到的,因为它并不知道你的密码。这也就是为什么账号被盗后还可以找回来的原因,因为非法用户无法在短时间内修改你的账号密码,此时你就可以通过追回的方式让当前的SessionID失效,让使用该账号的用户进行重新登录认证。
SessionID也有过期策略,比如SessionID是一个小时内是有效的。所以即便你的SessionID被非法用户盗取了,也仅仅是在一个小时内有效,而且在功能上受约束,所以不会造成太大的影响。
实验演示 :
当浏览器访问我们的服务器时,如果服务器给浏览器的HTTP响应当中包含Set-Cookie字段,那么当浏览器再次访问服务器时就会携带上这个cookie信息。
因此我们可以在服务器的响应报头当中添加上一个Set-Cookie字段,看看浏览器第二次发起HTTP请求时是否会带上这个Set-Cookie字段。
// 服务器端
std::string response = "HTTP/1.0 200 OK" + tol::Sep;
// 设置Cookie
response += "Set-Cookie: 1234aaaa" + tol::Sep;
response += tol::Sep;
运行服务器后,用浏览器访问我们的服务器,此时通过Fiddler可以看到我们的服务器发给浏览器的HTTP响应报头当中包含了这个Set-Cookie字段。
同时我们也可以在浏览器当中看到这个cookie,此时浏览器当中就写入了这样的一个cookie。
然后我们进行刷新网页,这相当于第二次访问我们的服务器,此时通过Fiddler可以看到,第二次的HTTP请求当中会携带上这个cookie信息。