Linux之Http协议分析以及cookie和session
- 一.分析请求行与响应行
- 1.1请求行
- 1.1.1资源的URL路径
- 1.1.2常见的方法
- 1.2响应行
- 二.cookie和session
- 2.1cookie
- 2.2session
一.分析请求行与响应行
在我们简单了解了请求和响应的格式以及模拟实现了请求和响应后我们已经可以通过网页来访问我们的服务器从而获得一些简单的信息,但是这些还只是http协议的冰山一角,今天我们还需要继续分析请求和响应中的首行从而来更了解http协议。
1.1请求行
请求行中一共分为三部分:方法+URL路径+版本号。其中版本号我们在上篇文章中已经谈过其中的内容也相对来说不那么重要,而方法我们也只要会用最常见的两种以及记住其余的大致作用即可但是URL路径却是我们最需要谈的。
1.1.1资源的URL路径
我们先来再看一下我们的请求行中存储的URL路径
这里的我只用了IP:端口号的方式在浏览器上访问服务器而这时URl路径显示的就只是一个/符号那么如果我在IP:端口号的基础上加上路径呢?那时候请求行中的URL路径会发生改改变吗?
我将路径改成了/a/b后很明请求行中的URL路径也发生了改变,就说明如果我们访问时在端口后不加上路径那么就会默认访问/路径下的资源,如果自己添加了路径就会访问该路径下的资源。可是问题是这个/是什么呢?为什么不添加路径就会默认访问该路径下的资源?
http协议规定的是如果我们不自己添加访问的路径,那么就会默认访问web根目录而web根目录通俗一点的话就是访问网址的首页那么具体是访问根目录下的哪个资源呢?其实我们默认访问的是/index.html。
可能大家会说我们模拟实现的代码里并没有什么index.html资源啊而且你说自己填路径和访问web根目录时访问的资源是不一样的那么为什么我们上面的图片里都是一样的helloworld呢?这是我们编写的代码的缘故我们现在并没有对URL路径加以分析而且一股脑的对于访问都塞一个一样的响应回去所以导致如此结果。
那么我们要如何区分URL路径而且那些不同路径下的资源又要怎么存放呢?
在Linux中一切皆文件所以大家很容易可以想到我们可以创建几个不同的文件来存放资源到时候只要我们对URL路径进行分析再让其去访问其中的内容即可。所以我们可以创建一共目录来存放这些不同路径下的资源,而这个目录在网络编程下一般都是设为wwwroot也可以是webroot。接下来我就来给大家模拟实现一下我们所说的这个方法。
// HttpProtocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <fstream>const static std::string HttpSep = "\r\n";// 默认访问的资源
const static std::string homepage = "index.html";
const static std::string wwwroot = "./wwwroot";class HttpRequest
{
public:HttpRequest(): _blankline(HttpSep), _path(wwwroot){}~HttpRequest(){}bool Getline(std::string &request, std::string *line){// 请求行\r\n请求报头\r\n空行\r\n请求正文// 在请求中查找HttpSepauto pos = request.find(HttpSep);if (pos == std::string::npos){return false;}// 分割请求*line = request.substr(0, pos);request.erase(0, pos + HttpSep.size());return true;}bool Deserializa(std::string &request){std::string line;// 判断是否只有请求行bool ok = Getline(request, &line);if (!ok){return false;}_req_line = line;// 一行一行的分割请求while (true){bool ok = Getline(request, &line);if (ok && line.empty()){// 如果分割行成功并且line中存储是空行// 说明request只剩下了正文(无论正文是否有内容)_req_content = request;break;}else if (ok && !line.empty()){// 如果分割成功并且line内有内容// 说明line中存储的是请求报头中的一行_req_header.push_back(line);}else{break;}}return true;}void DebugHttp(){std::cout << "_req_line:" << _req_line << std::endl;std::cout << "_req_header:" << std::endl;for (auto &line : _req_header){std::cout << "---> " << line << std::endl;}std::cout << "_req_blank: " << _blankline << std::endl;std::cout << "_req_content: " << _req_content << std::endl;std::cout << "Method: " << _method << std::endl;std::cout << "url: " << _url << std::endl;std::cout << "path: " << _path << std::endl;std::cout << "http_version: " << _http_version << std::endl;}// 分析请求行void AnalyseReqline(){// 大家可以用find来查找空格之后再进行分割的方法// 但是我这里就使用C++中专门处理字符串的输入输出流stringstream// 使用sstream需要包含头文件<sstream>std::stringstream ss(_req_line);// 请求行:method path version// 而stringstream的插入会自动以空格为分割ss >> _method >> _url >> _http_version;// 如果_url为根目录就还需要添加默认的资源if (_url == "/"){_path += _url;_path += homepage;}else{// 不是根目录则直接加URL路径即可_path += _url;}}// 分析后缀void AnalyseSuffix(){// index.html我们只需要拿到.和其之后的字符即可auto pos = _path.rfind(".");if (pos != std::string::npos){_suffix = _path.substr(pos);}else{_suffix = ".html";}}// 对资源文件做处理std::string GetFileContentHelper(const std::string &path){// 因为是对文件做处理所以使用ifstreamstd::cout << "this1" << std::endl;std::cout << path << std::endl;std::ifstream in(path, std::ios::binary);std::cout << "this2" << std::endl;// 打开文件,失败返回""成功则读取其中内容if (!in.is_open()){std::cout << "this3" << std::endl;return "";}std::cout << "this4" << std::endl;std::string content;std::string line;// 一行一行的读取while (std::getline(in, line)){content += line;}in.close();return content;}std::string GetFileContent(){return GetFileContentHelper(_path);}void Analyse(){// 1.分析请求行AnalyseReqline();// 2.分析后缀AnalyseSuffix();}std::string GetUrl(){return _url;}std::string GetPath(){return _path;}std::string GetSuffix(){return _suffix;}private:// 根据请求格式我们可以设计出四个变量std::string _req_line; // 请求行std::vector<std::string> _req_header; // 请求报头std::string _blankline; // 空行std::string _req_content; // 请求正文// 分析请求行后std::string _method; // 方法std::string _url; // 路径std::string _http_version; // 版本// 分析URL后将其分为路径和后缀两部分// 因为资源有很多种类所以我们需要判别资源的种类再加以不同处理std::string _path;std::string _suffix;
};
// httpserver.cc
#include <iostream>
#include <memory>#include "HttpProtocol.hpp"
#include "Tcpserver.hpp"// 文件的后缀决定了文件的类型,http协议需要响应报头中包含正文的文件类型
std::string SuffixToType(const std::string &suffix)
{if (suffix == ".html" || suffix == ".htm"){return "text/html";}else if (suffix == ".png"){return "image/png";}else if (suffix == ".jpg"){return "image/jpeg";}else{return "Unknown";}
}std::string HandleMethod(std::string request)
{HttpRequest http_req;http_req.Deserializa(request);http_req.Analyse();http_req.DebugHttp();std::string content = http_req.GetFileContentHelper(http_req.GetPath());if (!content.empty()){std::string httpstatusline = "Http/1.0 200 OK\r\n";std::string httpheader = "Content-Length: " + std::to_string(content.size()) + "\r\n"; // 正文的大小httpheader += "Content-Type: " + SuffixToType(http_req.GetSuffix()) + "\r\n"; // 正文的类型httpheader += "\r\n";std::string httpresponse = httpstatusline + httpheader + content;return httpresponse;}return "";
}int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usge:\n"<< argv[0] << " port" << std::endl;return 0;}uint16_t localport = std::stoi(argv[1]);std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandleMethod));svr->Loop();return 0;
}
}
我们的资源有很多种类型,一般常见的就是.html,.png等等。对应.html文件的编写是我们前端人员的工作所以大家可以自行去搜索学习一下。
这是我们现在用浏览器访问服务器web根目录资源的样子,大家可以发现我们的图片加载不出来这是因为我们的图片在解析的时候是全二进制的但是我们如今使用的对资源文件做处理是针对于文本文件的如.html文件所以我们还需要将处理方法改进成通用方法才行。
// 对资源文件做处理std::string GetFileContentHelper(const std::string &path){// 文本// // 因为是对文件做处理所以使用ifstream// std::ifstream in(path, std::ios::binary);// // 打开文件,失败返回""成功则读取其中内容// if (!in.is_open())// {// return "";// }// std::string content;// std::string line;// // 一行一行的读取// while (std::getline(in, line))// {// content += line;// }// in.close();// return content;// 通用方法std::ifstream in(path, std::ios::binary);if (!in.is_open()){return "";}// 跳转到in的末尾in.seekg(0, in.end);// 将in的大小填入filesize中int filesize = in.tellg();// 跳转到in的开头in.seekg(0, in.beg);std::string content;// 设置content的大小content.resize(filesize);// 将in的内容填入到content中in.read((char *)content.c_str(), filesize);in.close();return content;}
1.1.2常见的方法
在我们了解常见的方法前我们需要先知道上网这个行为的本质是什么,本质就是我们从网上上获取资源或者我们向服务器传送参数也就是资源,而我们最常用的GET方法和POST方法就包含了这两方面。
-
GET方法
通常用来获取资源也可以通过URL地址来传参,但是传输的参数私密性不强。传参这个动作需要依靠我们html文件的表单来实现
那么我们向里面输入信息后URL地址会发生怎么样的变化呢?
那么我们如果向里面填写信息呢信息会被加入到URL里吗?
至于为什么私密性不强我想大家一眼就看出来了因为在URL地址中所有人都能看见当然私密性不强了。 -
POST方法
通常用来上传数据但是POST方法上传数据的形式和GET不同,POST方法不会将数据存在URL地址中而是存于请求正文中传给服务器,通过请求正文传参那么肉眼可见的POST传参的私密性会好一点。
同时由于GET是利用URL传参所以会受到URL自身被规定的长度所限制但是POST方法则不会,虽然POST方法的私密性比GET方法好一点但是其实两者都不安全。那么我们想要我们传输的账号密码安全就必须经过加密和解密的过程这也就形成了我们之后的https协议。
至于其余的方法我就直接用别的博主写的来列给大家看看了因为都不是很重要
-
HEAD
获得报文首部,类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头。欲判断某个资源是否存在,我们通常使用GET,但这里用HEAD则意义更加明确。
向服务器获取某些易过期或丢失大型文件时,可用HEAD方式查询资源是否存在。 -
OPTIONS
询问支持的方法客户端询问服务器可以提交哪些请求方法。这个方法很有趣,它用于获取当前URL所支持的方法。若请求成功,则它会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST”。
极少使用。 -
PUT
传输文件。从客户端向服务器传送的数据取代指定的文档的内容,即指定上传资源存放路径。这个方法比较少见。HTML表单也不支持这个。本质上来讲, PUT和POST极为相似,都是向服务器发送数据,但它们之间有一个重要区别,PUT通常指定了资源的存放位置,而POST则没有,POST的数据存放位置由服务器自己决定。 -
PATCH
局部更新文件,是对 PUT 方法的补充,用来对已知资源进行局部更新 。
极少使用。 -
DELETE
删除文件。请求服务器删除指定的资源。
基本上这个也很少见,不过还是有一些地方比如amazon的S3云服务里面就用的这个方法来删除资源。 -
TRACE
追踪路径,回显服务器收到的请求,客户端可以对请求消息的传输路径进行追踪,TRACE方法是让Web服务器端将之前的请求通信还给客户端的方法。主要用于测试或诊断。
极少使用。 -
CONNECT
要求用隧道协议连接代理,HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。CONNECT方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信。主要使用SSL(安全套接层)和TLS(传输层安全)协议把通信内容加密后经网络隧道传输。
极少使用。
原文链接:https://blog.csdn.net/demo_yo/article/details/123596028
1.2响应行
对应响应行的分析就简单多了我们只需要分析状态码即可,状态码也很好理解就是告诉你完成的情况或者有一些特殊情况。所以我直接给状态码贴给大家我拿其中比较典型的几个给大家说一说然后我们也还需要模拟实现一下不同状态码的情况。
一共五大类状态码分别以12345开头每大类中还需要细分,我就给每大类中典型的跟大家解释一下这样大家也对五种状态码有个概念。
- 信息状态码(1xx)
信息状态码我们只需要记住100即可,100状态码出现的情况一般是当我们从客户端往服务器传输数据时过大服务器无法一次性接收完全,当服务器接收了一半之后就会传出100状态码来告诉客户端你可以继续上传数据了。所以100状态码就是继续的含义。 - 成功状态码(2xx)
成功状态码我们需要记住200和204。200很简单就是告诉你请求被处理成功了而204也是告诉你请求被处理成功了但是没有结果返回。 - 重定向状态码(3xx)
重定向状态码需要大家理解并且记住301和302分别是永久重定向和临时重定向。首先我们来解释一下什么是http里的重定向,我给大家举两个例子一个是当我们创建的网站过期了之后我们需要换IP地址换域名了但是用户只记得旧网站的网址另外一个是我们在各种的网站中会登入自己的账号有时候在登入账号后会自动条状到自己账号的主页。第一个需要我们在将旧网站重定向到新网站从而达成用户只要登入旧网站就会自动跳转到新网站上,第二个是如何完成的呢也就像我们刚刚展示表单时我们把登入按钮重定向到用户自己的主页上这样这要我们登入成功了就会自动跳转。从这两个例子中我们可以发现重定向的作用一般就是从一个网址跳转到另外一个网址上,并且这两个例子中的重定向还不一样第一个例子是永久重定向因为旧网站不用了过期了只用新网站现在,第二个例子是临时重定向我们只需要在当时让用户完成登入后跳转到主页即可。 - 客户端错误状态码(4xx)
客户端错误状态码则需要大家记住403和404这两个即可。404状态码其实算这些状态码里比较常见的了,我们只需要访问一些不存在的网站就很容易得出这个状态码。
而403状态码则是我们如果访问了一些我们没有全新的网址就会出现。
这两个状态码是很好理解的但是比较难理解的是4xx状态码为什么叫做客户端错误状态码,明明是我访问你们服务器为什么变成我错了呢?其实从这两个状态码的名字里就能看出来一个是访问不存在的网址一个是访问没权限的网址,从服务器的角度上我明明有这么多页面可以给你看你偏偏找一个不存在的网址或者找一个你没权限的网址,我用心用力的给你服务你这不是给我找茬吗。所以这是用户自己作的妖当然就是客户端错误状态码了。
- 服务器错误状态码(5xx)
这个状态码需要大家记住500和503,服务器错误状态码平时应该很少见因为我们如今使用的软件大多都是一些大厂发行的他们的服务器一般都是比较稳定的除了个别公司。500状态码的含义是服务器崩溃或者数据库崩溃导致页面无法加载出来,503状态码是服务器正在维护或者过载导致暂时无法处理请求。
在了解了这些状态码后我们也可以来模拟实现一下这些状态码对应的情况
const static std::string Space = " ";// 响应
class HttpResponse
{
public:HttpResponse(): _version("HTTP/1.0"), _status_code(200), _status_code_desc("OK"), _blankline(HttpSep){}~HttpResponse(){}void SetCode(int status_code){_status_code = status_code;}void SetDesc(std::string status_code_desc){_status_code_desc = status_code_desc;}void MakeStatusLine(){_status_line = _version + Space + std::to_string(_status_code) + Space + _status_code_desc;}void Addheader(std::string header){_resp_header.push_back(header);}void Addcontent(std::string content){_resp_content = content;}std::string Serializa(){std::string response_str = _status_line;for (auto header : _resp_header){response_str += header;}response_str += _blankline;response_str += _resp_content;return response_str;}private:std::string _status_line; // 状态行std::vector<std::string> _resp_header; // 响应报头std::string _blankline; // 空行std::string _resp_content; // 响应正文std::string _version; // 版本int _status_code; // 状态码std::string _status_code_desc; // 状态码解释
};
// Tcpserver.cc
#include <iostream>
#include <memory>#include "HttpProtocol.hpp"
#include "Tcpserver.hpp"// 文件的后缀决定了文件的类型,http协议需要响应报头中包含正文的文件类型
std::string SuffixToType(const std::string &suffix)
{if (suffix == ".html" || suffix == ".htm"){return "text/html";}else if (suffix == ".png"){return "image/png";}else if (suffix == ".jpg"){return "image/jpeg";}else{return "Unknown";}
}std::string CodeToDesc(int code)
{if (code == 200){return "OK";}else if (code == 301){return "Moved Permanently";}else if (code == 307){return "Temporary Redirect";}else if (code == 403){return "Forbidden";}else if (code == 404){return "Not Found";}else if (code == 500){return "Internal Server Error";}else{return "Unknown";}
}std::string HandleMethod(std::string request)
{HttpRequest http_req;http_req.Deserializa(request);http_req.Analyse();http_req.DebugHttp();int code = 200;std::string content = http_req.GetFileContentHelper(http_req.GetPath());if (content.empty()){code = 404;content = http_req.GetFile404();}HttpResponse response;// 设置状态码response.SetCode(code);// 设置状态码描述response.SetDesc(CodeToDesc(code));// 生成状态行response.MakeStatusLine();// 插入响应报头std::string content_len_str = "Content-Length: " + std::to_string(content.size()) + "\r\n"; // 正文的大小response.Addheader(content_len_str);std::string content_type_str = "Content-Type: " + SuffixToType(http_req.GetSuffix()) + "\r\n"; // 正文的类型response.Addheader(content_type_str);// 插入响应正文response.Addcontent(content);// 序列化return response.Serializa();
}int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usge:\n"<< argv[0] << " port" << std::endl;return 0;}uint16_t localport = std::stoi(argv[1]);std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandleMethod));svr->Loop();return 0;
}
二.cookie和session
我们在学习了GET和POST方法后知道我们是可以通过请求正文给服务器传输信息的从而达成登入的操作但是不知道大家在日常使用中有没有发现我们只要登入了某个网站之后的一段时间我们就不需要再输入账号密码来登入了,这是为什么呢?
我们很容易就能想到一个答案:Http协议记住了我们的账号密码这样只要我们登入过一次之后就不用再登入了。但是这是错的这是因为Http协议是无状态无连接的,什么叫做无状态无连接呢也就是Http协议是不记住自己的历史的所以每次打开一个网站理论上是都是第一次打开因为http协议不记得你之前来过那不就是当第一次吗。这个问题也就导致了理论上我们每次都需要重新登入就算我们答应网站的公司也不答应啊,这样的操作不就很花费用户的时间和耐心也就会让网站的流量下降盈利也就下降了。所以针对这种情况延申出了两种解决方法:cookie和session,准确的来说session是在cookie的基础上改进了。
2.1cookie
想要解决这个问题其实很简单我们只需要在第一次登入的时候通过POST方法将账号密码传入到服务器中之后服务器再在响应报头中添加进去返回给浏览器,这样浏览器中就存有了你的账号密码之后每次再登入对应的网址的时候浏览器只要将你的账号密码通过请求报头传入到服务器让服务器查询是否有此用户即可。而cookie就是浏览器保存起来的报头形式的账号密码。
但是这样的解决方案依然有问题:浏览器保存的都是我真实的私密信息但是浏览器中的信息太过容易被盗取只要有人获得了我的私密信息就可以随意使用我的账号了。这就造成了用户的隐私安全问题。
我们现在使用的都是https协议所以cookie都是被加密过的但是在以前只用http协议时cookie信息是明晃晃的存在浏览器中的。
2.2session
所以有人为了避免这种问题将cookie改进成了session,而改进的地方就在于当服务器接收了来自浏览器POST的数据后它会通过这个数据生成一个结构体session和一个独一无二的sessionid并且这个session中还会存放用户的状态例如地理位置等等,之后就将账号密码再传回给浏览器而是将这个sessionid传给浏览器。浏览器只需要保存这个sessionid在下次登入的时候将sessionid传给服务器,服务器再进行查找判断即可。这样的好处是浏览器保存的就不是用户的真实隐私而是一串独特的sessionid并且在session中你不仅可以通过账号密码来判断用户是否合法还可以通过用户此次登入的地理位置和上一次的地理位置的距离来判断。
这样就解决了用户的隐私安全问题。