前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【计算机网络】HTTP协议

【计算机网络】HTTP协议

作者头像
YoungMLet
发布2024-03-03 10:19:11
1070
发布2024-03-03 10:19:11
举报
文章被收录于专栏:C++/LinuxC++/Linux

虽然我们说,应用层协议是我们自己定的,但实际上,已经有大佬们定义了一些现成的,又非常好用的应用层协议,供我们直接参考使用。HTTP(超文本传输协议) 就是其中之一。

一、 认识URL

1. URL

在云服务器上,技术上一个客户端只需要知道 IP 地址和端口号,就能访问对应的服务端。但是在日常生活中我们并不使用 IP 地址,而是使用域名,因为 IP 地址对于用户来说体验非常不好,相对来说,域名比较直观,例如 https://www.baidu.com/,这就是一个域名,我们一看就知道是百度,而给一个百度的 IP 地址的话,我们也不知道是百度,我们也不敢轻易点进去。

实际上域名是一个字符串,但是它会被域名解析服务解析成 IP 地址,也就是每个域名会映射一个对应的 IP 地址,所以本质上我们还是使用 IP 地址访问服务器。例如我们可以使用 IP 地址 220.181.38.150 直接访问百度。但是我们在浏览器中将该 IP 地址复制过来后,我们会看见它会帮我们默认加上 http,也就是默认使用了 http 协议,例如 http://220.181.38.150/。那么一般像 httphttps 这种知名的服务,它的端口号在服务端一般是固定的,例如 http 默认绑定的端口是 80https 默认绑定的端口是 443;所以我们就能知道为什么我们没有使用端口号,而浏览器却知道访问哪个端口号了,因为浏览器默认使用 httphttp 服务器必须绑定 80 号端口,所以浏览器默认在请求里就会为我们添加端口号!例如我们使用 220.181.38.150:80 也能访问百度!

其实平时我们使用的“网址”,就是 URL,也就是统一资源定位符,例如 https://blog.csdn.net/YoungMLet/article/details/136111428?spm=1001.2014.3001.5502,在这个 URL 中,使用 / + 路径代表我们需要访问的资源,所有网络上的资源,都可以用唯一的一个 URL 标识,也就是唯一的一个“字符串”标识,并且可以获取到。

2. urlencode 和 urldecode

/ ? : 等这样的字符,已经被 url 当做特殊意义理解了。因此这些字符不能随意出现。比如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。例如我们在浏览器上搜索 xxx??//::yyy 这样的关键字,得到的 URL 如下:

我们关键词会被解析成上图所示的样子。少量的情况,提交或者获取的数据本身可能包含和 URL 中特殊的字符冲突的字符,要求浏览器和服务器双方(BS)要进行编码(encode)和解码(decode)

转义的规则如下:将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。

二、HTTP协议

1. http 请求

无论是 http 中的请求还是响应,都是以行的方式陈列出它的请求或者响应格式的,是由多行构成的,如下图:

每一行的分割符是 \r\n,但实际上它们也是字符,所以 http request 实际上是一个大的字符串,只不过中间有特殊字符,存的时候是按字节存的,只是打印出来变成了多行,所以存的结构和打印的结构是不一样的。

请求行的报头字段分为三部分,第一部分为请求的方法 Method,大部分分为两种,分别是 GETPOST;以空格作为分隔符,第二部分为 URL;以空格作为分隔符,第三部分为请求的协议版本 HTTP Version,最常见的版本为 1.1。

请求报头是由多行内容构成的,每一行都叫做 HTTP 请求的请求属性,大部分都是键值对 Key:ValueKey 表示是什么属性,Value 表示属性的值是什么。

那么未来在解析 http request 的时候,怎么区分有效载荷和报头呢?每一行都是以 \r\n 为分隔符,怎么保证区分呢?所以 http 协议又做了一个规定,在报头部分和正文部分,新加一个空行,称为 http 请求的第四部分,如下图:

那么怎么保证读取到一个完整的 http 请求呢?不能保证,但是我们能保证能读到一个完整的 http 报头,因为只需要读到空行就可以了!为了能把正文部分也完整读取,在报头中有一个属性包含了正文的长度,在读完报头之后,通过这个正文的长度继续读取对应的长度,就能保证读取到一个完整的 http 请求!

2. http 响应

http 响应和请求的格式几乎一模一样,第一部分称为状态行;第二也是大量的 k-v 结构,为响应报头;第三部分为响应正文。如下图:

那么当客户端收到了一个 http response,怎么保证把一个完整的 response 读完了呢?不能保证,所以响应中也包含了空行!

所以一个完整的 http 请求和响应如下图:

3. 获取响应

下面我们使用 telnet 连接百度的服务器,我们要捉到百度的首页需要最简单的请求,请求报头和请求正文可以没有,但是请求行和空行必须要有。连上服务器后,我们使用 GET 方法,获取 /,即首页资源,最后 HTTP/1.1 为版本,如下:

如上图,我们就获得了一个 http 响应。我们可以看到响应的状态行中的信息是:HTTP/1.1 200 OK,也就是它也被分为了三个部分,第一部分为 HTTP Version,协议的版本;第二部分为状态码;第三部分为状态码描述。

4. 最简单的 HTTP 服务器

(1)recv()

读取网络我们以前用的是 read(),今天我们再学一个接口,叫做 recv(),如下:

前三个参数和 read() 的一模一样,最后一个参数 flags 为读取的方式,我们默认设为0,作用就和 read() 的作用一样了。

(2)send()

向网络中写我们以前用的是 write(),现在我们也再学一个接口,叫做 send(),如下:

前三个参数也和 write() 一样,最后一个参数也是写的方式,我们设置为0,用法就和 write() 一样。

(3)代码实现

我们现在实现的是最简单的 HTTP 服务器,当用户发起请求,我们只给用户响应一个字符串 this is a response. 代码如下:

代码语言:javascript
复制
				static const int defaultport = 8080;
				
				class ThreadData
				{
				public:
				    ThreadData(int sockfd)
				        :_sockfd(sockfd)
				    {}
				public:
				    int _sockfd;
				};
				
				class HttpServer
				{
				public:
				    HttpServer(int port = defaultport)
				        :_port(port)
				    {}
				
				    bool Start()
				    {
				        _listensock.Socket();
				        _listensock.Bind(_port);
				        _listensock.Listen();
				        while(true)
				        {
				            std::string client_ip;
				            uint16_t client_port;
				            int sockfd = _listensock.Accept(&client_ip, &client_port);
				            if(sockfd < 0) continue;
				            pthread_t tid;
				            ThreadData* td = new ThreadData(sockfd);
				            pthread_create(&tid, nullptr, ThreadRun, td);
				        }
				    }
				
				    static void HandlerHttp(int sockfd)
				    {
				        char buffer[10240];
				        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
				        if(n > 0)
				        {
				            buffer[n] = 0;
				            std::cout << buffer;
				
				            // 做出响应
				            std::string text = "this is a response";
				            std::string response_line = "HTTP/1.0 200 OK\n\r";
				            std::string response_header = "Content-Length: ";
				            response_header += std::to_string(text.size());
				            response_header += "\r\n";
				            std::string blank_line = "\r\n";
				
				            std::string response = response_line;
				            response += response_header;
				            response += blank_line;
				            response += text;
				
				            send(sockfd, response.c_str(), response.size(), 0);
				        }
				        close(sockfd);
				    }
				
				    static void* ThreadRun(void* args)
				    {
				        pthread_detach(pthread_self());
				        ThreadData* td = static_cast<ThreadData*>(args);
				        
				        HandlerHttp(td->_sockfd);
				
				        delete td;
				        return nullptr;
				    }
				
				    ~HttpServer()
				    {}
				
				private:
				    Sock _listensock;
				    uint16_t _port;
				};

当我们创建 HttpServer 对象并调用 Start() 方法后,服务器就开始启动了,开始获取连接,当有用户在浏览器上发起连接时,浏览器会发出 http 请求,如下:

其中请求行中 GET 表示方法,/ 表示 URLHTTP/1.1 表示协议版本。User-Agent 代表浏览器访问服务器的客户端的详细信息。

服务器响应一般是包含图片、视频音频或者是网页,但我们现在的服务器响应的信息只是一个字符串,我们使用浏览器访问的结果如下:

5. 在 HTTP 服务器中简单应用 HTML

我们在做出响应时只是响应了一个字符串,接下来我们简单使用 HTML 进行响应。一般我们在使用浏览器访问时,只输入 IP 地址和端口号,浏览器默认会帮我们请求 /,也就是 web 根目录。我们需要访问的资源其实是通过 / 带路径的方式去访问的。也就是说,一个 HTTP 协议,一定要有自己的 web 根目录,这个根目录可以是 Linux 的根目录,也可以由我们自己指定,下面我们就指定我们自己的 HTTP 协议的根目录为 root_directory.

也就是我们在启动自己的 HTTP 服务时,我们的可执行程序的当前路径下有一个 root_directory 文件夹,将来我们就把所写的所有网页、图片、视频和网站的首页等等,全部放在这个目录下,以树状结构的形式从这个目录开始让别人去访问,我们把这个目录叫做 web 根目录。但是当别人只带有 / 这个路径时,也就是访问我们的目录,我们也不能将根目录下的所有内容给别人返回,因为这样就会导致别人将整个网站的所有内容都拿到了,所以此时我们只需要将整个网站的首页给别人返回即可,在我们的代码中,我们的首页就是 index.html.

代码如下:

代码语言:javascript
复制
				#include <iostream>
				#include <fstream>
				#include <sstream>
				#include <string>
				#include <pthread.h>
				#include <vector>
				#include "Socket.hpp"
				#include "log.hpp"
				
				static const int defaultport = 8080;
				const std::string root_directory = "./root_directory";   // web根目录
				const std::string sep = "\r\n";
				const std::string homepage = "index.html";
				
				class ThreadData
				{
				public:
				    ThreadData(int sockfd)
				        :_sockfd(sockfd)
				    {}
				public:
				    int _sockfd;
				};
				
				
				class HttpRequest
				{
				public:
				    void Deserialize(std::string req)
				    {
				        while(true)
				        {
				            // 查找分隔符
				            size_t pos = req.find(sep);
				            if(pos == std::string::npos) break;
				
				            // 截取 /r/n 之前的字串,如果是空串说明是空行,直接结束循环
				            std::string tmp = req.substr(0, pos);
				            if(tmp.empty()) break;
				
				            req_header.push_back(tmp);
				            req.erase(0, pos + sep.size());
				        }
				        // 剩下的都是正文
				        text = req;
				    }
				
				    void Parse()
				    {
				        std::stringstream ss(req_header[0]);
				        ss >> method >> url >> http_version;
				        req_path = root_directory;   // ./root_directory
				        if(url == "/" || url == "/index.html") 
				        {
				            req_path += "/";
				            req_path += homepage;   // ./root_directory/index.html
				        }
				        else req_path += url;   // /a/b/c/d.html -> ./root_directory/a/b/c/d.html
				    }
				
				public:
				    std::vector<std::string> req_header;    // 所有请求,包括请求行和请求报头
				    std::string text;   // 正文
				
				    std::string method; // 方法
				    std::string url;    // URL
				    std::string http_version;   // 协议版本
				    std::string req_path;
				};
				
				
				class HttpServer
				{
				public:
				    HttpServer(int port = defaultport)
				        :_port(port)
				    {}
				
				    bool Start()
				    {
				        _listensock.Socket();
				        _listensock.Bind(_port);
				        _listensock.Listen();
				        while(true)
				        {
				            std::string client_ip;
				            uint16_t client_port;
				            int sockfd = _listensock.Accept(&client_ip, &client_port);
				            if(sockfd < 0) continue;
				            pthread_t tid;
				            ThreadData* td = new ThreadData(sockfd);
				            pthread_create(&tid, nullptr, ThreadRun, td);
				        }
				    }
				
				    static std::string ReadHTMLcontent(const std::string& htmlpath)
				    {
				        std::ifstream in(htmlpath);
				        if(!in.is_open()) return "404";
				
				        std::string content;
				        std::string line;
				        while(std::getline(in, line))
				        {
				            content += line;
				        }
				        in.close();
				        return content;
				    }
				
				    static void HandlerHttp(int sockfd)
				    {
				        char buffer[10240];
				        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
				        if(n > 0)
				        {
				            buffer[n] = 0;
				            std::cout << buffer;    // 假设我们读取到的就是一个完整的独立的 http 请求
				            HttpRequest req;
				            req.Deserialize(buffer);
				            req.Parse();
				
				            // 做出响应
				            std::string text = ReadHTMLcontent(req.req_path);
				            
				            std::string response_line = "HTTP/1.0 200 OK\n\r";
				            std::string response_header = "Content-Length: ";
				            response_header += std::to_string(text.size());
				            response_header += "\r\n";
				            std::string blank_line = "\r\n";
				
				            std::string response = response_line;
				            response += response_header;
				            response += blank_line;
				            response += text;
				
				            send(sockfd, response.c_str(), response.size(), 0);
				        }
				        close(sockfd);
				    }
				
				    static void* ThreadRun(void* args)
				    {
				        pthread_detach(pthread_self());
				        ThreadData* td = static_cast<ThreadData*>(args);
				        
				        HandlerHttp(td->_sockfd);
				
				        delete td;
				        return nullptr;
				    }
				
				    ~HttpServer()
				    {}
				
				private:
				    Sock _listensock;
				    uint16_t _port;
				};

6. HTTP 方法

http 的方法有如下:

到目前为止,我们发起的 http 请求,全部都是 GET 方法;POST 也可以获取网页,但是更多的是上传资源,提交参数;剩下的方法都是不常用的。通过网页、浏览器提交参数的时候,通常是通过表单来提参的,如果表单的提交方法是 GET,那么参数就会拼接到 URL 的后面,用 ? 作为分隔符,左侧是访问的资源,右侧是提交的参数,参数和参数之间用 & 进行分割。如果表单的提交方法是 POST,采用的是正文提交参数。所以相比较 GET 方法提参,POST 方法更私密一些,因为它不会在 URL 中进行信息的回显。

7. HTTP 状态码

HTTP 响应时的状态码有如下:

最常见的状态码,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway).

(1)404 页面

下面我们对上面的代码稍作修改,让我们的代码支持 404 页面。首先在 ReadHTMLcontent() 中如果路径无效,返回空串。

代码语言:javascript
复制
				    static std::string ReadHTMLcontent(const std::string& htmlpath)
				    {
				        std::ifstream in(htmlpath);
				        if(!in.is_open()) return "";
				
				        std::string content;
				        std::string line;
				        while(std::getline(in, line))
				        {
				            content += line;
				        }
				        in.close();
				        return content;
				    }

如果返回空串,说明页面不存在,我们就在 HandlerHttp() 方法中做出相应的响应,代码如下:

代码语言:javascript
复制
				    static void HandlerHttp(int sockfd)
				    {
				        char buffer[10240];
				        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
				        if(n > 0)
				        {
				            buffer[n] = 0;
				            std::cout << buffer;    // 假设我们读取到的就是一个完整的独立的 http 请求
				            HttpRequest req;
				            req.Deserialize(buffer);
				            req.Parse();
				
				            // 做出响应
				            std::string text;
				            bool flag = true;
				            text = ReadHTMLcontent(req.req_path);
				            if(text.empty())
				            {
				                flag = false;
				                std::string err_html = root_directory;
				                err_html += "/";
				                err_html += "err.html";
				                text = ReadHTMLcontent(err_html);
				            }
				
				            // 处理 404
				            std::string response_line;
				            if(flag) response_line = "HTTP/1.0 200 OK\n\r";
				            else response_line = "HTTP/1.0 404 Not Found\r\n";
				
				            std::string response_header = "Content-Length: ";
				            response_header += std::to_string(text.size());
				            response_header += "\r\n";
				            std::string blank_line = "\r\n";
				
				            std::string response = response_line;
				            response += response_header;
				            response += blank_line;
				            response += text;
				
				            send(sockfd, response.c_str(), response.size(), 0);
				        }
				        close(sockfd);
				    }

接下来我们需要写一个 404 页面,我们直接在 w3cschool 该网站上查找一个页面源代码即可。我们找到的页面代码如下,err.html

代码语言:javascript
复制
				<!doctype html>
				<html lang="en">
				
				<head>
				    <meta charset="UTF-8">
				    <title>404 Not Found</title>
				    <style>
				        body {
				            text-align: center;
				            padding: 150px;
				        }
				
				        h1 {
				            font-size: 50px;
				        }
				
				        body {
				            font-size: 20px;
				        }
				
				        a {
				            color: #008080;
				            text-decoration: none;
				        }
				
				        a:hover {
				            color: #005F5F;
				            text-decoration: underline;
				        }
				    </style>
				</head>
				
				<body>
				    <div>
				        <h1>404</h1>
				        <p>页面未找到<br></p>
				        <p>
				            您请求的页面可能已经被删除、更名或者您输入的网址有误。<br>
				            请尝试使用以下链接或者自行搜索:<br><br>
				            <a href="https://www.baidu.com">百度一下></a>
				        </p>
				    </div>
				</body>
				</html>

当我们访问的路径非法时显示的页面如下:

(2)301(永久重定向) 和 302(临时重定向)

在讲这两个重定向前,我们先认识一下 HTTP 报头当中的 Location 字段,Location 就是搭配 3xx 状态码使用,告诉客户端接下来要去哪里访问。

什么是重定向状态码呢?假设我们向目标服务器发起一个请求,当服务器给我们响应时,状态码如果是 3XX,响应报头里面有一个字段 Location,该属性后面会跟一个新的地址,这就是服务器告诉我们应该去哪里访问该资源,所以此时浏览器收到了 3XX 状态码和 Location 字段后,它就知道它需要访问的服务器无法向它进行服务,此时浏览器就会自动发起二次请求,这一次的 http 请求就是根据 Location 的地址去发起请求。也就是说,重定向是一种让服务器指导浏览器,让浏览器访问新的地址!

下面我们修改 HandlerHttp() 方法,让我们的代码支持重定向状态码,代码如下:

代码语言:javascript
复制
				static void HandlerHttp(int sockfd)
				    {
				        char buffer[10240];
				        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
				        if(n > 0)
				        {
				            buffer[n] = 0;
				            std::cout << buffer;    // 假设我们读取到的就是一个完整的独立的 http 请求
				            HttpRequest req;
				            req.Deserialize(buffer);
				            req.Parse();

				            // 做出响应
				            std::string text;
				            bool flag = true;
				            text = ReadHTMLcontent(req.req_path);
				            if(text.empty())
				            {
				                flag = false;
				                std::string err_html = root_directory;
				                err_html += "/";
				                err_html += "err.html";
				                text = ReadHTMLcontent(err_html);
				            }
				
				            // 测试 302
				            std::string response_line = "HTTP/1.0 302 Redirect\r\n";
				
				            std::string response_header = "Content-Length: ";
				            response_header += std::to_string(text.size());
				            response_header += "\r\n";
				
				            response_header += "Location: https://www.baidu.com";
				            response_header += "\r\n";
				            std::string blank_line = "\r\n";
				
				            std::string response = response_line;
				            response += response_header;
				            response += blank_line;
				            response += text;
				
				            send(sockfd, response.c_str(), response.size(), 0);
				        }
				        close(sockfd);
				    }

那么永久重定向和临时重定向的区别是什么呢?永久重定向就是我们经常访问一个网站,但是如果当这个网站需要更新时,需要把域名也更换,但是又有许多的老用户,这时候就需要给老网站部署一个服务,永久重定向,让老用户访问老网站时直接跳转到新网站。临时重定向则是做登录界面的临时跳转。

8. HTTP 常见的 Header(报头)

  • Content-Type:数据类型(text/html等);
  • Content-Length:Body(正文) 的长度;
  • Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上;
  • User-Agent:声明用户的操作系统和浏览器版本信息;
  • referer:当前页面是从哪个页面跳转过来的;
  • Location:搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
  • Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能;
(1)Connection

接下来我们查看一下在服务器上收到的 http 请求:

我们看到 Connection 属性,一个巨大的页面是会包含非常多的元素的,其中每一个元素就是一个资源。当一个网页有许多图片时,假设有100张图片,如果要让该网页让用户看到,我们可能要发起 101 次请求,第一次请求是把该网页本身请求到,剩下100次都是为图片发起请求。我们要知道,HTTP 协议的底层是基于 TCP 的,而 TCP 是面向连接的,而每一次发起 HTTP 请求时,交互 RequestResponse 的前提条件是先得让浏览器发起基于 TCPconnect 连接的请求,也就是说,一张网页如果要获取完整,可能需要建立上 101 次连接,每一次建立好连接返回资源后连接就断开了!无疑这种方案是低效的!这种一次请求响应一个资源,关闭连接,我们称之为短连接!所以为了高效,有一种方案是建立一个 TCP 连接,发送和返回多个 httprequestresponse,这种叫做长连接!我们上面的 Connection 属性中的 keep-alive 就叫做长连接!而 http/1.0 默认支持的就是短连接,http/1.1 就默认支持长连接。

(2)Content-Type

下面我们修改一下我们的代码,在我们的代码首页放上一张图片。首先,图片是二进制形式的,所以读取的方式需要改变;而且还需要告诉浏览器图片是什么格式的,什么类型的,才好让浏览器给我们进行显示;所以就需要一个报头叫做 Content-Type,我们以前显示网页从来没有用过这个报头,因为网页的标签 <!DOCTYPE html></html> 等等是非常明显的,浏览器能够识别到正文部分就是网页,所以它自动给我们去解释了,但是有些浏览器不会自动去解释。所以我们的 HTTP 响应需要再加一个 Content-Type 报头。

我们可以上网查询到 Content-Type 对照表,可以根据文件扩展名得到对应的 Content-Type,下面我们使用 .jpg.html 做演示:

在这里插入图片描述
在这里插入图片描述

接下来开始修改代码,首先我们需要知道后缀名,所以在路径中需要查找后缀名,这个工作在 Parse() 中处理,将后缀名使用成员变量 suffix 记录下来。接下来在 HttpServer 中维护一张文件后缀和 Content-Type 的哈希表,方便使用后缀名直接转换为 Content-Type.

但是 ReadHTMLcontent() 中还有一些问题,就是图片是二进制的,但是该方法中是按照文本去读取的,以前读取的时候 HTML 本身就是文本,所以不会有问题。所以还需要对该方法进行修改,以二进制方式读取。

代码如下:

代码语言:javascript
复制
				#include <iostream>
				#include <fstream>
				#include <sstream>
				#include <string>
				#include <vector>
				#include <unordered_map>
				#include <pthread.h>
				#include "Socket.hpp"
				#include "log.hpp"
				
				static const int defaultport = 8080;
				const std::string root_directory = "./root_directory";   // web根目录
				const std::string sep = "\r\n";
				const std::string homepage = "index.html";
				
				class HttpServer;
				
				class ThreadData
				{
				public:
				    ThreadData(int sockfd, HttpServer* svr)
				        :_sockfd(sockfd)
				        ,_svr(svr)
				    {}
				public:
				    int _sockfd;
				    HttpServer* _svr;
				};
				
				
				class HttpRequest
				{
				public:
				    void Deserialize(std::string req)
				    {
				        while(true)
				        {
				            // 查找分隔符
				            size_t pos = req.find(sep);
				            if(pos == std::string::npos) break;
				
				            // 截取 /r/n 之前的字串,如果是空串说明是空行,直接结束循环
				            std::string tmp = req.substr(0, pos);
				            if(tmp.empty()) break;
				
				            req_header.push_back(tmp);
				            req.erase(0, pos + sep.size());
				        }
				        // 剩下的都是正文
				        text = req;
				    }
				
				    void Parse()
				    {
				        std::stringstream ss(req_header[0]);
				        ss >> method >> url >> http_version;
				        req_path = root_directory;   // ./root_directory
				        if(url == "/" || url == "/index.html") 
				        {
				            req_path += "/";
				            req_path += homepage;   // ./root_directory/index.html
				        }
				        else req_path += url;   // /a/b/c/d.html -> ./root_directory/a/b/c/d.html
				
				        auto pos = req_path.rfind(".");
				        if(pos == std::string::npos) suffix = ".html";
				        else suffix = req_path.substr(pos);
				    }
				public:
				    std::vector<std::string> req_header;    // 所有请求,包括请求行和请求报头
				    std::string text;   // 正文
				
				    std::string method; // 方法
				    std::string url;    // URL
				    std::string http_version;   // 协议版本
				    std::string req_path;
				
				    std::string suffix; // 文件后缀
				};
				
				
				class HttpServer
				{
				public:
				    HttpServer(int port = defaultport)
				        :_port(port)
				    {
				        _content_type.insert({".html", "text/html"});
				        _content_type.insert({".jpg", "image/jpeg"});
				    }
				
				    bool Start()
				    {
				        _listensock.Socket();
				        _listensock.Bind(_port);
				        _listensock.Listen();
				        while(true)
				        {
				            std::string client_ip;
				            uint16_t client_port;
				            int sockfd = _listensock.Accept(&client_ip, &client_port);
				            if(sockfd < 0) continue;
				            pthread_t tid;
				            ThreadData* td = new ThreadData(sockfd, this);
				            pthread_create(&tid, nullptr, ThreadRun, td);
				        }
				    }
				
				    static std::string ReadHTMLcontent(const std::string& htmlpath)
				    {
				        std::ifstream in(htmlpath, std::ios::binary);
				        if(!in.is_open()) return "";
				
				        // 计算文件大小
				        in.seekg(0, std::ios_base::end);
				        auto len = in.tellg();
				        in.seekg(0, std::ios_base::beg);
				
				        std::string content;
				        content.resize(len);
				
				        in.read((char*)content.c_str(), content.size());
				        
				        in.close();
				        return content;
				    }
				
				    std::string SuffixToDesc(const std::string& suffix)
				    {
				        auto iter = _content_type.find(suffix);
				        if(iter == _content_type.end()) return _content_type[".html"];
				        else return _content_type[suffix];
				    }
				
				    void HandlerHttp(int sockfd)
				    {
				        char buffer[10240];
				        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
				        if(n > 0)
				        {
				            buffer[n] = 0;
				            std::cout << buffer;    // 假设我们读取到的就是一个完整的独立的 http 请求
				            HttpRequest req;
				            req.Deserialize(buffer);
				            req.Parse();
				            // req.Print();
				
				            // 做出响应
				            std::string text;
				            bool flag = true;
				            text = ReadHTMLcontent(req.req_path);
				            if(text.empty())
				            {
				                flag = false;
				                std::string err_html = root_directory;
				                err_html += "/";
				                err_html += "err.html";
				                text = ReadHTMLcontent(err_html);
				            }
				
				            // 处理 404
				            std::string response_line;
				            if(flag) response_line = "HTTP/1.0 200 OK\n\r";
				            else response_line = "HTTP/1.0 404 Not Found\r\n";
				
				            // Content-Length
				            std::string response_header = "Content-Length: ";
				            response_header += std::to_string(text.size());
				            response_header += "\r\n";
				
				            // Content-Type
				            response_header += "Content-Type: ";
				            response_header += SuffixToDesc(req.suffix);
				            response_header += "\r\n";
				
				            // 空行
				            std::string blank_line = "\r\n";
				
				            std::string response = response_line;
				            response += response_header;
				            response += blank_line;
				            response += text;
				
				            send(sockfd, response.c_str(), response.size(), 0);
				        }
				        close(sockfd);
				    }
				
				    static void* ThreadRun(void* args)
				    {
				        pthread_detach(pthread_self());
				        ThreadData* td = static_cast<ThreadData*>(args);
				        
				        td->_svr->HandlerHttp(td->_sockfd);
				
				        delete td;
				        return nullptr;
				    }
				
				    ~HttpServer()
				    {}
				
				private:
				    Sock _listensock;
				    uint16_t _port;
				    std::unordered_map<std::string, std::string> _content_type;
				};
(3)Cookie

当我们登录一个网站时,然后关闭浏览器再打开,继续访问该网站时,可能就不需要我们再次登录该网站了,也就是把我们的登录状态一直记录着。由于 http 协议默认是没有状态的,请求什么资源就是什么资源,又需要处于登录状态才能访问某些资源,它怎么知道我们是处于登录状态的呢?这就是 http 对登录用户的会话保持功能的概念,所以就需要使用 Cookie 的方案实现。

这个实现方案的原理是什么呢?首先我们在首次登录一个网站时,使用账号密码登录该网站,然后对该网站的服务器以 POST 方法发起 HTTP 请求,然后服务器发起响应,先让客户端重定向至网站首页,此外,在响应的报头中还包括 Set Cookie 这样的选项,该选项中会写上当前已经处于登录状态的用户的账号和密码,然后浏览器收到后,会把 Set Cookie 中账号密码的内容保留起来,一般把保留该数据的文件称为 Cookie 文件,从此之后,当该浏览器继续访问同一个网站时,每一次请求都会携带 Cookie 文件中的内容,也就是用户名和密码!这也就是我们登录后不用再次登录的原因。

但是如果我们的电脑中了某些病毒,然后可能会在浏览器开放了一些端口,然后在我们的电脑上全文式地扫描这种 Cookie 文件,然后就把 Cookie 文件偷走了!那么如果是这种把用户的个人信息、登录信息放在本地用户端的浏览器中,这是非常不安全的!所以这种方法会面临两个问题,一是个人私有信息泄漏,二是 Cookie 文件被盗取。那么怎么解决呢?其实有一种解决方案就是,我们首次登录认证的时候,向网站服务器发起请求,在服务器后端为我们此次的首次登录创建一个 Session 文件,这个文件会记录当前用户的个人信息、登录信息等相关内容。每一个 Session 文件都会有唯一的 Session ID,并且每个 Session 文件都会以它唯一的 Session ID 命名。然后,服务器在响应时,在 Set-Cookie 字段中写入 Session ID,即将 Session ID 写入到用户浏览器的 Cookie 文件中。所以此后浏览器再次访问服务器时,都会携带上 Cookie 文件中的 Session ID,服务器在认证时,就只需要判断该 Session ID 是否存在即可!至此我们就解决了第一个问题,个人私有信息就不会被泄漏了,因为我们的个人信息由服务端给我们维护了;但是 Cookie 文件本身被盗取是不可避免的。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-03-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、 认识URL
    • 1. URL
      • 2. urlencode 和 urldecode
      • 二、HTTP协议
        • 1. http 请求
          • 2. http 响应
            • 3. 获取响应
              • 4. 最简单的 HTTP 服务器
                • (1)recv()
                • (2)send()
                • (3)代码实现
              • 5. 在 HTTP 服务器中简单应用 HTML
                • 6. HTTP 方法
                  • 7. HTTP 状态码
                    • (1)404 页面
                    • (2)301(永久重定向) 和 302(临时重定向)
                  • 8. HTTP 常见的 Header(报头)
                    • (1)Connection
                    • (2)Content-Type
                    • (3)Cookie
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档