计算机网络基础2/http协议

bash 复制代码
netstat -nltp

netstat -nltp 是一个在Linux中使用的命令,它利用netstat工具来显示网络状态信息。具体来说,这个命令的各个选项代表以下功能:

  • -n:这个选项让netstat显示网络地址的数字形式,而不是尝试解析主机名、服务名等。这通常会使输出更快,因为它避免了DNS查找和其他名字解析操作。
  • -l:这个选项告诉netstat只显示那些正在监听的套接字。一个"监听"的套接字是一个网络端点,它正在等待进入的连接。这通常与服务器应用程序相关,这些应用程序需要接受来自客户端的连接。
  • -t:这个选项限制输出到TCP套接字。TCP是一种面向连接的传输层协议,它提供了可靠的、有序的、基于字节流的通信。
  • -p:这个选项让netstat显示与每个套接字关联的进程ID和进程名。这可以帮助你识别哪个进程正在使用特定的网络端口。

综合起来,netstat -nltp 命令会显示系统上所有正在监听TCP端口的数字地址,以及与这些端口相关联的进程ID和进程名。这个命令在诊断网络问题、查看系统开放的端口以及确定哪些进程正在使用这些端口时非常有用。

学习http的预备知识

域名和URL

虽然说应用层的协议是要由程序猿们自己定的,但是已经有大佬们定义了一些现成的,非常好用的应用层协议,供我们直接参考使用,比如今天要学习的http协议。

首先我们要知道什么叫做域名

https://www.baidu.com 这个就是一个域名,我们可以在浏览器上通过直接输入域名,来访问一个网址。但是之前我们学过,客户端要想访问一个服务,首先要知道服务端的ip地址和端口号。为什么我们仅需通过域名就可以访问到网址了呢?

程序猿当然知道访问网址或者请求服务是要ip地址的,但是对于普通用户来说如果要用ip地址来访问的话未免太麻烦了,所以才设计了域名。域名是会被域名解析成为ip地址的。所以我们真正用到的还是ip地址。

而端口号则是http协议规定的,服务端的端口号必须是80。不同的协议规定的端口号可能不一样,比如https规定的端口号就是443。客户端当然就没要求了。

我们可以直接使用 220.181.38.150 + :80(就是冒号加端口号80)来直接访问百度的首页,而且不需要加http//,浏览器会默认加上的,或者我们也可以连端口号都不加,因为http规定服务端的端口号得是80。

在Windows上我们可以用命令行窗口通过ping指令来查看一个网址的公网ip

其实我们一般的网址其实就是URL---资源统一定位符。

URL(统一资源定位符)是用于指定Internet上资源位置的地址,而域名则是URL中的一部分,用于标识特定的网站或服务器。简单来说,域名是URL的组成部分之一,它指定了要访问的网站或服务器的名称。

一个完整的URL通常包括协议(如http://或https://)、域名(如www.example.com)、以及可能的路径、查询参数和片段标识符等其他信息。其中,域名是URL中最重要和最直观的部分之一,它通常由一个或多个单词组成,并通过点号(.)分隔不同的级别。

例如,在URL"https://www.example.com/index.html"中,"www.example.com"就是域名部分。

需要注意的是,虽然域名通常是URL的一部分,但并非所有URL都包含域名。例如,本地文件或内部网络资源的URL可能不包含域名,而是使用其他标识符来指定资源位置。此外,URL中的域名部分也不一定总是以"www"开头,这取决于网站的具体配置和命名约定。

比如这就是一个较为完整的网址(URL),其中在 带层次的文件路径那里,第一个斜杠代表的是web的根目录,不过这个也是一个相对地址。

而且登录信息现在也很少见了。

服务器地址就是我们刚刚说的域名。

这个?uid=1,可以把uid看作Key,1看作Val。这个代表的是请求的一些动态数据,

我们用百度搜索一下 helloword,我们可以看到我们的网络请求,其中我们看到有一个问号,问号后面就是wd=hellword,也就是说这个wd(Key)就是我们输入的关键字,此外我们看到在 & 分隔符后还有很多类似的 K-V对,以此来支持多个参数的提交。

而且我们发现 & 是特殊符号最为分隔符,如果我们搜索的关键字带有这样的特殊符号会怎么样呢
像 / ? : 等这样的字符 , 已经被 url 当做特殊意义理解了 . 因此这些字符不能随意出现。
比如 , 某个参数中需要带有这些特殊字符 , 就必须先对特殊字符进行转义。
转义的规则如下 :
将需要转码的字符转为 16 进制,然后从右到左,取 4 位 ( 不足 4 位直接处理 ) ,每 2 位做一位,前面加上 % ,编码成 %XY
格式

"+" 被转义成了 "%2B"
urldecode 就是 urlencode 的逆过程 ;
网上也有很多在线的URL的转换工具。

请求和响应 (http的协议格式)

我们一般上网,概况总结无非就是两种行为:1.把别人的东西拿下来。2.把自己的东西传上去。

我们在百度上搜索一些信息,也是一种请求,服务端接收到请求后,经过处理再把结果返回给我们。

在Linux命令行下我们可以用

bash 复制代码
telnet 183.2.172.185 80

来直接访问百度的首页(也就是抓包)

再往下翻

我们还看到了 "百度一下,你就知道"的字样,说明确实是百度的首页。

http请求

可以把http请求看成4部分

每一个部分都是用换行来分隔的,但是我们要站在计算机的视角来看,其实就是\r\n来分隔的。本质也是一大坨字符串。

第一部分也就是首行, 因为它要用换行符跟请求报头做区分,所以它只有一行。放在开头的就是请求方法,虽然方法很多,但是95%的场景下都是用GET和POST方法,也就是之前我们说的。下一个就是URL,它表示当前请求的资源是谁。它们之间用空格作为分隔符,第三个就是请求协议的版本。

第二部分也就是请求报头,包含了很多的K-V结构的键值对,用 \r\n 分隔。

第三部分是一个空行,用来隔开请求报头和请求正文的。因为在请求报头中是用\r\n来分隔的,所以我们并不知到哪部分是请求报头,哪部分是请求正文,因为多加一行空行来做区分,记住在计算机看来无非就是连着两个\r\n。

第四部分就是请求正文。
首行: [方法] + [url] + [版本]
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个
Content-Length属性来标识Body的长度;

http响应

状态行也分为三部分,分别是http版本+状态码+状态码描述 。

其余部分都差不多。

状态码其实就是结果执行完的一个返回码,状态码描述其实就是对返回码的解释。

也是这个结构,只是有些部分的叫法不同而已,原理大致是一样的。
首行: [版本号] + [状态码] + [状态码解释]
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个
Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在
body中.

现在再来看当时抓到的请求,是不是就很熟悉了?

其中 Content-Length: 9508,说明正文部分的字符长度是9508。

再说说为什么要带版本,这是因为兼容性问题,比如微信要更新,不可能说所有用户同时更新,它是要慢慢更新的,也就是比如一天10万用户更新,以此类推,这是因为万一新版本会有潜在的bug还没发现,如果出现重大bug那不就完了,但是对于新旧版本的客户端,服务端都要提供服务,所以服务端需要根据客户端的版本不同来提供不同的服务。

抓包工具fiddler可以帮助我们查看http请求和响应。

简单实现httpServer

通过简单实现一个httpServer可以加深我们对http的理解,并且我们写好后可以直接使用浏览器来访问我们的服务。

代码部分

关于前端

对于网页我们这里会用到一点前端的知识,但是我们其实直接到网上找源代码就可以了

比如首页面的

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- <form action="/a/b/hello.html" method="post">
        name: <input type="text" name="name"><br>
        password: <input type="password" name="passwd"><br>
        <input type="submit" value="提交">
    </form> -->
    <h1>这个是我们的首页</h1>
     <!-- <img src="/image/1.png" alt="这是一直猫" width="100" height="100"> 根据src向我们的服务器浏览器自动发起二次请求 -->
    <!-- <img src="/image/2.jpg" alt="这是花"> -->
</body>
</html>

还有404页面

html 复制代码
<!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>

服务端

HttpServer.hpp
cpp 复制代码
#pragma

#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>

#include "Socket.hpp"
#include "Log.hpp"

const std::string wwwroot = "./wwwroot";  // web 根目录
const std::string sep = "\r\n";  // 分隔符
const std::string homepage = "index.html";  // 我们网站的首页

static const int defaultport = 8080;

class HttpServer;

class ThreadData
{
public:
    ThreadData(int fd,HttpServer* s)
        :sockfd(fd),svr(s)
        {}
public:
    int sockfd;
    HttpServer* svr;
};

class HttpRequest    // 服务器的响应
{
public:
    void Deserialize(std::string req)
    {
        while(true)
        {
            std::size_t pos = req.find(sep);
            if(pos == std::string::npos)
                break;
            std::string temp = req.substr(0,pos);  // 拿到该行信息
            if(temp.empty())
                break;
            req_header.push_back(temp);
            req.erase(0,pos+sep.size());  // 除掉报头该行,为了拿到正文部分
        }
        text = req;
    }

    void Parse()  // 解析
    {
        // 让我们可以用操作文件的方式来操作字符串
        std::stringstream ss(req_header[0]);
        ss >> method >> url >> http_version;
        file_path = wwwroot;
        if(url == "/" || url == "/index.html") // 默认/就是访问首页
        {
            file_path += "/";
            file_path += homepage;
        }  
        else
        {
            file_path += url;  // 用户自定义访问的路径
        }

        auto pos = file_path.find(".");
        if(pos == std::string::npos)
        {
            suffix = ".html";
        }
        else
        {
            suffix = file_path.substr(pos);
        }
    }

    void DebugPrint()
    {
        for(auto& line:req_header)
        {
            std::cout << "-------------------------" << std::endl;
            std::cout << line << "\n\n";
        }

        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version" << http_version << std::endl;
        std::cout << "file_path: " << std::endl;
        std::cout << text << std::endl;
    }
public:
    std::vector<std::string> req_header;
    std::string text;

    // 解析之后的结果
    std::string method;  // 方法
    std::string url;
    std::string http_version;  // 当前http的版本
    std::string file_path;  // 文件路径
    
    std::string suffix;  // 后缀
};

class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport)
        :port_(port)
        {
            content_type.insert({".html","text/html"});
            content_type.insert({".png","image/png"});
        }

        bool Start()
        {
            listensock_.Socket();
            listensock_.Bind(port_);
            listensock_.Listen();
            for(;;)
            {
                std::string clientip;
                uint16_t clientport;
                int sockfd = listensock_.Accept(&clientip,&clientport);
                if(sockfd < 0)
                    continue;
                lg(Info,"get a new connect,sockfd: %d",sockfd);
                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()); // 最后将这个二进制的内容读到content缓冲区中

            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 << std::endl; // 这里假设我们读到的请求都是完整的
                HttpRequest req;
                req.Deserialize(buffer);
                req.Parse();

                // 返回响应的过程
                std::string text;
                bool ok = true;
                text = ReadHtmlContent(req.file_path);
                if(text.empty())
                {
                    ok = false;  // 没有读到资源,说明路径错误
                    std::string err_html = wwwroot;
                    err_html += "/";
                    err_html += "err.html";
                    text = ReadHtmlContent(err_html);
                }

                std::string response_line;
                if(ok)
                {
                    response_line = "HTTP/1.0 200 ok\r\n";
                }
                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 += sep;  // 注意记得要分隔符
                response_header += "Content-Type: ";
                response_header += SuffixToDesc(req.suffix);
                response_header += sep;
                response_header += "Set-Cookie: name=hzj&&passwd=54088";
                response_header += sep;

                std::string blank_line = sep; // 空行

                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; // 文件的后缀
};

因为我们目的仅仅是为了加深对http的理解,所以有些地方处理的非常粗糙,比如在提取报文那里,我们没有保证每次提取的都是完整的报文,因此请求稍微多一点,就有可能导致服务器挂掉

HttpServer.cc
cpp 复制代码
#include "HttpServer.hpp"
#include <iostream>
#include <memory>
#include <pthread.h>
#include "Log.hpp"

using namespace std;

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        cout << "请按正确格式输入" << endl;
        exit(1);
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<HttpServer> svr(new HttpServer(port));
    svr->Start();
    return 0;
}
makefile
cpp 复制代码
HttpServer:HttpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f HttpServer

测试

一般我们在访问我们的服务的时候,是不需要加http:的,但是有些浏览器默认的是https:协议,比如Edge默认的就是https,如果是https就不能正确的发送请求给我们的服务端,而我们的服务端又存在一些问题(比如空请求的时候就会段错误),服务就会直接挂掉。

另外,我们的服务器是轻量级服务器,因此图片大了就有可能无法加载出来,比如第二张图片就没有加载出来。

这张图还能看到我们的Cookie。

对代码知识的补充

传输文件

rz -E 可以在Xshell中,将电脑的本地文件导入到Linux中。

sz 可以将linux的文件分享到本地

前提是我们安装lrzsz软件包,可以用命令

bash 复制代码
sudo yum install lrzsz 

std::stringstream

std::stringstream是C++标准库中的一个类,它提供了一种方便的方式来操作字符串。这个类可以被视为一个字符串流,允许你像操作I/O流 (如cin)一样来操作字符串。因此,你可以使用插入运算符(<<)和提取运算符(>>)来向流中插入数据和从流中提取数据。

以下是一些std::stringstream的主要特点和用法:

  1. 字符串与流的关联 :你可以将字符串与std::stringstream对象关联起来,这样就可以像处理流一样处理字符串。例如,你可以向std::stringstream对象写入数据,然后从该对象读取数据,就像处理文件或控制台输入/输出一样。
  2. 格式化字符串std::stringstream允许你创建格式化的字符串。你可以使用插入运算符将各种数据类型(如整数、浮点数、字符串等)插入到std::stringstream对象中,然后可以使用str()成员函数获取格式化后的字符串。
  3. 类型转换std::stringstream也可以用于类型转换。例如,你可以将一个整数插入到std::stringstream对象中,然后使用>>运算符将其提取为一个字符串。
  4. 清空流 :在使用std::stringstream进行多次操作之前,可能需要使用clear()方法来清空流。这可以确保之前的操作不会影响后续的操作。

下面是一个简单的示例,演示了如何使用std::stringstream

cpp 复制代码
#include <iostream>  
#include <sstream>  
  
int main() {  
    int num = 1234;  
    std::stringstream ss;  
      
    // 将整数插入到字符串流中  
    ss << num;  
      
    // 从字符串流中提取数据并转换为字符串  
    std::string str;  
    ss >> str;  
      
    // 输出字符串  
    std::cout << str << std::endl;  // 输出:1234  
      
    return 0;  
}

std::ifstream && std::ios::binar

  1. std::ifstream in :这是声明一个名为 instd::ifstream 对象。std::ifstream 是输入文件流类,用于从文件中读取数据。

  2. htmlpath :这是一个 std::string 类型的变量,它包含了要打开的文件的路径。在这个例子中,htmlpath 变量包含了我们想要读取的 HTML 文件的路径。

  3. std::ios::binary :这是一个标志,它告诉 std::ifstream 对象以二进制模式打开文件。在二进制模式下,文件会按照其原始的字节进行读取,不会进行任何特定的字符转换或解释。这对于读取非文本文件(如图像、音频、视频等)或者需要精确控制文件内容的文本文件(如某些二进制格式的文本文件)来说是非常有用的。

cpp 复制代码
std::ifstream in(htmlpath,std::ios::binary);

所以这里的代码就是以二进制打开这个路径文件。

顺便再说下

cpp 复制代码
in.seekg(0, std::ios_base::beg);

in.seekg(0, std::ios_base::beg); 是一个对 std::ifstream 对象 in 进行的操作,用于移动文件读取指针(也称为"get"指针或"seek"指针)到文件的指定位置。这里,我们使用的是 seekg 成员函数,它是专为 istream(包括 ifstream)的读取指针设计的。

函数的两个参数分别是:

  1. 偏移量(Offset) :在这个例子中,偏移量是 0。它表示从指定的起始点开始移动的字节数。

  2. 起始点(Dir) :这里使用的是 std::ios_base::beg,它是 std::ios_base 类中的一个静态成员,用于指示文件的开始位置。使用 beg 意味着文件读取指针将被设置回文件的开头。

假设你已经读取了文件的一部分,然后你想从头开始重新读取整个文件,或者在文件的不同部分之间跳转以读取不同的数据块,这时你就可以使用 seekg 函数来设置文件读取指针的位置。

需要注意的是,seekg 函数通常只在文件以二进制模式打开时才能保证正确定位,因为文本模式可能会根据操作系统的不同对换行符等字符进行转换。所以,如果你需要精确控制文件指针的位置,最好以二进制模式打开文件。

recv && send

我们之前的网络读写操作使用的是read和write。

其实我们还可以用以上这两个系统接口,与read和write不同的是,它们只有在最后一个参数不同,并且在这里我们设置为0,就是阻塞等待,那么作用实际是和read和write一模一样的。

关于参数的提交

在标签语言的代码中,我们可以用get或者post方法来进行参数的提交

在前端的表单form里面这个method后面跟的就是方法,一般如果不写,默认的就是get方法。

改post方法就是

使用get方法来获取参数,会在URL中进行回显

而post方法则不会,因此post方法会比get方法更私密一些。post会将参数放在正文部分。

但是安全性是差不多的,因为都没有加密,我们依旧可以在请求的报头中看到我们提交的参数。比如用一些抓包工具。

总结就是:

get方法通过URL提参,post采取的是请求的正文部分提交参数。

http中的一些属性

Location-状态码

除了3开头的,其他的都好理解,更有我们日常就很常见的404,这些状态码和描述我们不需要特别记忆,需要用的时候到网上搜即可。

接下来就关于重定向状态码了

关于重定向

首先我们要知道,在报头中的一个属性:

location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;

比如今天我们像浏览器发起了一个请求,但是由于某种原因,服务器不能给我们提供服务,那么服务器返回的响应就会有3XX的状态码,并且在报头中的Location属性中,会有一个新的域名,想让我们访问新的地址, 浏览器解析这样的信息后,就会发起二次请求,去访问新的地址。

所以重定向就是:让服务器指导浏览器,让浏览器去访问新的地址。

根据情况直接这样加就可以了。

临时重定向和永久重定向,其实二者也就只是字面意思了。

永久重定向的场景比如:一个老网站想要重新换新,连域名都换了,但是为了考虑到老用户,那么就可以考虑在老网站上进行永久重定向到新网站。

临时重定向其实很常见,比如我们登录网站首先是在登录界面,当我们登录成功以后就会跳转到网站首页,这个跳转工作肯定不是我们用户手动的,这个就是网站进行了临时的重定向。其实就是配合3XX的状态码和Location来让浏览器发起二次请求。

常见的属性

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

我们平常上网的时候,通常可以随时返回到上一个页面,这是因为在referer中记录了当前页面是从哪个页面跳转过来的。

可以简单对照一下

还有一个补充就是 Connection属性,我们来谈一谈

我们在网上看京东这样的商城时,发现页面除了文字,还有很多的图片。

一个巨大的页面是会包含非常多的元素的!(每一个元素就是一个资源)

比如这个网页假设有100张图片,那么我们就需要发请101次http的请求,其中第一次是请求这个网页的。 然后后面的100次请求就是把图片给申请下来,经过渲染形成了这个完整的网页。

但是我们知道,http协议底层是用的tcp协议,tcp协议是面向连接的,按照今天我们的代码来看,就需要建立101次链接,每一次返回,链接就断开,然后再链接。这样看来效率也是比较低效的,并且我们把这样的基于 请求-响应-关闭链接的响应称为 短链接响应。也就是短链接

经过后续的升级

客户端和服务端建立连接后,可以发送多个http的请求,服务端一边处理,一边返回响应,直到本轮的请求全部处理完毕,再断开链接,此时就只需要建立一次链接。这就有点像我们之前实现的网络计算器,一次链接可以做很多次的运算,直到我们不想再运算为止。

用一个tcp链接来传送多个请求,这就是长链接

因为早期的网页一般都比较小,所以采取的是短链接的方式。但是现在有很多大型的网站,那么就需要采取长链接的方式。

所以http1.0默认采取的就是短链接,并且也只能支持短链接。

http1.1默认采取的就是长链接。

因此虽然客户端和服务端所使用的http协议不一定都支持长链接技术,但是支持的一方可以不用长链接,因此才有Connection选项,来标识是否使用长链接技术。比如上图的中的就是 keep-alive,也就是只要双方在这里的属性都是keep-alive,那么就会采用长链接技术,否则就是短链接技术。

Content-Type

在前端的代码中,我们直接这样写,把图片的路径放进去,然后打开这个页面的时候,浏览器会自动发起二次请求。

因为图片是二进制的,因此我们得告诉浏览器它是什么类型的图片才能显示。

所以我们就需要一个报头,也就是Content-Type。

不过有的浏览器看到标签语言的开头就知道这是一个网页了,那么它就会自动解释图片,但是有的游览器不会,最好还是我们自己写上。

我们可以先看看Content-Type常用的对照表

我们要显示什么样的资源,我们的Content-Type就得是对应的才行。但是我们的一张网页可能会显示很多资源,不可能将Content-Type进行硬编码,在代码中还得做灵活的选择才行。

我们要访问什么资源,这个资源的后缀在file_path里面,因此我们可以通过后缀来选择Content-Type。

我们在构造函数中就先插入了两个类型

然后再通过一个函数来选择

cpp 复制代码
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];
        }

如果没有Content-Type,直接打开一个图片的话,大概率就是乱码

再看看读取操作,主要是对图片的

cpp 复制代码
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()); // 最后将这个二进制的内容读到content缓冲区中

            in.close();
            return content;
        }

要用二进制来读。

另外,当我们获取同一个图片多次的时候

有些浏览器会进行优化,将重复相同的申请转变成一次申请,浏览器会复用刚刚用到的图片。

这个属性是用来登录验证用的,比如我们第一次访问B站,它进行登录。我们在登录之后,下一次访问B站的时候,发现它已经登录好了,不需要我们再登录了,这是因为Cookie保留了我们的登录信息,并且一般存在一个有效期。

比如在Edge浏览器中,我们点击左上角那个锁就能看到这个网站正在使用的Kookie,Cookie里面也包含了很多很多的属性,并且各自有各自的有效期。

http协议默认是无状态的。

我们可以在代码中,定义报头的时候加上,比如我们设置的Cookie属性就只有用户名和密码

这样我们的浏览器在访问同一个网站的时候,就会携带Cookie文件中的内容--比如用户名和密码。

浏览器保存Cookie文件一般有两种保存方法:1.文件级 2.内存级

这也很好理解,内存级就是浏览器把这个文件加载到内存中了,如果我们把浏览器关了,那么下次打开浏览器的时候这个Cookie文件就没有了。

文件级就是把Cookie文件保存在硬盘中,这样下次打开浏览器这个Cookie文件依旧被保留了下来。

如果想让安全性高的就用内存级。

再说说代码细节,我们是通过setCookie来写入浏览器的。并且我们也可以设置Cookie的有效时长。

cpp 复制代码
response_header += "Set-Cookie: name=hzj";
response_header += sep;
response_header += "Set-Cookie: pasword=54088";
response_header += sep;

设置格式是这样的。

(我们的代码中仅仅只是为了测试效果而已。)

网上有一些黑客,当我们访问一些带病毒的网站的时候,这些木马就会在我们的浏览器上开一个小端口,然后全盘扫描浏览器的Cookie文件,当黑客拿到Cookie文件后去相应的网站去登录,就可以直接以我们的身份登录了,以前的QQ盗号原理也是这么来的。

那么用Cookie来保存用户的登录信息就会面临两个问题:

1.Cookie被盗取的问题,别人就可以用你的身份去登录

2.个人私有信息泄露的问题,比如我们的账号密码都会被黑客一并知道。

对于这些状况,大多数网站的企业是这样的,我们还是以B站为例。

现在登录后,在B站的服务器上会给我们生成一个session文件,文件里面包含了用户合法信息与登录相关。并且会生成一个sessionID,登录后就给用户返回一个sessionID,那么下一次用户就不再使用Cookie而是sessionID来登录网站了。这样做的好处就是即便黑客再次盗取了你的信息,他最多只能以你的身份登录网站,并不能知道你的用户名和密码,也就是说个人私有信息得到了保护。

这个方案中,sessionID是服务器统一管理分配的,在大型网站中,一般都会有几百万的用户在登录,服务器一般会把session文件放在redis中管理。

另外,现在对于Cookie被盗,现在也有很多的解决方案,比如可以进行ip定位,如果两次登录的ip位置差距太明显,那么就会设置登录异常来让这次登录的用户进行验证(比如短信验证)等等。

相关推荐
秋凉 づᐇ9 小时前
每日一题--计算机网络
计算机网络
丿罗小黑10 小时前
Pytest项目_day01(HTTP接口)
网络协议·http·pytest
小白学大数据10 小时前
Superagent 异步请求:如何处理复杂的 HTTP 场景
开发语言·网络·python·网络协议·http
贺椿椿12 小时前
ensp服务器DNS/HTTP/DHCP配置
linux·服务器·网络·http·智能路由器
给我个面子中不13 小时前
https握手过程
网络协议·http·https
day day day ...13 小时前
Spring Boot 应用的接口访问从 HTTP 改为 HTTPS
spring boot·http·https
智联视频超融合平台14 小时前
大华HTTP协议在智联视频超融合平台中的接入方法
网络协议·http·音视频·实时音视频·视频编解码
噼里啪啦啦.16 小时前
计算机网络原理
服务器·网络·计算机网络
编程之升级打怪19 小时前
HTTP服务器的工作逻辑
http
小爬虫程序猿21 小时前
爬虫代码中需要设置哪些HTTP头部信息?
爬虫·网络协议·http