【Linux | 网络】应用层(HTTP)
目录
一、认识URL
平时我们俗称的 “网址” 其实就是说的 URL

域名会通过DNS被解析为服务器的IP地址。但是现在的服务器不允许用户直接通过IP地址请求服务。

二、urlencode和urldecode
像 / ? : 等这样的字符,已经被url当做特殊意义理解了。所以这些字符不能随意出现。
例如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。

“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程。
三、HTTP协议格式(使用Fiddler抓包)
3.1 安装并使用Fiddler抓包
Fiddler 下载地址:https://www.telerik.com/download/fiddler
这里我们使用软件 Fiddler 进行抓包,刚刚安装的软件无法对HTTPS进行抓包,如果需要对HTTPS进行抓包,需要进行以下操作。


接下来我们就可以看一下HTTP协议的格式了。

3.2 HTTP协议格式
将上面的内容进行提取就是下面图片中的内容了。这里我只提取了部分内容。

3.2.1 HTTP请求
- 提到请求报文来说,我们需要解决以下问题:
- 报头和有效载荷是如何分离的
- 有效载荷如何交付交付的
- 对于HTTP请求报文来说,我们需要解决以下问题:
- HTTP是如何做的读取到完整的报文的
- HTTP是如何进行反序列化的
- 回答问题
- HTTP的报头和有效载荷是通过空行进行分离的
- HTTP是顶层协议,所以不需要解决向上交付的问题
- 首先读取到空行,说明报头已经读取完整,报头中有一个字段为Content-Length,再读取Content-Length字节大小的内容,即可将请求正文的读取完整,结合起来就将整个报文读取完整
- 正文部分可以根据正文的类型进行反序列化,根据类型的不同,方式有很多种,这里先不考虑。其他部分则是通过\r\n反序列化即可。
3.2.1.1 资源URL路径
资源URL路径就是图片、网页、音频等资源的路径。
在服务器上,通常会使用一个特定的目录,来保存HTTP服务器的所有资源。
就如下图中,我这个简易的HTTP服务器中,就将主页和图片资源都放在了wwwroot这个目录下。web根目录是web服务器存储网站文件的顶级目录,由于HTTP服务器是web服务器的子集,所以我这里的wwwroot目录就可以被当做web根目录。
当用户使用的网站并没指定URL,则URL为 \ ,此时用户访问的就是web根目录下的默认文件(通常为首页)。

当用户使用的网站并指定了URL,则请求中的URL就是用户指定的URL。

3.2.1.2 请求方法(Method)
下面就是HTTP可以使用的方法,但是最常用的还是GET和POST方法。
GET:可以用来获取资源,也可以用来上传数据(参数)。
POST:可以用来上传数据(参数)。

在一些场景下,我们需要将数据上传给服务器,例如登录、注册、搜索等,我们通过GET/POST再结合HTML(HTML表单)来实现这样的场景。
下面我实现了一个简单的登录页面,当我输入了用户名和密码后,再点击提交,发现我提交的信息作为了参数被拼接到了URL中。

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>登录页面</title></head><body><formaction="login.html"> 用户名:<br><inputtype="text"name="name"value="chineseperson04"><br> 密码:<br><inputtype="text"name="password"><br><br><inputtype="submit"value="登录"></form><ahref="http://47.109.128.33:8888/index.html">回到首页</a></body></html>在HTML表单中我们还可以设置提交表单时使用的HTTP请求方法。
下面我就设置了HTTP请求方法为GET,当我提交参数后,参数就直接拼接到了URL中了。

这里我设置HTTP请求方法为POST,再次提交参数后,发现参数并没有拼接到URL中,而是保存到了HTTP的正文部分了。

当我默认没有设置HTTP请求方法,则HTTP请求方法默认为GET。

到这里我们就知道GET方法是直接将参数拼接到URL中,而POST方法是将参数保存到正文中。
这时候有的人就会认为POST方法比GET方法安全,实际上两者都不安全,因为HTTP是明文传输,虽然POST方法无法从URL中读取到参数,但是不代表无法被抓包,通过下图我们也可以看出来,所以所以POST方法比GET方法只是私密性好一些,并没有更安全。

3.2.1.3 Location头字段(重定向相关)
Location头字段:服务器在重定向响应中通过Location头字段提供新URL。
例如状态码中的301就代表着永久重定向,举个例子:搜索引擎可以爬取网页中的URL关键字,关键字又对应着无数个网络连接。当一个用户通过网页访问一个网站时,这个网站由于改变了域名导致老网站过期了,服务器给用户的响应中状态码为301,并且Location字段中保存着新网站的URL,这时候用户会直接跳转到新网站,然后再过一段时间后还可以使搜索引擎中该网站URL更新。
再例如状态码中的302和307代表临时重定向,这有什么用呢?在生活中,当我们想使用某一个刷题网站时,服务器检测到用户未登录则会设置状态码为302或307,并设置Location为登录页面的URL,该网站就会跳转到登录页面,当我们登录完毕后,服务器检测到用户登录完毕后,则会设置状态码为302或307,并设置Location为首页页面的URL,然后再次跳转到首页。临时重定向最主要的功能就是让用户跳转到目标网页。
3.2.2 HTTP响应
3.2.2.1 状态码和状态码描述
状态码:表示网页服务器超文本传输协议响应状态的3位数字代码
状态码描述:状态码对应的简短文本说明

下面是一些常见的状态码和状态码描述:
| 状态码 | 状态码描述 |
|---|---|
| 200 - OK | 代表一切正常,服务器处理客户端请求,并返回客户端请求的资源 |
| 204 - No Content | 与200基本相同,但是并不返回给客户端任何内容 |
| 301 - Moved Permanently | 永久重定向,请求资源已经不在,需要新的URL重新访问 |
| 302 - Found | 临时重定向,请求资源还在,但是暂时需要使用新的URL重新访问 |
| 304 - Not Modified | 缓存重定向,没有跳转的含义,表示资源还在,重定向已存在的缓存文件,也就是告诉客户端可以继续使用缓存资源 |
| 307- Temporary Redirect | 临时重定向,类似302,但明确要求客户端保持请求方法不变。 |
| 400 - Bad Request | 客户端报文有错误, 但是是笼统的错误 |
| 403 - Forbidden | 客户端报文没有错误,但是服务器禁止访问资源 |
| 404 - Not Found | 客户端请求的资源,在服务器上不存在 |
| 500 - Internal Server Error | 服务器发生错误,是个笼统的错误码 |
| 501 - Not Implemented | 客户端请求的资源,服务器目前还不支持 |
| 502 - Bad Gateway | 服务器运行正常,但访问后端服务器发生错误 |
| 503 - Service Unavailable | 服务器繁忙,无法进行响应 |
3.2.3 请求与响应共有字段
3.2.3.1 HTTP协议版本
注意:请求与响应中的HTTP协议版本的定义是不同的。
请求中的HTTP协议版本:浏览器支持的HTTP协议
响应中的HTTP协议版本:服务器支持的HTTP协议
请求声明客户端上线,响应声明服务器实现。版本匹配通过协商机制确保兼容性,这样可以让不同的客户端分别有不同的服务。
3.2.3.2 Content-Type
Content-Type在请求与响应中指的都是描述正文内容的类型。
- 请求中描述的是客户端发送的数据格式
- 响应中描述的是服务器返回的数据格式
关于Content-Type对应文件后缀,我这里列举一些常见的,其他的大家可以到HTTP content-type这个网站中了解。
| Content-Type | 文件后缀 |
|---|---|
| text/html | .html / .htm |
| application/json | .json |
| image/jpeg | .jpg / .jpeg |
| image/png | .png |
| application/pdf | |
| text/plain | .txt |
3.2.4 Cookie 和 Session
3.2.4.1 Cookie
首先我们需要知道HTTP是无连接和无状态的,无状态就是HTTP 协议在设计上不会保留客户端与服务器之间的交互状态,这就导致了服务器并不知道后序的请求是否来自同一个用户。
例如:用户想要在某刷题网站上刷题,这时候就需要用户登录,登录完毕后,用户回到了首页,此时用户想要完成第一道题目,这时候服务器不知道这个用户已经登录过了,需要让用户再次登录,当用户完成第一题,需要完成第二道题时,服务器又不认识这个用户了,又需要用户再次登录,多次登录势必会导致用户不想再次使用了。

如何解决用户需要多次登录的问题呢?这里就要提到Cookie了。
当用户首次登录成功以后,服务器会在响应报头中添加一个或多个SetCookie字段,当浏览器得到响应后,会将Cookie信息保存起来,当用户再次访问该网站时,浏览器会自动将Cookie信息添加到请求报头中,而服务器得到请求报头后,会自动根据请求报头中的Cookie信息进行认证,认证通过则将用户请求的资源发送给浏览器。

下面我使用Edge浏览器来演示如何看到Cookie,并且删除Cookie后发生什么。
这里我以B站为例,当我将Cookie删除后,当我再次访问B站时,就需要重新登录了。如果在登录状态下,再次访问则不需要登录。

但是仅仅这样让浏览器保存Cookie可能会导致用户的敏感信息泄露,例如下图,我向我的HTTP服务器发送请求,浏览器中确实保存了Cookie信息,但是很容易的就被抓包了。这样就可能导致别人拿着用户的Cookie信息,登录用户账号,所以Cookie信息不能保存在用户的浏览器中,下面的Session中会讲到。

3.2.4.2 Session
上面我们提到了浏览器保存Cookie信息会使得用户的敏感信息很容易就被抓包,所以我们就让服务器保存Cookie信息,当用户首次登录成功以后,服务器会创建一个Session(会话)来保存Cookie信息,并且使用一种方法在MySQL中生成一个唯一的Sessionid,将Sessionid添加到响应报头中,浏览器就会只会保存Sessionid,当用户再次访问的时候,浏览器会自动建Sessionid添加到请求报头中,服务器会自动根据Sessionid在服务器中查找并认证,认证通过则将用户请求的资源发送给浏览器。
虽然浏览器中的Sessionid会被抓包,别人也可以使用Sessionid来登录用户的账号,但是却无法抓取用户的敏感信息了。
如何减少别人登录用户的账号呢?大家可以去查一下,可以设置Session的时长,到时间后,Session就自动失效,需要用户再次使用账号密码进行登录,也可以匹配登录账号的地理位置,当位置相隔过大时,就让Session失效,让用户再次使用账号密码进行登录。
一个服务器中一定会有很多用户同时登录,那服务器中就会存在很多的Session(Sessionid),所以服务器一定需要对Session进行管理,管理就需要先描述再组织。(先描述)定义一个Session类,将一个用户的Cookie信息保存起来,(再组织)定义一个SessionManager类,使用某种数据结构将所有的Session管理起来,SessionManager中需要包含对Session的增删查改的方法。

四、实现一个简单的HTTP服务器
整体HTTP服务器文件结构

4.1 Socket.hpp(封装套接字)
#pragmaonce#include<iostream>#include<string>#include<string.h>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<errno.h>#defineCONV(addrptr)(structsockaddr*)addrptrenum{ Socket_err =1, Bind_err, Listen_err };conststaticint defalutsockfd =-1;constint defalutbacklog =5;classSocket{public:virtual~Socket(){};virtualvoidCreateSocketOrDie()=0;virtualvoidBindSocketOrDie(uint16_t port)=0;virtualvoidListenSocketOrDie(int backlog)=0;virtual Socket*AcceptConnection(std::string* ip ,uint16_t* port)=0;virtualboolConnectServer(const std::string& serverip ,uint16_t serverport)=0;virtualintGetSockFd()=0;virtualvoidSetSockFd(int sockfd)=0;virtualvoidCloseSockFd()=0;virtualboolRecv(std::string& buffer,int size)=0;virtualvoidSend(const std::string& send_string)=0;virtualvoidReUseAddr()=0;public:voidBuildListenSocketMethod(uint16_t port){CreateSocketOrDie();ReUseAddr();BindSocketOrDie(port);ListenSocketOrDie(defalutbacklog);}boolBuildConnectSocketMethod(const std::string& serverip ,uint16_t serverport){CreateSocketOrDie();returnConnectServer(serverip,serverport);}voidBuildNormalSocketMethod(int sockfd){SetSockFd(sockfd);}};classTcpSocket:publicSocket{public:TcpSocket(int sockfd = defalutsockfd):_sockfd(sockfd){}~TcpSocket(){};voidCreateSocketOrDie() override { _sockfd =::socket(AF_INET,SOCK_STREAM,0);if(_sockfd <0)exit(Socket_err);}voidBindSocketOrDie(uint16_t port) override {structsockaddr_in addr;memset(&addr,0,sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port =htons(port); socklen_t len =sizeof(addr);int n =::bind(_sockfd,CONV(&addr),len);if(n <0)exit(Bind_err);}voidListenSocketOrDie(int backlog) override {int n =::listen(_sockfd,backlog);if(n <0)exit(Listen_err);} Socket*AcceptConnection(std::string* clientip ,uint16_t* clientport) override {structsockaddr_in client;memset(&client,0,sizeof(client)); socklen_t len =sizeof(client);int fd =::accept(_sockfd,CONV(&client),&len);if(fd <0)returnnullptr;char buffer[64];inet_ntop(AF_INET,&client.sin_addr,buffer,len);*clientip = buffer;*clientport =ntohs(client.sin_port); Socket* s =newTcpSocket(fd);return s;}boolConnectServer(const std::string& serverip ,uint16_t serverport) override {structsockaddr_in server;memset(&server,0,sizeof(server)); server.sin_family = AF_INET;// server.sin_addr.s_addr = inet_addr(serverip.c_str());inet_pton(AF_INET,serverip.c_str(),&server.sin_addr); server.sin_port =htons(serverport); socklen_t len =sizeof(server);int n =connect(_sockfd,CONV(&server),len);if(n <0)returnfalse;elsereturntrue;}intGetSockFd() override {return _sockfd;}voidSetSockFd(int sockfd) override { _sockfd = sockfd;}voidCloseSockFd() override {if(_sockfd > defalutsockfd){close(_sockfd);}}boolRecv(std::string& buffer ,int size) override {char inbuffer[size];int n =recv(_sockfd,inbuffer,sizeof(inbuffer)-1,0);if(n >0){ inbuffer[n]=0;}else{returnfalse;} buffer += inbuffer;returntrue;}voidSend(const std::string& send_string) override {send(_sockfd,send_string.c_str(),send_string.size(),0);}virtualvoidReUseAddr() override {int opt =1;setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));}private:int _sockfd;};4.2 HttpProtocol.hpp(自定义HTTP协议)
#pragmaonce#include<iostream>#include<string>#include<vector>#include<sstream>#include<fstream>const std::string BlankSep ="\r\n";// 空行const std::string SpaceSep =" ";// 空格const std::string wwwroot ="./wwwroot";const std::string homepage ="index.html";classHttpRequest{public:HttpRequest():_request_blank(BlankSep),_path(wwwroot){};boolGetline(std::string &request, std::string &line){auto pos = request.find(BlankSep);if(pos == request.size())returnfalse; line = request.substr(0, pos); request.erase(0, pos + BlankSep.size());returntrue;}// 反序列化voidDeserialize(std::string &request){ std::string line;bool flag =Getline(request, line); _request_line = line;while(true){ flag =Getline(request, line);if(flag && line.empty()){ _request_content = request;break;}elseif(flag &&!line.empty()){ _request_hander.push_back(line);}else{break;}}}// 将请求行进行拆解voidParseRequestLine(){ std::stringstream in(_request_line); in >> _method >> _url >> _version;if(_url =="/") _path +=("/"+ homepage);else _path += _url;}voidParseSuffix(){int pos = _request_line.rfind(".");if(pos != std::string::npos) _suffix = _request_line.substr(pos);}voidParse(){// 将请求行每个部分拆出来ParseRequestLine();// 获取请求资源的后缀ParseSuffix();}voidDebug(){ std::cout <<"_request_line:"<< _request_line << std::endl;for(auto line : _request_hander){ std::cout << line << std::endl;} std::cout << _request_blank << std::endl; std::cout <<"_request_content:"<< _request_content << std::endl; std::cout <<"method : "<< _method <<" url : "<< _url <<" version : "<< _version << std::endl;} std::string GetFileContentHelper(const std::string &path){ std::ifstream in(path);if(!in.is_open())return""; in.seekg(0, in.end);int filesize = in.tellg(); in.seekg(0, in.beg); std::string content; content.resize(filesize); in.read((char*)content.c_str(), filesize); in.close();return content;} std::string GetFileContent(){returnGetFileContentHelper(_path);} std::string Get_404(){returnGetFileContentHelper(wwwroot +"/"+"404.html");} std::string Url(){return _url;} std::string Path(){return _path;} std::string Suffix(){return _suffix;}~HttpRequest(){};private: std::string _request_line;// 请求行 std::vector<std::string> _request_hander;// 请求报头 std::string _request_blank;// 空白行 std::string _request_content;// 请求内容 std::string _method; std::string _url; std::string _version; std::string _path; std::string _suffix;};classHttpResponse{public:HttpResponse():_response_blank(BlankSep),_method("HTTP 1.0"),_code(200),_desc("OK"){};voidSetCode(int code){ _code = code;}voidSetDesc(const std::string& desc){ _desc = desc;}voidMakeResponseLine(){ _response_line = _method + SpaceSep + std::to_string(_code)+ SpaceSep + _desc + BlankSep;}voidAddHander(const std::string line){ _response_hander.push_back(line);}voidAddContent(const std::string& content){ _response_content = content;} std::string Serialize(){// 响应行 std::string response_str = _response_line;// 响应报头for(auto& line : _response_hander){ response_str += line;}// 空行 response_str += _response_blank;// 响应正文 response_str += _response_content;return response_str;}~HttpResponse(){};private: std::string _response_line; std::vector<std::string> _response_hander; std::string _response_blank; std::string _response_content; std::string _method;int _code; std::string _desc;};4.3 TcpServer.hpp(服务端封装)
#pragmaonce#include"Socket.hpp"#include<string>#include<functional>#include<pthread.h>using func_t = std::function<std::string(std::string &)>;classTcpServer;classThreadDate{public:ThreadDate(TcpServer *tser_this, Socket *socket):_this(tser_this),_socket(socket){}public: TcpServer *_this; Socket *_socket;};classTcpServer{public:TcpServer(uint16_t port, func_t handler_request):_port(port),_listensock(newTcpSocket()),_handler_request(handler_request){ _listensock->BuildListenSocketMethod(_port);}staticvoid*HandlerRequest(void*arg){pthread_detach(pthread_self()); ThreadDate *th =(ThreadDate *)arg; std::string inbufferstream;// 1、读取报文if(th->_socket->Recv(inbufferstream,4096)){// 2、调用函数处理报文 std::string send_string = th->_this->_handler_request(inbufferstream);// 3、发送 th->_socket->Send(send_string);} th->_socket->CloseSockFd();delete th->_socket;delete th;returnnullptr;}voidLoop(){while(1){ std::string clientip;uint16_t clientport;// 接收连接会返回一个新的文件描述符 Socket *NewSocket = _listensock->AcceptConnection(&clientip,&clientport); std::cout <<"get a new sockfd , sockfd : "<< NewSocket->GetSockFd()<<" , clinet info "<< clientip <<":"<< clientport << std::endl;// 创建线程 pthread_t pid; ThreadDate *td =newThreadDate(this, NewSocket);pthread_create(&pid,nullptr, HandlerRequest, td);} _listensock->CloseSockFd();}~TcpServer(){}private: TcpSocket *_listensock;// 监听套接字uint16_t _port;public: func_t _handler_request;// 回调函数};4.4 wwwroot/index.html(主页)
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><h2>欢迎来到是阿建吖的主页!</h2><ahref="http://47.109.128.33:8888/login.html">登录</a></body></html>4.5 wwwroot/login.html(登录页面)
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>登录页面</title></head><body><formaction="login.html"method="post"> 用户名:<br><inputtype="text"name="name"value="chineseperson04"><br> 密码:<br><inputtype="text"name="password"><br><br><inputtype="submit"value="登录"></form><ahref="http://47.109.128.33:8888/index.html">回到首页</a></body></html>4.6 wwwroot/404.html(404页面、网上搜)
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>404 - 页面未找到</title><style>body{margin: 0;padding: 0;font-family:'Arial', sans-serif;background:linear-gradient(135deg, #ff7e5f, #feb47b);color: #fff;display: flex;justify-content: center;align-items: center;height: 100vh;overflow: hidden;}.container{text-align: center;animation: fadeIn 2s ease-in-out;}h1{font-size: 10rem;margin: 0;animation: float 3s ease-in-out infinite;}p{font-size: 1.5rem;margin-top: -20px;}a{display: inline-block;margin-top: 20px;padding: 10px 20px;font-size: 1rem;color: #fff;background:rgba(255, 255, 255, 0.2);text-decoration: none;border-radius: 25px;transition: all 0.3s ease;}a:hover{background:rgba(255, 255, 255, 0.4);transform:scale(1.1);}@keyframes float{0%, 100%{transform:translateY(0);}50%{transform:translateY(-20px);}}@keyframes fadeIn{from{opacity: 0;transform:scale(0.8);}to{opacity: 1;transform:scale(1);}}</style></head><body><divclass="container"><h1>404</h1><p>哎呀!页面走丢了...</p><ahref="/">返回首页</a></div></body></html>4.7 Main.cpp(服务端)
#include"TcpServer.hpp"#include"HttpProtocol.hpp"#include<iostream>#include<unistd.h>#include<string>#include<string.h>#include<memory>#include<fstream>usingnamespace std; string CodeToType(int code){switch(code){case200:return"OK";break;case204:return"No Content";break;case301:return"Moved Permanently";break;case302:return"Found ";break;case304:return"Not Modified";break;case307:return"Temporary Redirect";break;case400:return"Bad Request";break;case403:return"Forbidden ";break;case404:return"Not Found";break;case500:return"Internal Server Error";break;case501:return"Not Implemented";break;case502:return"Bad Gateway";break;case503:return"Service Unavailable";break;default:return"Unknown";break;}} string SuffixToType(const string &suffix){if(suffix ==".html"|| suffix ==".htm")return"text/html";elseif(suffix ==".jpg"|| suffix ==".jpeg")return"image/jpeg";elseif(suffix ==".png")return"image/png";elsereturn"";} string handler_http_request(string &request){ HttpRequest req;int code =200; string desc ="OK"; req.Deserialize(request); req.Parse(); req.Debug(); std::string content = req.GetFileContent();if(content.empty()){ code =404; desc =CodeToType(404); content = req.Get_404();} HttpResponse resp; resp.SetCode(code); resp.SetDesc(desc); resp.MakeResponseLine(); std::string http_content_length ="Content-Length: "+ std::to_string(content.size())+"\r\n"; resp.AddHander(http_content_length); std::string http_content_type ="Content-Type: "+SuffixToType(req.Suffix())+"\r\n"; resp.AddHander(http_content_type); resp.AddContent(content); string response_str = resp.Serialize();return response_str;}voidUsage(string proc){ cout << proc <<" serverport"<< endl;}intmain(int argc,char*argv[]){if(argc !=2){Usage(argv[0]);exit(1);}uint16_t serverport =stoi(argv[1]); unique_ptr<TcpServer> up = make_unique<TcpServer>(serverport, handler_http_request); up->Loop();return0;}结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹