文章目录
1 🍑URL🍑
平时我们俗称的 “网址” 其实就是说的 URL.
当我们向浏览器中搜索关键字时,我们可以将最上面的URL粘贴下来看看:
我们发现上面有服务器的地址,但是却没有服务器的端口号,这是因为服务器的端口号是众所周知的,浏览器底层已经帮助我们添加了所要访问服务器的端口号。
urlencode和urldecode:
像 / ? :
等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现。 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面上%
,编码成%XY
格式。
比如下面:
urldecode就是urlencode的逆过程。
2 🍑HTTP协议格式🍑
2.1 🍎HTTP请求协议格式🍎
- 首行: [方法] + [url] + [版本];
Header
: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束;Body
: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length
属性来标识Body的长度。
我们可以在代码中来验证验证:
这里写的代码我们之前已经全部都写过了,所以这里就不在多解释了。
Sock.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
using namespace std;
const int defaultSock=-1;
const int gbacklog=32;
class Sock
{
public:
Sock()
{}
void Socket()
{
_sock=socket(AF_INET,SOCK_STREAM,0);
if(_sock<0)
{
cout<<"sock fail:code"<<errno<<",err string:"<<strerror(errno)<<endl;
exit(1);
}
cout<<"sock success"<<endl;
}
void Bind(const uint16_t &port)
{
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_sock, (sockaddr *)&local, sizeof(local)) < 0)
{
cout<<"bind fail:code"<<errno<<",err string:"<<strerror(errno)<<endl;
exit(2);
}
}
void Listen()
{
if (listen(_sock, gbacklog) < 0)
{
cout<<"listen fail:code"<<errno<<",err string:"<<strerror(errno)<<endl;
exit(3);
}
}
int Accept(string *clientip, uint16_t *clientport)
{
sockaddr_in temp;
socklen_t len = sizeof(temp);
int sock = accept(_sock, (sockaddr *)&temp, &len);
if (sock < 0)
{
cout<<"accept fail:code"<<errno<<",err string:"<<strerror(errno)<<endl;
}
else
{
*clientip = inet_ntoa(temp.sin_addr);
*clientport = ntohs(temp.sin_port);
}
return sock;
}
int Connect(const string &serverip, const uint16_t &serverport)
{
sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
return connect(_sock, (sockaddr *)&server, sizeof(server));
}
int Fd()
{
return _sock;
}
~Sock()
{
if (_sock != defaultSock)
close(_sock);
}
private:
int _sock=defaultSock;
};
HttpServer.hpp:
#include <iostream>
#include "Sock.hpp"
#include <functional>
#include <pthread.h>
using namespace std;
using fun_t = function<const string &(const string &)>;
class HttpServer;
class ThreadData
{
public:
ThreadData(int sock, const std::string &ip, const uint16_t &port, HttpServer *tsvrp)
: _sock(sock), _ip(ip), _port(port), _tsvrp(tsvrp)
{
}
~ThreadData() {}
public:
int _sock;
std::string _ip;
uint16_t _port;
HttpServer *_tsvrp;
};
class HttpServer
{
public:
HttpServer(fun_t funt, uint16_t port)
: _funt(funt), _port(port)
{
}
void Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
}
void HandlerHttpRequest(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 = _funt(request);
send(sock, response.c_str(), response.size(), 0);
}
else
{
cout<<"client quit……"<<endl;
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->_tsvrp->HandlerHttpRequest(td->_sock);
close(td->_sock);
delete td;
return nullptr;
}
void Start()
{
while(true)
{
std::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);
}
}
private:
uint16_t _port;
Sock _listensock;
fun_t _funt;
};
Main.cc:
#include<iostream>
#include"HttpServer.hpp"
#include<memory>
using namespace std;
const string& HandlerHttp(const string& message)
{
cout<<"-------------------------------------"<<endl;
cout<<message<<endl;
cout<<endl;
return message;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
exit(-1);
}
uint16_t port = stoi(argv[1]);
unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp, port)); // TODO
tsvr->Init();
tsvr->Start();
return 0;
}
当我们使用云服务器的IP地址+端口号在浏览器上访问时:
服务器就会收到如下信息:
上面有一个比较重要的信息Connection
,后面跟着一个Keep-alive
表示长连接,这样使用长连接的优势是在于当有些情况需要传送较多报文时不用再像短连接(close
)那样重新断开再建立连接,效率会得到一定的提升。
在补充一些其他字段:
- HTTP协议通过3xx状态码来设定重定向,并通过
Location
字段来设定新的请求URL; User-Agent
: 声明用户的操作系统和浏览器版本信息;Content-Type
: 正文数据类型,用于告知对端正文的编码类型及处理方式,比如 Content-Type: text/html; charset=utf-8 用于表示正文是 text/html文档类型,字符集为utf-8;Host
: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;Content-Language
:用于表示用户希望采用的语言或语言组合,比如 Content-Language: de-DE 表示该文件为说德语的人提供,但是要注意者不代表文件内容就是德语的。
通过我们之前画的HTTP响应报文可知,我们好像无法将报头跟有效载荷进行分离?但实际上并非如此,在报头字段中存在着一个有效载荷长度的字段,当读取了有效载荷之后自然就可以知道有效载荷的大小,而且为了区分是何种资源(网页,视频,音频,图片),我们还得加上ContentType
.
ContentType:内容类型,一般是指网页中存在的Content-Type,用于定义网络文件的类型和网页的编码,决定文件接收方将以什么形式、什么编码读取这个文件,这就是经常看到一些Asp网页点击的结果却是下载到的一个文件或一张图片的原因。
有了上面思路后我们就可以采用下面的方式来写代码:
#include "HttpServer.hpp"
#include "Util.hpp"
#include <unordered_map>
#include <memory>
#include <vector>
using namespace std;
const string SEP = "\r\n";
// 一般一个webserver,不做特殊说明,如果用户之间默认访问‘/’, 我们绝对不能把整体给对方
// 需要添加默认首页!!而且,不能让用户访问wwwroot里面的任何一个目录本身,也可以给每一个目录都带上一个默认首页!
const string defaultHomePage = "index.html";
const string webRoot = "./wwwroot"; // web根目录
class HttpRequest
{
public:
HttpRequest() : path_(webRoot)
{
}
~HttpRequest() {}
void Print()
{
printf("method: %s, url: %s, version: %s",
method_.c_str(), url_.c_str(), httpVersion_.c_str());
printf("path: %s", path_.c_str());
printf("suffix_: %s", suffix_.c_str());
}
public:
string method_;
string url_;
string httpVersion_;
vector<string> body_;
string path_;
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(const 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";
return content_type + SEP;
}
string HandlerHttp(string &message)
{
//1. 读取请求
//确信,request一定是一个完整的http请求报文
//给别人返回的是一个http response
//资源,图片(.png, .jpg...),网页(.html, .htm),视频(.mp3),音频(..)->文件!都要有自己的后缀!
//2. 反序列化和分析请求
HttpRequest req = Deserialize(message);
req.Print();
// 3. 使用请求
string body;
bool ret = Util::ReadFile(req.path_, &body);
// /a/b/c.html
string response;
if (true == Util::ReadFile(req.path_, &body))
{
response = "HTTP/1.0 500 OK" + SEP;
response += "Content-Length: " + to_string(body.size()) + SEP;
response += GetContentType(req.suffix_);
response += SEP;
response += body;
}
cout<<response<<endl;
return response;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
exit(-1);
}
uint16_t port = stoi(argv[1]);
unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp, port));
tsvr->Init();
tsvr->Start();
return 0;
}
这里面有几个注意事项:
- 1️⃣
我们进行反序列化就是将其序列化的字段解析,方便我们能够拿到这些字段。 - 2️⃣
上述ReadFile函数的作用是将文件中资源的数据用string形式来读取,具体方式可以参照下面:
- 3️⃣
上述这两个函数的实现参考下面:
其本质都是分割出我们想要的字符串。
我们在wwwroot目录下(weeb根目录)创建一个imaga目录,向里面添加一个1.gpg,这里可以使用wget
命令。另外在与image同级下创建3个html文件。
这时我们就可以来验证了:
当我们不使用路径时就会打开默认首页:
当我们使用路径访问时:
这时可以清晰的看见都能够访问成功了。
2.2 🍎HTTP响应协议格式🍎
- 首行: [版本号] + [状态码] + [状态码解释];
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束;
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中。
2.3 🍎HTTP的请求方法🍎
HTTP的请求方法:
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0/1.1 |
POST | 传输实体主体 | 1.0/1.1 |
PUT | 传输文件 | 1.1/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 |
HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法;
HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
其中最常用的就是GET
方法和POST
方法。
GET方法一般用于获取某种资源,而POST一般是用来提交表单数据的(POST方法也可以上传数据),其中GET和POST方法都可以携带参数:其中POST
方法是通过正文
传参,而GET
方法是通过url
传参。
所以我们不难推出使用POST方法是能够传递更多参数的,因为url的长度是有着限制的。但是大家一定要注意一点使用GET/POST方法都是不安全的,因为都是明文传输,但是使用POST方法比使用GET方法要私密一些,因为数据是在正文中而不是在url中会回显。
其他一些常见问题:
- POST请求主要用于向服务器提交表单数据,因此POST请求不会被缓存,POST请求不会保留在浏览器历史记录当中,POST请求不能被保存为书签,POST请求对数据长度没有要求,所以我们无法从浏览器历史记录中查找到POST请求。
- PUT方法请求服务器去把请求里的实体存储在请求URI(Request-URI)标识下:
1.如果请求URI(Request-URI)指定的的资源已经在源服务器上存在,那么此请求里的实体应该被当作是源服务器关于此URI所指定资源实体的最新修改版本。
2.如果请求URI(Request-URI)指定的资源不存在,并且此URI被用户代理定义为一个新资源,那么源服务器就应该根据请求里的实体创建一个此URI所标识下的资源。如果一个新的资源被创建了,源服务器必须能向用户代理(user agent) 发送201(已创建)响应。如果已存在的资源被改变了,那么源服务器应该发送200(Ok)或者204(无内容)响应。
2.4 🍎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)
还有一些状态码大家可以了解下:
- 403:服务器拒绝接收到请求但拒绝提供服务,原因较多,比如权限不足,IP被拉入黑名单等。
- 301:状态码表明目标资源被永久的移动到了一个新的 URI,任何未来对这个资源的引用都应该使用新的 URI。
- 303:303状态码表示服务器要将浏览器重定向到另一个资源。从语义上讲,重定向到的资源并不是你所请求的资源,而是对你所请求资源的一些描述。比如303 常用于将 POST 请求重定向到 GET 请求,比如你上传了一份个人信息,服务器发回一个 303 响应,将你导向一个“上传成功”页面。
- 307:307的定义实际上和 302 是一致的,唯一的区别在于,307 状态码不允许浏览器将原本为 POST 的请求重定向到 GET 请求上。
- 308:308的定义实际上和 301 是一致的,唯一的区别在于,308 状态码不允许浏览器将原本为 POST 的请求重定向到 GET 请求上。
- 503:由于临时的服务器维护或者过载,服务器当前无法处理请求;这个状况是临时的,并且将在一段时间以后恢复。
2.5 🍎HTTP常见Header🍎
- Content-Type: 数据类型(text/html等);
- Content-Length: Body的长度;
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能.
这里我们再来回忆下有哪些常用端口号:
0 - 1023
: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的;
ssh
服务器, 使用22
端口;
ftp
(文件传输协议)服务器,20
端口号用于传输数据,21
端口号用于传输控制;
telnet
服务器, 使用23
端口;
http
服务器, 使用80
端口;
https
服务器, 使用443
端口.1024 - 65535
: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的;
这里面比较有意思的就是Cookie
了,我们来看看下面这种场景:
当我们打开腾讯视频的官网并且向里面登录我们的账号:
当我们干掉网页再次打开腾讯视频的官网时:
我们发现账号依然是登录着的,不用我们再次扫码验证了,为什么呢?其实这个就跟Cookie
有关:
Cookie
是一种保存在客户端的小型文本文件,用于保存服务器通过Set-Cookie
字段返回的数据,在下次请求服务器时通过Cookie字段将内容发送给服务器(只能存储字符串)。是HTTP进行客户端状态维护的一种方式。Cookie
中记录了我们上次登录时的密码(有一定的时间限制,过了一定时间就自动销毁密码了)。cookie的可以记录用户的ID,记录用户的密码,记录用户浏览过的商品记录。但是无法记录用户的浏览器设置。(浏览器设置属于浏览器,而并不属于某次请求的信息)
Cookie
的基础属性:
domain
:可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”。path
:Cookie的使用路径。如果设置为“/sessionWeb/”,则只有contextPath为“/sessionWeb”的程序可以访问该Cookie。如果设置为“/”,则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为“/”。httponly
:如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性,但不是绝对防止了攻击secure
:该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。expires
:指定了coolie的生存期,默认情况下cookie是暂时存在的,他们存储的值只在浏览器会话期间存在,当用户退出浏览器后这些值也会丢失,如果想让cookie存在一段时间,就要为expires属性设置为未来的一个过期日期。现在已经被max-age属性所取代,max-age用秒来设置cookie的生存期.
那我们可不可以猜测一下,当我们向浏览器中输入密码时,浏览器会默认保存一下密码,当我们再次请求时会向请求报文中增加我们保存的密码,自然就不用验证了。大家想想上述的猜测会存在什么明显的漏洞?我们如果将密码保存,下次发送报文时在将密码发送给服务器时如果此时有第三方抓包,将密码给窃取了那么可就完蛋了,用户的隐私和权限全部都成了透明的了,所以这样猜测明显时不合理的,那么我们应该采取怎样的措施呢?
因此有了Session
管理。session是服务器为了保存用户状态而创建的临时会话,或者说一个特殊的对象,保存在服务器中,将会话ID通过cookie进行传输即可,就算会话ID被获取利用,但是session中的数据并不会被恶意程序获取,这一点相对cookie来说就安全了一些,但是session也存在一些缺陷,需要建立专门的session集群服务器,并且占据大量的存储空间(要保存每个客户端信息)
服务器会使用某种算法将Session
生成唯一的Sessionid
,浏览器发送报文时并不是发送真正的密码,而是将Sessionid给发送过去,而服务器中维护着大量的Sessionid结构,当发送的请求报文中的Sessionid存在于服务器中维护的Sessionid结构中就让其通过验证即可。这样做的好处是用户的密码就是隐私的了,第三方抓包抓的也是Sessionid,并没有抓到用户的密码,用户的隐私得到了一定的保证(除此之外为了保护用户的隐私可能还有其他的措施,比如IP地址变动范围过大以及非正常的活跃等等)。
另外要注意Cookie
数据是在HTTP协议头部字段中传输,不是在正文中。