【Linux网络】应用层协议——http协议

目录

HTTP协议

认识URL

urlencode和urldecode   

HTTP协议格式

HTTP请求协议格式

获取浏览器的HTTP请求

HTTP响应协议格式

构建HTTP响应给浏览器

构建处理HTTP请求类及代码完善

HTTP的方法

GET方法和POST方法

HTTP的状态码

HTTP常见Header

Cookie&Session


HTTP协议

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

HTTP(Hyper Text Transfer Protocol)协议又叫做超文本传输协议,是一个简单的请求-响应协议。是一种最基本的客户机/服务器的访问协议。浏览器向服务器发送请求,而服务器回应相应的网页。它是从万维网(WWW)服务器传输超文本到本地浏览器的传送协议。HTTP通常运行在TCP之上。

认识URL

平时我们俗称的 "网址" 其实就是说的 URL。

URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。

下面我们来具体分析一下上面这一段url: 

1.协议方案名

http:// 表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS。

HTTPS:这是HTTP的安全版本,通过在HTTP下加入SSL层,对传输的数据进行加密和身份认证,保证了数据传输过程的安全性。

常见的应用层协议:

  • HTTP(Hyper Text Transfer Protocol,超文本传输协议)
  • HTTPS:(Hyper Text Transfer Protocol over SecureSocket Layer)协议:安全数据传输协议。
  • FTP(File Transfer Protocol,文件传输协议)
  • SMTP(Simple Mail Transfer Protocol,简单邮件传送协议)
  • POP3(Post Office Protocol 3,邮件读取协议)
  • Telnet:这是一个简单的远程终端协议,允许用户在本地就能控制服务器。
  • DNS(Domain Name System,域名系统)
  • SNMP(Simple Network Management Protocol,简单网络管理协议)
  • DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)

2.登录信息

usr:pass表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器。

3.服务器地址

www.example.jp表示的是服务器地址,也叫做域名,比如www.qq.comwww.baidu.com

需要注意的是,我们用IP地址标识公网内的一台主机,但IP地址本身并不适合给用户看。比如说我们可以通过ping命令,分别获得www.baidu.comwww.qq.com这两个域名解析后的IP地址。

如果用户直接看到两个IP地址,他们可能无法直接了解这些地址背后是哪个网站或服务。但对于像www.baidu.comwww.qq.com这样的域名,用户能够轻易地识别出它们分别对应的是百度和腾讯的服务。因此,域名比IP地址更具自描述性,能够帮助用户更直观地了解他们正在访问的网站或服务的来源。

从技术角度看,域名和IP地址在功能上是等价的。在计算机网络内部,无论是域名还是IP地址,都可以用来唯一标识一个服务器。然而,对于用户来说,URL 是他们接触到的网络地址形式,因此URL中通常使用域名而非IP地址来表示服务器地址。

4.服务器端口号 

80表示的是服务器端口号。HTTP协议和套接字编程一样都是位于应用层的,在进行套接字编程时我们需要给服务器绑定对应的IP和端口,而这里的应用层协议也同样需要有明确的端口号。

常见协议对应的端口号:

协议名称对应端口号
HTTP80
HTTPS443
SSH22

当我们利用特定的协议进行网络交互时,这些协议本质上就是在为我们提供各种服务。现在,各种常用的网络服务与其对应的端口号之间已经有了明确的映射关系。因此,在实际使用中,我们往往不需要显式地指定某个协议所使用的端口号,因为计算机系统能够根据协议类型自动识别出应使用哪个端口。在URL(统一资源定位符)中,服务器的端口号通常也是被省略的,这是因为浏览器或其他客户端软件会根据URL中的协议类型自动选择正确的端口号进行连接。

5.带层次的文件路径

/dir/index.htm 这部分表示的是要访问的资源在服务器上的存储路径。当我们想要从服务器上获取某种资源时,除了要知道服务器在哪里(通过域名和端口确定),还需要告诉服务器我们想要的具体内容是什么,这就需要通过资源路径来指明。

以百度为例,当我们在浏览器中输入百度的域名并按下回车,浏览器会向百度服务器发送一个请求,请求中包含了想要获取的资源路径(通常是网站的首页)。百度服务器在接收到请求后,会根据请求中的路径信息找到相应的资源,并将其返回给浏览器。这样,我们就能在浏览器中看到百度的首页内容了。

我们通过网站的开发者工具可以看到,当我们发起网页请求时,本质是获得了这样的一张网页信息,然后浏览器对这张网页信息进行解释,最后就呈现出了对应的网页。 

我们将网络上的这些资源统称为网页资源,但实际上,当我们通过浏览器访问互联网时,我们可能会请求各种各样的资源,包括视频、音频、图片等。HTTP协议之所以被称为“超文本传输协议”而不是简单的“文本传输协议”,是因为它所传输的资源类型远远超出了纯文本的范围。

在URL中,有一个特定的部分用于指示所请求资源的具体位置,这部分被称为路径。细心观察可以发现,路径中的分隔符使用的是正斜杠“/”而不是反斜杠“\”。这实际上是一个有趣的细节,它反映了大多数网络服务都是基于类Unix系统(如Linux)进行部署的。因为在Unix和Linux系统中,文件路径的分隔符是正斜杠“/”,而在Windows系统中,文件路径的分隔符是反斜杠“\”。

因此,URL中使用的路径分隔符“/”不仅是一个简单的符号,它还隐含着背后服务器操作系统和文件系统的选择。这体现了网络协议与技术实现之间的紧密联系。

6.查询字符串

  • 定义:查询字符串是URL中位于问号(?)后面的部分,它用于传递参数和数值给服务器。
  • 示例:在URL https://www.example.com/search?q=apple&category=fruit 中,?q=apple&category=fruit 就是查询字符串。
  • 作用:服务器可以根据查询字符串中的参数和值来执行相应的操作。例如,在上述示例中,服务器可能会根据q=apple和category=fruit这两个参数来搜索包含“apple”关键字的水果。

因此双方在进行网络通信时,是能够通过URL进行用户数据传送的。 

7.片段标识符

  • 定义:片段标识符是URL中“#”后面的部分,它用于标识文档内的某个位置(如锚点)。
  • 示例:在URL https://www.example.com/article#section1 中,#section1 就是片段标识符。
  • 作用:片段标识符允许用户直接跳转到页面的某个部分,而不需要滚动整个页面。这对于长页面或具有多个部分的页面非常有用。  

urlencode和urldecode   

/ ? : 等这样的字符,已经被url当做特殊意义理解了。因此这些字符不能随意出现。
比如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。

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

例如:

比如当我们搜索C++时,由于+加号在URL当中也是特殊符号,而+字符转为十六进制后的值就是0x2B,因此一个+就会被编码成一个%2B。

说明:URL当中除了会对这些特殊符号做编码,对中文也会进行编码。 

urldecode就是urlencode的逆过程; 

在线编码工具 

UrlEncode编码/UrlDecode解码 - 站长工具

选中其中的URL编码/解码模式,在输入C++后点击编码就能得到编码后的结果。

再点击解码就能得到原来输入的C++。

实际当服务器拿到对应的URL后,也需要对编码后的参数进行解码,此时服务器才能拿到你想要传递的参数,解码实际就是编码的逆过程。

HTTP协议格式

在网络通信中,应用层协议如HTTP和HTTPS负责数据的表示与交换,而传输层常见的TCP协议则确保数据的可靠传输。网络层常见的IP协议负责数据的路由与寻址,而数据链路层常见的MAC帧则负责物理地址的识别与数据的封装。值得注意的是,下三层(传输层、网络层和数据链路层)的工作通常是由操作系统或相应的驱动程序来处理的,这些层次主要聚焦于通信过程中的细节问题。因此,在应用层的视角中,它通常不需要关心这些底层细节,而是可以直接认为它正在与对方的应用层进行直接的数据交互。

下三层协议(传输层、网络层和数据链路层)是网络通信中的基础设施,它们负责处理数据的传输、路由和物理传输等细节,确保数据能够准确无误地从一台主机传输到另一台主机。而应用层则专注于如何利用这些传输过来的数据,为用户或应用程序提供服务。在这个过程中,应用层并不直接关心数据是如何在底层网络中传输的,因为下三层协议已经为它们提供了可靠的通信支持。

HTTP是一种典型的应用层协议,它基于请求和响应的模式进行工作。作为客户端,我们可以向服务器发送请求(request),服务器在接收到请求后,会对请求进行分析,了解我们想要访问的资源,并据此构建相应的响应(response)。这种请求-响应模式构成了客户端-服务器(CS)或浏览器-服务器(BS)的通信模式。

为了有效地使用HTTP协议,我们需要了解HTTP请求和响应的格式。这些格式定义了如何组织数据、如何指定要访问的资源以及如何传递状态信息等。因此,学习HTTP的重点之一就是掌握其请求和响应的格式,以便能够正确地发送请求并处理响应。

HTTP请求协议格式

HTTP请求协议格式如下:

HTTP请求由四个主要部分组成:

  • 请求行:这部分包含了请求方法(如GET、POST等)、请求的URL地址,以及所使用的HTTP版本信息。这三者通过空格分隔,构成了请求行的核心内容。
  • 请求报头:请求报头包含了多个以“key: value”形式呈现的属性,每一对属性占据一行。这些属性为服务器提供了关于请求的额外信息,如发送请求的客户端类型、请求资源的类型等。
  • 空行:在请求报头之后,通常会跟随一个空行。这个空行的出现标志着请求报头的结束,同时也是请求正文开始的信号。
  • 请求正文:请求正文是可选的,它允许为空字符串。当客户端需要向服务器发送数据时(如表单提交、文件上传等),这些数据会被放置在请求正文中。为了确保服务器能够正确解析请求正文,请求报头中通常会包含一个“Content-Length”属性,用于标识请求正文的长度

在以上四个部分中,请求行、请求报头和空行通常是HTTP协议标准规定的,由HTTP协议自行处理。而请求正文则是由用户或应用程序根据实际需要来生成的,它包含了用户想要发送给服务器的数据或信息。如果用户在请求时没有需要上传的数据,那么请求正文就会是一个空字符串。

如何将HTTP请求的报头与有效载荷进行分离?

  • 当应用层收到一个HTTP请求时,它必须想办法将HTTP的报头与有效载荷进行分离。对于HTTP请求来讲,这里的请求行和请求报头就是HTTP的报头信息,而这里的请求正文实际就是HTTP的有效载荷。
  • 为了实现报头与有效载荷的分离,服务器在接收HTTP请求时会按行读取数据。在正常情况下,HTTP请求的各个部分之间以换行符(\n)分隔。HTTP请求当中的空行就是用来分离报头和有效载荷的。所以当服务器连续读取到两个换行符时,这标志着报头部分的结束和有效载荷的开始。具体来说,第一个换行符标志着请求报头的结束,而紧接着的第二个换行符则作为报头与有效载荷之间的分隔符。

获取浏览器的HTTP请求

在网络通信中,应用层之下的层级是传输层,HTTP协议在底层通常依赖于TCP(传输控制协议)来确保数据的可靠传输。因此,我们可以编写一个基于TCP的服务器,并通过套接字(socket)来实现。随后,通过启动浏览器访问这个服务器,我们可以模拟真实的网络交互。

由于我们的服务器是直接通过TCP套接字来读取浏览器发送的HTTP请求,这意味着在应用层之上,并没有对HTTP请求进行任何预处理或解析。因此,我们可以直接将浏览器发送的HTTP请求数据捕获并打印输出。这样一来,我们就能够直观地看到HTTP请求的基本结构,包括请求行、请求头部和请求正文等组成部分,从而更好地理解HTTP协议的工作机制。

因此下面我们编写一个简单的TCP服务器,这个服务器要做的就是把浏览器发来的HTTP请求进行打印即可。

HttpServer.hpp

#pragma once

#include <iostream>
#include "Socket.hpp"
#include"Log.hpp"


static const int defaultport = 8888;

class HttpServer;

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

class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport)
    :port_(port)
    {
    }

    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);
        }
    }

    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; // 假设我们读取到的就是一个完整的,独立的http 请求
        }
        close(sockfd);
    }

    static void* ThreadRun(void* args)//
    {
        pthread_detach(pthread_self());
        ThreadData* td = static_cast<ThreadData*>(args);

        td->svr->HandlerHttp(td->sockfd);
        return nullptr;
    }

    ~HttpServer()
    {}
private:
    Sock listensock_;
    uint16_t port_;
};

HttpServer.cpp

#include "HttpServer.hpp"
#include <iostream>
#include <memory>
#include <pthread.h>
#include <string>
#include "Log.hpp"

using namespace std;

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        exit(1);
    }

    uint16_t port = std::stoi(argv[1]);

    // HttpServer* svr = new HttpServer(port);
    //std::unique<HttpServer> svr(new HttpServer());
    std::unique_ptr<HttpServer> svr(new HttpServer(port));
    svr->Start();
    return 0;
}

  • 浏览器向我们的服务器发起HTTP请求后,因为我们的服务器没有对进行响应,此时浏览器就会认为服务器没有收到,然后再不断发起新的HTTP请求,因此虽然我们只用浏览器访问了一次,但会受到多次HTTP请求。
  • 由于浏览器发起请求时默认用的就是HTTP协议,因此我们在浏览器的url框当中输入网址时可以不用指明HTTP协议。
  • url当中的/不能称之为我们云服务器上根目录,这个/表示的是web根目录,这个web根目录可以是你的机器上的任何一个目录,这个是可以自己指定的,不一定就是Linux的根目录。

在HTTP请求中,请求行所包含的URL通常不包含域名和端口号。这些信息会在请求报头的Host字段中明确指出。请求行中的URL主要标识了客户端希望访问服务器上的哪个路径下的资源。当浏览器访问服务器并指定要访问的资源路径时,浏览器发起的HTTP请求中的URL也会相应地改变,以反映这一特定的资源路径。 

而请求报头当中全部都是以 key: value 形式按行陈列的各种请求属性,请求属性陈列完后紧接着的就是一个空行,空行后的就是本次HTTP请求的请求正文,此时请求正文为空字符串,因此这里有两个空行。 

HTTP响应协议格式

HTTP响应协议格式如下:

HTTP响应由以下四部分组成:

  • 状态行:[http版本]+[状态码]+[状态码描述]
  • 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。
  • 空行:遇到空行表示响应报头结束。
  • 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。

如何将HTTP响应的报头与有效载荷进行分离?

对于HTTP响应来讲,这里的状态行和响应报头就是HTTP的报头信息,而这里的响应正文实际就是HTTP的有效载荷。与HTTP请求相同,当应用层收到一个HTTP响应时,也是根据HTTP响应当中的空行来分离报头和有效载荷的。当客户端收到一个HTTP响应后,就可以按行进行读取,如果读取到空行则说明报头已经读取完毕。

构建HTTP响应给浏览器

服务器在接收到来自客户端的HTTP请求后,会深入解析这个请求中的各种数据,并据此构建一个相应的HTTP响应以返回给客户端。然而,在实际操作中,当我们的服务器与客户端建立连接后,它仅读取客户端发送的HTTP请求,随后就断开了连接。

为了响应客户端的请求,我们可以构建一个HTTP响应并发送给浏览器。由于目前尚无法分析浏览器发送的HTTP请求,我们可以选择返回一个固定的HTTP响应。为此,我们将当前服务程序所在的目录指定为web的根目录,并在该目录下创建一个HTML文件。接着,我们编写一个简单的HTML文档,将其作为当前服务器的默认首页。这样,无论客户端请求的具体路径是什么,服务器都将返回这个固定的HTML页面作为响应。

index.html

<html>
    <head></head>
    <body>
        <h1>这里是首页</h1>
        <h2>Hellp Http</h2>
    </body>
</html>

当浏览器向服务器发起HTTP请求时,不管浏览器发来的是什么请求,我们都将这个网页响应给浏览器,此时这个html文件的内容就应该放在响应正文当中,我们只需读取该文件当中的内容,然后将其作为响应正文即可。

这里我们将html文件放在当前文件夹下的wwwroot文件夹下:

#pragma once

#include <iostream>
#include <fstream>
#include "Socket.hpp"
#include"Log.hpp"

const std::string wwwroot = "./wwwroot";// web 根目录

static const int defaultport = 8888;

class HttpServer;

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

class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport)
    :port_(port)
    {}

    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);
        }
    }
    
    //读取html文件
    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;
    }

    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; // 假设我们读取到的就是一个完整的,独立的http 请求

            // 返回响应的过程
            //响应正文
            std::string text;
            text = ReadHtmlContent("wwwroot/index.html");//失败?
            //状态行
            std::string response_line = "HTTP/1.0 200 OK\r\n";
            //响应报头
            std::string response_header = "Content-Length: "; 
            response_header += std::to_string(text.size()); // Content-Length: 11
            response_header += "\r\n";
            //空行
            std::string blank_line = "\r\n"; // \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);
        return nullptr;
    }

    ~HttpServer()
    {}
private:
    Sock listensock_;
    uint16_t port_;

};

因此当浏览器访问我们的服务器时,服务器会将这个index.html文件响应给浏览器,而该html文件被浏览器解释后就会显示出相应的内容。

实际我们在进行网络请求的时候,如果不指明请求资源的路径,此时默认你想访问的就是目标网站的首页,也就是wwwroot根目录下的index.html文件。
由于只是作为示例,我们在构建HTTP响应时,在响应报头当中只添加了一个属性信息Content-Length,表示响应正文的长度,实际HTTP响应报头当中的属性信息还有很多。

HTTP为什么要交互版本?

  • 在HTTP通信中,请求行和状态行各自包含了发送方(客户端或服务器)所使用的HTTP版本信息。客户端通过请求行传达其支持的HTTP版本,而服务器则通过状态行回应其实际使用的版本。这种版本信息的交互至关重要,因为它确保了不同版本的HTTP客户端和服务器能够相互兼容并顺利通信。
  • 由于客户端和服务器可能采用不同的HTTP版本,为了提供最佳的服务和避免通信障碍,双方需要进行版本协商。客户端在发送请求时明确告知服务器其支持的HTTP版本,服务器据此确定能否提供相应版本的服务。通过这种方法,服务器可以确保为不同类型的客户端提供合适的响应,避免因版本不匹配而引发的通信问题。

通过在HTTP请求和响应中交换版本信息,客户端和服务器能够确保彼此的兼容性,从而提供稳定、高效的通信服务。

构建处理HTTP请求类及代码完善

上面前一小节的内容中我们的浏览器向服务器发起HTTP请求时,不管浏览器发来的是什么请求,我们都将同一个网页响应给浏览器。

但实际上浏览器肯定还会请求申请其它资源,所以为了处理浏览器发来申请其它资源的请求,我们需要对浏览器发来的请求进行解析。

我们定义一个名为HttpRequest的类,该类用于处理HTTP请求。这个类具有以下几个功能: 

  1. 反序列化(Deserialize):该方法用于解析传入的HTTP请求字符串(req)。它使用一个分隔符(\r\n)来分割请求字符串中的不同部分。反序列化过程中,将请求头(直到空行为止)分割成多行,并将这些行存储在req_header向量中。之后,空行之后的内容被认为是请求体(text),并存储在text成员变量中。

  2. 解析(Parse):该方法用于解析请求行(存储在req_header的第一个元素中)。它使用std::stringstream来按空格分隔请求行中的各个部分,并将它们分别赋值给method(请求方法,如GET或POST)、url(请求的URL路径)和http_version(HTTP版本,如HTTP/1.1)。然后,根据解析出的URL,它构建了一个实际要访问的资源的文件路径(file_path),这个路径基于一个预定义的根目录(wwwroot)。

  3. 调试打印(DebugPrint):该方法用于打印请求的内容。它遍历req_header向量,打印每一行请求头,然后打印解析出的请求方法、URL、HTTP版本和文件路径,最后打印请求体。

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);
            req_header.push_back(temp);//将读取到req的每一行放入req_header
            req.erase(0,pos + sep.size());//将已经读取到的当前行删除
        }
        text = req;//空行之前的内容都存放在req_header中了,这时候的req就是正文部分了,我们把他放在text
    }   

    void Parse()
    {
        std::stringstream ss(req_header[0]);//stringstream可以按空格分开字符串,我们用stringstream解析请求行
        ss >> method >> url >> http_version;
        file_path = wwwroot; // ./wwwroot
        //根据用户申请访问的url,动态更改文件的路径
        if(url == "/" || url == "/index.html"){//访问的是网页根目录或者首页
            file_path += "/";
            file_path += homepage; // ./wwwroot/index.html
        }
        else//访问的是其它页面
            file_path += url;// /a/b/c/d.html->./wwwroot/a/b/c/d.html

        auto pos = file_path.rfind(".");
        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: " << 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;
    std::string file_path; // ./wwwroot/a/b/c.html 2.png

    std::string suffix;//文件类型
};

为了方便更好展示,我们再wwwroot目录下再创建两个文件夹,文件夹里面分别放两个html文件

hello.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>
    <h1>这是第二张网页</h1>
    <h1>这是第二张网页</h1>
    <h1>这是第二张网页</h1>
    <h1>这是第二张网页</h1>
    <h1>这是第二张网页</h1>
    <h1>这是第二张网页</h1>
    <h1>这是第二张网页</h1>
    <a href="http://60.204.169.245:8888">回到首页</a>

    <a href="http://60.204.169.245:8888/x/y/world.html">到第三张网页</a>
</body>
</html>

world.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>
    <h1>这是第3张网页</h1>
    <h1>这是第3张网页</h1>
    <h1>这是第3张网页</h1>
    <h1>这是第3张网页</h1>
    <h1>这是第3张网页</h1>
    <h1>这是第3张网页</h1>

    <a href="http://60.204.169.245:8888">回到首页</a>
    <a href="http://60.204.169.245:8888/a/b/hello.html">到第二张网页</a>
</body>
</html>

我们再修改index.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>
    <h1>这里是首页</h1>
    <img src="/image/1.png" alt="这是一直猫" width="100" height="100"> <!--根据src向我们的服务器浏览器自动发起二次请求 -->
    <h2>访问其它页面:</h2>
    <a href="http://60.204.169.245:8888/a/b/hello.html">到第二张网页</a>
    <a href="http://60.204.169.245:8888s/x/y/world.html">到第三张网页</a>
</body>
</html>
<a href="http://60.204.169.245:8888/a/b/hello.html">到第二张网页</a>
<a href="http://60.204.169.245:8888s/x/y/world.html">到第三张网页</a> 

这两个HTML锚标签(<a> 标签)用于创建超链接,允许用户点击链接跳转到指定的网页。

实际上我们在用浏览器访问一个网页时,比如我们在访问淘宝时,通过点击商品进入到其它链接。所以我们在每个html中放入两个链接用于申请访问其它网页资源。实际上,我们每一次点击,都是一次Http请求。

完整代码:

为了能够显示图片,我们对ReadHtmlContent函数进行修改,读取文件时我们需要使用二进制读取。

要请求的文件类型通过解析之后放在HttpRequest类中的suffix变量。我们还需要添加文件类型的报头,在HttpServer添加映射表content_type,传入suffix变量到SuffixToDesc函数,并获取文件类型。

#pragma once

#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#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 = 8888;

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);
            req_header.push_back(temp);//将读取到req的每一行放入req_header
            req.erase(0,pos + sep.size());//将已经读取到的当前行删除
        }
        text = req;//空行之前的内容都存放在req_header中了,这时候的req就是正文部分了,我们把他放在text
    }   

    void Parse()
    {
        std::stringstream ss(req_header[0]);//stringstream可以按空格分开字符串,我们用stringstream解析请求行
        ss >> method >> url >> http_version;
        file_path = wwwroot; // ./wwwroot
        //根据用户申请访问的url,动态更改文件的路径
        if(url == "/" || url == "/index.html"){//访问的是网页根目录或者首页
            file_path += "/";
            file_path += homepage; // ./wwwroot/index.html
        }
        else//访问的是其它页面
            file_path += url;// /a/b/c/d.html->./wwwroot/a/b/c/d.html

        //获取文件后缀
        auto pos = file_path.rfind(".");
        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: " << 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;
    std::string file_path; // ./wwwroot/a/b/c.html 2.png

    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::iostream::binary);
        if(!in.is_open())   return "404";

        // seekg是一个成员函数,用于设置输入流的位置
        in.seekg(0,std::ios_base::end);//将文件流的位置指示器移动到文件的最后一个字节之后。0表示偏移量,而std::ios_base::end是一个常量,表示从文件的末尾开始计算偏移量。
        auto len = in.tellg();//获取当前文件流的位置,并将其存储在变量len中。
        in.seekg(0,std::ios_base::beg);//将文件流的位置指示器重新设置到文件的开头。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 << std::endl; // 假设我们读取到的就是一个完整的,独立的http 请求
            HttpRequest req;
            req.Deserialize(buffer);
            req.Parse();
            req.DebugPrint();

            // 返回响应的过程
            //响应正文
            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_line = "HTTP/1.0 307 OK\r\n";
            //响应报头
            std::string response_header = "Content-Length: "; 
            response_header += std::to_string(text.size()); // Content-Length: 11
            response_header += "\r\n";
            response_header += SuffixToDesc(req.suffix);//文件类型
            response_header += "\r\n";

            //空行
            std::string blank_line = "\r\n"; // \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;//文件的类型映射表,key是文件的后缀
};

代码测试:

HTTP的方法

HTTP常见的方法如下: 

其中最常用的就是GET方法和POST方法.

GET方法和POST方法

GET方法常用于检索或获取特定资源的信息,而POST方法则多用于向服务器提交或上传数据。尽管传统上认为POST是上传数据的首选方法,但在某些情境下,例如百度搜索时,GET方法也被用于数据提交。

两者在传递参数时有所不同:

  • GET方法将参数附加到URL的末尾,通过查询字符串的形式传递;
  • 而POST方法则是将参数包含在请求的正文中,通常用于发送大量数据。

由于URL的长度存在限制,因此GET方法在传递大量数据时可能不太适用,而POST方法则无此限制,可以携带更多的数据。

此外,从隐私性的角度来看,POST方法相较于GET方法更为安全。因为POST方法不会在浏览器的地址栏中显示参数,从而减少了数据被他人窥探的风险。然而,需要注意的是,无论是GET方法还是POST方法,都不应被视为绝对安全的数据传输方式。为了确保数据的安全性,应该采用适当的加密措施,如HTTPS协议,来加密传输的数据。

Postman演示GET和POST的区别

当使用GET方法访问服务器时,参数通常是通过URL进行传递的。在Postman这样的工具中,Params部分允许你设置这些参数,而这些参数实际上会附加到URL的末尾。当你在Params中设置参数时,你会注意到URL本身也在实时更新,以反映这些新添加的参数。

而当我们使用POST方法时,参数通常是通过请求的正文(body)进行传递的。在Postman中,你可以在Body部分设置这些参数。选择Raw方式传参意味着你输入的参数将保持原样进行传递,不做任何格式转换或编码。这种方式允许你精确地控制传递给服务器的数据格式和内容。

我们看到,由于此时响应正文不为空字符串,因此响应报头当中出现了Content-Length属性,表示响应正文的长度。

TCP套接字演示GET和POST的区别

要演示GET方法和POST方法传参的区别,就需要让浏览器提交参数,此时我们可以在index.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>
    <h1>这里是首页</h1>
    <form action="/a/b/hello.html" method="get">
        name: <input type="text" name="name"><br>
        password: <input type="password" name="passwd"><br>
        <input type="submit" value="提交">
    </form>
</body>
</html>

我们可以通过修改表单当中的method属性指定参数提交的方法,还有一个属性叫做action,表示想把这个表单提交给服务器上的哪个资源。

此时当我们用浏览器访问我们的服务器时,就会显示这两个表单。

提交后我们看到参数附加到URL的末尾

同时在服务器这边也通过url收到了刚才我们在浏览器提交的参数。

由于我们实际访问到的路径带上了参数,所以没有将网页显示出来。

如果我们将提交表单的方法改为POST方法,此时当我们填充完用户名和密码进行提交时,对应提交的参数就不会在url当中体现出来,而会通过正文将这两个参数传递给了服务器。

我们输入用户名和密码后提交,此时用户名和密码就通过正文的形式传递给服务器了。

  • 在使用GET方法时,提交的参数会直接显示在URL中,这意味着这些参数对用户和任何查看URL的人都可见。因此,GET方法通常适用于传输不敏感或非私密的数据。
  • 如果你需要传递敏感或私密信息,通常推荐使用POST方法,尽管GET和POST在传输数据时都是明文形式,安全性上并无显著差异。POST方法之所以被视为更私密,是因为它通过请求正文传递参数,而不是像GET方法那样将参数附加到URL上。这样,敏感信息就不会立即显示在浏览器的地址栏中,从而提高了私密性。然而,这并不意味着POST方法就绝对安全,开发者仍需要采取其他安全措施来保护传输的数据。

HTTP的状态码

HTTP的状态码如下: 

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

Redirection(重定向状态码)

  • 重定向是网络请求处理中的一种机制,用于将用户的访问引导到不同的网络位置。在这个过程中,服务器起到了指引的作用,帮助用户访问到正确的目标地址。
  • 重定向可以分为临时重定向和永久重定向两种类型。其中,状态码301代表的是永久重定向,意味着目标地址已经发生了改变,且这种改变是永久性的。而状态码302和307则代表临时重定向,表示目标地址只是暂时发生了变化,这种变化可能是暂时的,未来可能还会发生改变。
  • 这两种重定向方式的主要区别在于它们对客户端行为的影响。对于永久重定向,当浏览器首次访问一个网站并遇到301状态码时,它会记住这个新的目标地址,并在以后的访问中直接访问这个新地址,而不再需要重定向。而对于临时重定向,浏览器在每次访问网站时,如果仍然需要重定向,都会进行跳转,直到目标地址发生变化或重定向不再需要。

临时重定向演示

进行临时重定向时需要用到Location字段,Location字段是HTTP报头当中的一个属性信息,该字段表明了你所要重定向到的目标网站。

我们这里要演示临时重定向,可以将HTTP响应当中的状态码改为307,然后跟上对应的状态码描述,此外,还需要在HTTP响应报头当中添加Location字段,这个Location后面跟的就是你需要重定向到的网页,比如我们这里将其设置为腾讯网的首页。

此时当浏览器访问我们的服务器时,就会立马跳转到CSDN的首页。 

HTTP常见Header

  • Content-Type: 数据类型(text/html等)
  • Content-Length: Body的长度
  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent: 声明用户的操作系统和浏览器版本信息;
  • referer: 当前页面是从哪个页面跳转过来的;记录上一个页面的好处一方面是方便回退,另一方面可以知道我们当前页面与上一个页面之间的相关性。
  • location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
  • Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

Host

Host字段表明了客户端要访问的服务的IP和端口,比如当浏览器访问我们的服务器时,浏览器发来的HTTP请求当中的Host字段填的就是我们的IP和端口。但客户端不就是要访问服务器吗?为什么客户端还要告诉服务器它要访问的服务对应的IP和端口?

因为有些服务器实际提供的是一种代理服务,也就是代替客户端向其他服务器发起请求,然后将请求得到的结果再返回给客户端。在这种情况下客户端就必须告诉代理服务器它要访问的服务对应的IP和端口,此时Host提供的信息就有效了。

User-Agent

User-Agent包含了关于发送请求的客户端设备的信息,如操作系统类型、版本,以及所使用的浏览器类型和版本等。当我们的电脑或其他设备向某个网站发送请求时,User-Agent字段会将这些信息一并发送给网站服务器。

例如,当我们在浏览器中访问一个提供软件下载的网站时,该网站会根据User-Agent字段中携带的操作系统信息,自动为我们展示与我们的操作系统兼容的软件版本。这样,无论我们使用的是Windows、macOS、Linux还是其他操作系统,我们都能得到适合自己系统的软件版本推荐。

Keep-Alive(长连接)

HTTP/1.0遵循请求-响应模式,意味着每次客户端向服务器发送请求后,服务器会响应并随后关闭连接。然而,这种“一问一答”的方式在每次交互后都关闭连接会造成资源的浪费。为了解决这个问题,HTTP/1.1引入了长连接的概念。

长连接允许客户端和服务器在建立连接后,不立即关闭它,而是允许客户端继续通过这个已经建立的连接发送多个HTTP请求。服务器则会按顺序读取并处理这些请求,然后发送相应的响应。这种方式显著提高了网络效率,因为一条连接可以处理多个请求和响应,减少了频繁建立和关闭连接的开销。

要表明一个HTTP请求或响应支持长连接,可以在报头中的Connection字段设置值为"Keep-Alive"。这告诉对端,当前连接应保持打开状态,以便后续的请求和响应可以继续通过这个连接进行。通过这种方式,HTTP/1.1的长连接特性显著提升了Web通信的性能和效率。

Cookie&Session

HTTP协议本质上是一种无状态的通信机制,意味着每个请求和响应之间是相互独立的,服务器不会记住之前的请求状态。然而,在实际的网页浏览体验中,我们经常会发现网站能够记住我们的某些状态,如登录信息、购物车内容等,这背后的技术就是Cookie。

以CSDN网站为例,当我们首次登录后,CSDN服务器会创建一个包含我们登录信息的Cookie,并将这个Cookie发送给浏览器。浏览器会将这个Cookie保存在本地,并在随后的每次请求中自动将这个Cookie发送回CSDN服务器。这样,即使我们关闭了CSDN网站或重启了电脑,只要浏览器还保存着这个Cookie,CSDN服务器就能识别出我们的身份,从而无需再次输入账号和密码即可登录。

我们可以在浏览器的设置中查看到这些Cookie信息,通常会有一个锁形图标来表示安全相关的内容。点击这个图标,我们可以查看和管理网站存储在我们电脑上的Cookie数据。

这些cookie数据实际上是由相应的服务器创建和发送的。当你在浏览器端删除某些cookie后,可能会遇到需要重新进行登录认证的情况。这是因为,你可能刚刚删除的是与登录状态相关的cookie信息。

cookie的作用

由于HTTP协议的无状态特性,如果没有cookie机制,每次访问网站页面时都需要重新输入账号和密码进行身份验证,这无疑大大降低了用户体验。特别是对于像视频网站这样的平台,拥有大量VIP视频内容,每次点击新视频都需重新认证显然是不现实的。

为了解决这个问题,HTTP协议引入了cookie技术作为补充。当你首次登录某个网站并成功通过身份验证后,服务器会发送一个包含Set-Cookie指令的HTTP响应。这个指令告诉浏览器将一段特定的信息(通常是加密的身份验证令牌)保存起来,这就是cookie。

浏览器会自动执行这个Set-Cookie指令,将cookie信息保存在本地的一个专门区域。此后,当你再次访问该网站时,浏览器会自动在每次请求中附带这个cookie信息。服务器通过验证这个cookie来确定你的身份,从而避免了重复的身份验证过程。

在成功完成首次登录认证后,浏览器会自动向该网站后续发出的每个HTTP请求中添加一个特殊的cookie字段。这个字段中包含了初次认证时的相关信息,通常是经过加密的身份验证令牌。这样,每当浏览器与服务器进行交互时,服务器就能够从HTTP请求中读取到这个cookie字段,并据此验证用户的身份。不需要重新让你输入账号和密码了。

也就是在第一次认证登录后,后续所有的认证都变成了自动认证,这就叫做cookie技术。

内存级Cookie&文件级Cookie

Cookie是存储在浏览器中的一个小型数据文件,其中包含了用户的个人信息和登录状态。根据存储方式的不同,Cookie可以分为内存级和文件级两种。

  • 内存级Cookie是在浏览器运行期间临时存储在内存中的。这种Cookie在浏览器关闭后会自动清除。因此,如果你关闭了浏览器后重新打开,并尝试访问之前登录过的网站,系统要求你重新输入账号和密码,那么很可能是因为你之前的登录状态信息存储在了内存级Cookie中。
  • 而文件级Cookie则会被持久化保存在浏览器的硬盘上,即使关闭浏览器或重启电脑也不会丢失。这意味着,如果你在关闭浏览器或重启电脑后,仍然能够访问之前登录过的网站而无需重新输入账号和密码,那么你的登录状态很可能是通过文件级Cookie来维持的。

cookie被盗

如果非法用户能够获取到你浏览器中保存的cookie信息,他们就可以利用这些信息进行伪装,以你的身份访问你曾经登录过的网站。这种情况被称为cookie被盗取。

举个例子,如果你不慎点击了一个可疑链接,这个链接可能实际上是一个伪装成普通链接的恶意程序。一旦你点击,这个程序就会下载到你的电脑上并自动执行。这个恶意程序会扫描你的浏览器cookie存储位置,收集所有cookie信息,并通过网络将这些信息发送给攻击者。

一旦攻击者获取了你的cookie数据,他们就可以将这些信息复制到他们自己的浏览器cookie目录中。这样,攻击者就能够伪装成你,以你的身份访问你曾经登录过的网站,获取你的个人信息,甚至进行恶意操作。因此,保护好自己的cookie信息非常重要,要避免点击不明链接,定期清理和更新cookie,以确保个人信息安全。

SessionID

单纯依赖cookie存在安全隐患,因为cookie文件中存储的私密信息一旦泄露,用户的隐私将面临风险。为了解决这一问题,现代服务器引入了SessionID的概念来增强安全性。

在用户首次成功登录某个网站并经过服务器认证后,服务器会为用户生成一个唯一的SessionID。这个SessionID与用户的具体信息无关,但能够唯一标识用户的会话状态。服务器会维护一个所有登录用户SessionID的列表。

随后,服务器在HTTP响应中将这个SessionID发送给浏览器。浏览器会自动提取这个SessionID并将其保存在cookie文件中。此后,当用户继续访问该网站时,浏览器会在每个HTTP请求中自动附带这个SessionID。

而服务器识别到HTTP请求当中包含了SessionID,就会提取出这个SessionID,如果服务器能够在集合中找到与请求中携带的SessionID相匹配的项,这意味着该用户之前已经成功登录过,并且其会话状态仍然有效。在这种情况下,服务器会自动认为用户的身份已经通过验证,无需再次进行繁琐的登录流程。

安全是相对的

尽管引入了SessionID,浏览器中的cookie文件若被盗取,用户的SessionID仍面临泄露风险。这意味着非法用户可以利用这个SessionID来访问用户曾访问过的服务器,这与之前的问题类似。

早期的做法是在浏览器中存储账号和密码的副本,并在每次请求时都发送这些信息,但这种做法存在安全风险,因为明文传输的账号和密码容易被截获。

相比之下,现代的做法是仅在首次认证时传输账号和密码,之后则通过发送SessionID来验证用户身份。虽然这种方法没有彻底消除安全风险,但它显著提高了系统的安全性。

在互联网领域,绝对的安全是不存在的,所有的安全措施都是相对的。即使对数据进行加密,也不能完全保证不被破解。然而,安全领域有一条原则:如果破解某个信息的成本远超过破解后可能获得的收益,那么可以认为该信息是相对安全的。这意味着在设计安全系统时,我们需要权衡成本和收益,以达到一个合理的安全水平。

引入SessionID的帮助

  • 在引入SessionID之前,用户的登录信息由浏览器客户端负责管理。用户账号等敏感数据直接存储在浏览器内部。然而,这种方式存在安全风险,因为一旦浏览器被攻击或用户设备被盗,账号信息就可能泄露。
  • 引入SessionID后,情况发生了改变。用户的登录信息不再由浏览器管理,而是由服务器负责维护。浏览器仅保存一个与服务器会话相关联的SessionID。即便这个SessionID被盗取,攻击者也只能模拟用户的会话,而不能直接获取到用户的账号信息。

为了增强账号安全性,服务器会采用多种策略。

  • 例如,通过分析用户的IP地址,服务器能够识别出账号登录地址的异常情况。一旦发现短时间内账号登录地点发生了巨大变化,服务器会立即中断该SessionID,要求用户重新进行身份验证。这样,即使攻击者盗取了SessionID,也无法持续利用它进行非法操作。
  • 此外,对于高权限操作,服务器会要求用户再次输入账号和密码进行二次验证。这种机制确保了即使账号被盗,攻击者也无法轻易修改密码,因为通常需要旧密码作为验证的一部分。这为被盗账号的用户提供了挽回的机会,他们可以通过重置密码使当前的SessionID失效,从而迫使非法用户重新登录。
  • 同时,为了限制SessionID的有效期,服务器通常会设置过期策略。例如,一个SessionID可能仅在一小时内有效。这大大减少了攻击者利用被盗SessionID进行非法活动的时间和范围。

虽然非法用户的存在对网络安全构成了威胁,但也推动了服务器安全技术的不断发展和完善。正是这种对抗,促使双方不断进步,提高了整个互联网环境的安全性。

下面我们通过一个实验来演示

当浏览器访问我们的服务器时,如果服务器给浏览器的HTTP响应当中包含Set-Cookie字段,那么当浏览器再次访问服务器时就会携带上这个cookie信息。

因此我们可以在服务器的响应报头当中添加上一个Set-Cookie字段,看看浏览器第二次发起HTTP请求时是否会带上这个Set-Cookie字段。

#pragma once

#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#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 = 8888;

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);
            req_header.push_back(temp);//将读取到req的每一行放入req_header
            req.erase(0,pos + sep.size());//将已经读取到的当前行删除
        }
        text = req;//空行之前的内容都存放在req_header中了,这时候的req就是正文部分了,我们把他放在text
    }   

    void Parse()
    {
        std::stringstream ss(req_header[0]);//stringstream可以按空格分开字符串,我们用stringstream解析请求行
        ss >> method >> url >> http_version;
        file_path = wwwroot; // ./wwwroot
        //根据用户申请访问的url,动态更改文件的路径
        if(url == "/" || url == "/index.html"){//访问的是网页根目录或者首页
            file_path += "/";
            file_path += homepage; // ./wwwroot/index.html
        }
        else//访问的是其它页面
            file_path += url;// /a/b/c/d.html->./wwwroot/a/b/c/d.html

        //获取文件后缀
        auto pos = file_path.rfind(".");
        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";
        }

        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
        std::cout << "file_path: " << 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;
    std::string file_path; // ./wwwroot/a/b/c.html 2.png

    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::iostream::binary);
        if(!in.is_open())   return "404";

        // seekg是一个成员函数,用于设置输入流的位置
        in.seekg(0,std::ios_base::end);//将文件流的位置指示器移动到文件的最后一个字节之后。0表示偏移量,而std::ios_base::end是一个常量,表示从文件的末尾开始计算偏移量。
        auto len = in.tellg();//获取当前文件流的位置,并将其存储在变量len中。
        in.seekg(0,std::ios_base::beg);//将文件流的位置指示器重新设置到文件的开头。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 << std::endl; // 假设我们读取到的就是一个完整的,独立的http 请求
            HttpRequest req;
            req.Deserialize(buffer);
            req.Parse();
            req.DebugPrint();

            // 返回响应的过程
            //响应正文
            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_line = "HTTP/1.0 307 OK\r\n";
            //响应报头
            std::string response_header = "Content-Length: "; 
            response_header += std::to_string(text.size()); // Content-Length: 11
            response_header += "\r\n";
            response_header += SuffixToDesc(req.suffix);//文件类型
            response_header += "\r\n";
            response_header += "Set-Cookie: gtycsy";
            response_header += "\r\n";

            // response_header += "Location: https://www.qq.com\r\n";
            //空行
            std::string blank_line = "\r\n"; // \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;//文件的类型映射表,key是文件的后缀
};

启动服务器后,当你使用浏览器访问该服务器时,通过Fiddler这样的网络调试工具,你可以观察到服务器发送给浏览器的HTTP响应头中包含了一个名为“Set-Cookie”的字段。这个字段的作用是向浏览器设置或更新一个cookie。浏览器在接收到这个响应后,会根据“Set-Cookie”字段的值来存储或更新相应的cookie信息。

同时我们也可以在浏览器当中看到这个cookie,这个cookie的值就是我们设置的,此时浏览器当中就写入了这样的一个cookie。 

在输入用户名和密码并提交表单后,我们实际上是对服务器进行了第二次访问。通过Fiddler观察,由于我们采用POST方法提交表单参数,这些参数是以请求正文的形式发送给服务器的。值得注意的是,第二次的HTTP请求中包含了之前服务器设置的cookie信息,这些信息会被自动附加在请求头中发送给服务器,以便服务器能够识别并验证用户的身份。 

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值