Bootstrap

应用层 —— HTTP协议

目录

1、HTTP协议

        HTTP简介

        认识URL 

        urlencode和urldecode

2、HTTP协议格式

        HTTP协议的请求格式

        HTTP响应协议格式

        telnet命令远程登陆服务

3、HTTP的方法

        表单

        GET方法

        POST方法

        GET vs POST 

4、HTTP的状态码

        重定向状态码

5、HTTP常见Header

        cookie + session

        Connection

6、源码gitee链接


1、HTTP协议

HTTP简介

HTTP(Hyper Text Transfer Protocol)协议又叫做超文本传输协议,是一个简单的请求-响应协议,HTTP通常运行在TCP之上。

  • 在编写网络通信代码时,我们可以自己进行协议的定制,但实际有很多优秀的工程师早就已经写出了许多非常成熟的应用层协议,其中最典型的就是HTTP协议。

认识URL 

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

协议方案名:

  • http:// 表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS。HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。

登陆信息:(可以省略)

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

服务器地址:

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

  • 如果用户看到的是这两个IP地址,那么用户在访问这个网站之前并不知道这两个网站到底是干什么的,但如果用户看到的是www.baidu.com和www.qq.com这两个域名,那么用户至少知道这两个网站分别对应的是哪家公司,因此域名具有更好的自描述性。
  • 实际我们可以认为域名和IP地址是等价的,在计算机当中使用的时候既可以使用域名,也可以使用IP地址。但URL呈现出来是可以让用户看到的,因此URL当中是以域名的形式表示服务器地址的。

端口号:(可以省略)

  • 使用确定协议的时候,一般显示的时候,会缺省端口号,但是当浏览器访问指定url的时候,浏览器或app必须给我们自动添加port。

特定的众所周知服务,端口号必须是确定的!常见协议对应的端口号如下:

协议名称对应端口号
HTTP80
HTTPS443
SSH22
  • 注意:用户自己写的网络服务bind端口的范围一定是1024之后的:[1024, n];因为前1023个是给这些httpserver服务的。
  • 它们特定的服务与特定的端口之间的关系就比如:110与警察;120与救护车;119与火警

带层次的文件路径

  • /dir/index.htm表示的是要访问的资源所在的路径。访问服务器的目的是获取服务器上的某种资源,通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径。
  • 比如我们打开浏览器输入百度的域名后,此时浏览器就帮我们获取到了百度的首页。

  • 当我们发起网页请求时,本质是获得了这样的一张网页信息,然后浏览器对这张网页信息进行解释,最后就呈现出了对应的网页。

  • 我们可以将这种资源称为网页资源,此外我们还会向服务器请求视频、音频、网页、图片等资源。HTTP之所以叫做超文本传输协议,而不叫做文本传输协议,就是因为有很多资源实际并不是普通的文本资源。
  • 因此在URL当中就有这样一个字段,用于表示要访问的资源所在的路径。此外我们可以看到,这里的路径分隔符是/,而不是\,这也就证明了实际很多服务都是部署在Linux上的。 

查询字符串

  • uid=1表示的是请求时提供的额外的参数,这些参数是以键值对的形式,通过&符号分隔开的。
  • 比如我们在百度上面搜索qq,此时可以看到URL中有很多参数,而在这众多的参数当中有一个参数wd(word),表示的就是我们搜索时的搜索关键字wd=qq。

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

片段标识符

  • ch1表示的是片段标识符,是对资源的部分补充。

问1:http协议是做什么的?

  • 用于查阅文档,看音视频,这些都是以网页的形式呈现的。网页实际就是一个 .htmI文件
  •  http用途:获取网页资源的,视频,音频等也都是文件!
  • 解释:http是向特定的服务器申请特定的”资源”的,把资源获取到本地(本地可以是浏览器/app/迅雷播放器)进行展示或者某种使用的!

问2:如果我们client没有获取的时候,资源在哪里呢?

  • 就在你的网络服务器(软件)所在的服务器(硬件,计算机)上

问3:服务器用的都是什么系统呢?

  • 服务器都是Linux系统的,这些资源都是文件,即资源文件在Linux服务器上。要打开资源文件,读取和发送会给客户端——前提:软件服务器,必须先找到这个文件! !通过路径查找。

/ 就是Linux下的路径分隔符,示例如下:

https://www.mydown.com/soft/432/479673432.shtml?f=bdj_646119


urlencode和urldecode

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

转义的规则如下 :

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

示例:

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

当我们搜索的是CPP时,CPP没有特殊符号,因此也就不会进行编码:

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

如下是一个在线编码解码工具:

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

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

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


2、HTTP协议格式

  • HTTP是基于请求和响应的应用层服务,作为客户端,你可以向服务器发起request,服务器收到这个request后,会对这个request做数据分析,得出你想要访问什么资源,然后服务器再构建response,完成这一次HTTP的请求。这种基于request&response这样的工作方式,我们称之为cs或bs模式,其中c表示client,s表示server,b表示browser。
  • 由于HTTP是基于请求和响应的应用层访问,因此我们必须要知道HTTP对应的请求格式和响应格式,这就是学习HTTP的重点。

HTTP协议的请求格式

HTTP请求由如下四部分构成:(每行以\r\n结尾)

  • 请求行:[请求方法] + [url(一般省略了域名和端口,只有路径)] + [http版本]。ps:http协议请求时大小写是忽略的,例如请求行的 GET / HTTP/1.1 和get / http/1.1都一样
  • 请求报头:请求的属性,这些属性都是以 key: value的形式按行陈列的(: 和value中间有空格)
  • 空行:因为只包含了一个 \r\n,用于做分隔符,把报头和有效载荷分离
  • 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度

其中,前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。

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

  • 当应用层收到一个HTTP请求时,它必须想办法将HTTP的报头与有效载荷进行分离。对于HTTP请求来讲,这里的请求行和请求报头就是HTTP的报头信息,而这里的请求正文实际就是HTTP的有效载荷。
  • 如果将HTTP请求想象成一个大的线性结构,此时每行的内容都是用 \r\n 隔开的,因此在读取过程中,如果连续读取到了两个 \r\n ,就说明已经将报头读取完毕了,后面剩下的就是有效载荷了。

如下我们编写一个TCP服务器,此服务器的注意认为就是把浏览器发来的HTTP请求进行打印即可:

#include <iostream>
#include <cstdio>
#include <cassert>
#include <cstring>
#include <string>
#include <signal.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

void handlerHttpRequest(int sock)
{
    cout << "++++++++++++++++++++++++++++++++++++++++++++++++" << endl;
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof(buffer));
    if (s > 0)
        cout << buffer;
}
class ServerTcp
{
public:
    ServerTcp(uint16_t port, const string &ip = "")
        : port_(port), ip_(ip), listensock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listensock_ >= 0)
            close(listensock_);
    }

public:
    // 初始化
    void init()
    {
        // 1、创建socket
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            exit(1);
        }
        // 2、bind绑定
        // 2.1、填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof(local));
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2、将本地socket信息,写入listensock_对应的内核区域
        if (bind(listensock_, (const struct sockaddr *)&local, sizeof(local)) == -1)
        {
            exit(2);
        }
        // 3、监听socket
        if (listen(listensock_, 5) < 0)
        {
            exit(3);
        }
        // 允许别人连接你了
    }
    // 启动服务端
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号
        while (!quit_)
        {
            // 4、获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int serviceSock = accept(listensock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1.1版本 —— 多进程 ———— 让孙子进程提供服务
            // 爷爷进程
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                // 爸爸进程
                close(listensock_); // 建议关掉
                if (fork() > 0)     // 又进行了一次fork,让爸爸进程直接终止
                    exit(0);
                // 孙子进程 ———— 没有爸爸 ———— 孤儿进程 ———— 被系统领养 ———— 回收问题就交给了系统来回收
                handlerHttpRequest(serviceSock);
                exit(0);
            }
            close(serviceSock); // 一定要做
            // 爸爸进程直接终止,立马得到退出码,释放僵尸状态
            wait(nullptr);
        }
    }
    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    int listensock_; // 监听套接字socket
    uint16_t port_;  // port
    string ip_;      // ip
    // 安全退出
    bool quit_;
};

static void Usage(string proc)
{
    cerr << "Usage:\n\t" << proc << "port" << endl;
    cerr << "Example:\n\t" << proc << "8080\n"
         << endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);
    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

编译后,运行我们的服务器,用浏览器(ip + 端口)访问,此时我们的服务器就会收到浏览器发来的HTTP请求,并将收到的HTTP请求进行打印输出:

当我手机浏览器对此服务器访问的结果如下:(对比电脑)

注意:

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

HTTP响应协议格式

HTTP响应由以下四部分组成:(也是每行以 \r\n 结尾)

  • 状态行:[http版本 (http/1.1) ]+[状态码 (例如404报错,200代表OK) ]+[状态码描述 (例如404对应的"Not Found"描述) ] 
  • 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。(注意:和value中间有空格)比如 Content-Type: text/html; charset=utf-8 用于表示正文是 text/html文档类型,字符集为utf-8
  • 空行:因为只包含了一个 \r\n ,用与做分隔符,把报头和有效载荷分离
  • 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。


telnet命令远程登陆服务

我们可以使用telnet命令获取百度给我的响应:

结果如下:

现在重新编写我们的服务器,让服务器读取到客户端发来的HTTP请求后,对次HTTP请求进行各种数据分析,然后构建成对应的HTTP响应发回给客户端。这里的响应我们简要处理,只是模拟个大概,内容如下:

  • 定义string类型的字符串response
  • 添加响应行为"HTTP/1.0 200 OK\r\n"
  • 添加响应报头,这里用"\r\n"代替
  • 添加正文,这里以"hello world"代替
  • 利用send函数将response字符串的内容发送到套接字中

在这之前先认识下send接口,send函数的函数原型如下:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要发送的数据。
  • len:需要发送数据的字节个数。
  • flags:发送的方式,一般设置为0,表示阻塞式发送。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

我们把上述整个过程封装在前面服务器的handlerHttpRequest函数中:

void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof(buffer));
    if (s > 0)
        cout << buffer;

    // 开始响应
    string response;
    response = "HTTP/1.0 200 OK\r\n";
    response += "Content-Type: text/html\r\n";
    response += "\r\n";        // 响应报头
    response += "<html><h1>hello world</h1></html>\n"; // 正文
    send(sock, response.c_str(), response.size(), 0);
}

解释上文各数据含义:

  • "HTTP/1.0 200 OK\r\n":HTTP/1.0 版本,200状态码表示通过,OK状态码描述
  • "Content-Type: text/html\r\n":Content-Type内容类型,正文的类型是html文本类型。(text-文本类型)
  • "\r\n":这是空行
  • "<html><h1>hello world</h1></html>\r\n":正文内容是hello world,<html>……</html>是html网页的格式,<h1>……</h1>是使正文成为大标题

编写好后,运行服务器,利用telnet命令(远程以协议方式登录某种服务)

telnet 127.0.0.1 8080

登陆该服务器后,按下ctrl + ] 键,进入telnet命令行,按下回车键,发送http/1.0。此时就能看到响应了。不过我们这里实现的响应比较简单:

现在打开浏览器,让其链接我们的服务器:(此时我们就能在浏览器上看到响应正文(hello world)这样的字段了)

注意:任何协议的request or response,都有包含报头 + 有效载荷。

http如何保证自己的报头和有效载荷被全部读取呢?无论是请求还是响应

  • 读取完整报头:按行读取,直到读取到空行上

你又如何保证 你能读取到完整的正文呢?

  • 报头能读取完毕,请求或者响应属性中”一定”要包含正文的长度!

我们需要加上正文的长度,代码如下:

效果如下:

注意:作为一款服务器,我关心的只是网络请求并推送过去,网页里的内容不需要我操心。所以上述html的内容我们应该从文件中拿上来。衍生如下两个注意事项。

  • 请求的请求行中,第二个字段就是你要访问的文件。如:请求行:GET /a/b/c.html http/1.0 ,/a/b/c.html就是要访问的文件
  • GET /a/b/c.html http/1.0 中的 / 是web根目录,不是根目录。web根目录可以自己指定,所以可以设置为根目录。

现在修改代码,创建wwwroot文件,以此作为我们的web根目录,我们可以在该目录下创建一个html文件,然后编写一个简单的html作为当前服务器的首页。注意:

  • path = "/a/b/index.html":请求的人请求的文件路径
  • resource = "./wwwroot":我们的web根目录,我们服务器内部给请求的路径自动加上前缀
  • resource += path:./wwwroot/a/b/index.html
// 提取路径
string getPath(string http_request)
{
    size_t pos = http_request.find(CRLF);
    if (pos == string::npos)
        return "";
    string request_line = http_request.substr(0, pos); // 请求行
    // GET /a/b/c http/1.1
    size_t first = request_line.find(SPACE); // 从头开始找
    if (pos == string::npos)
        return "";
    size_t second = request_line.rfind(SPACE); // 从尾开始找
    if (second == string::npos)
        return "";
    // 提取path路径,即两个空格之间的数据
    string path = request_line.substr(first + SPACE_LEN, second - (first + SPACE_LEN));
    if (path.size() == 1 && path[0] == '/') // 若访问的是根目录,则返回首页
        path += HOME_PAGE;
    return path;
}

string readFile(const string &recource)
{
    ifstream in(recource);
    if (!in.is_open())
        return "404";
    string line;
    string content;
    while (getline(in, line))
    {
        content += line;
    }
    in.close();
    return content;
}

void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof(buffer));
    if (s > 0)
        cout << buffer;
    string path = getPath(buffer);
    cout << path << endl;
    // path = "/a/b/index.html":请求的人请求的文件路径
    // resource = "./wwwroot":我们的web根目录,我们服务器内部给请求的路径自动加上前缀
    // resource += path:./wwwroot/a/b/index.html
    string recource = ROOT_PATH;
    recource += path;
    cout << recource <<endl;
    string html = readFile(recource);
    // 开始响应
    string response;
    response = "HTTP/1.0 200 OK\r\n";
    response += "Content-Type: text/html\r\n";
    response += ("Content-Length: " + to_string(html.size()) + "\r\n");
    response += "\r\n"; // 响应报头
    response += html;   // 正文
    send(sock, response.c_str(), response.size(), 0);
}

html文件代码如下:(网页)

后续服务器运行起来,任何人想访问该网站,先输入我的域名、ip、端口号,默认发过来 / ,此时会在服务器内部提取你的路径,再进行路径拼接,最后在wwwroot目录下找到网页的内容,并返回。


3、HTTP的方法

方法说明支持的HTTP协议版本
GET获取资源1.0、1.1
POST传输实体文件1.0、1.1
PUT传输文件1.0、1.1
HEAD获得报文首部1.0、1.1
DELETE删除文件1.0、1.1
OPTIONS询问支持的方法1.1
TRACE追踪路径1.1
CONNECT要求用隧道协议连接代理1.1
LINK建立和资源之间的联系1.0
UNLINE断开连接关系1.0
  • 其中最常用的就是GET方法和POST方法
  • HEAD请求是没有响应体的,仅传输状态行和标题部分
  • DELETE方法用来删除指定的资源,它会删除URI给出的目标资源的所有当前内容
  • PUT方法用于将数据发送到服务器以创建或更新资源,它可以用上传的内容替换目标资源中的所有当前内容

表单

我们的网络行为无非有两种:

  • 我们想把远端的资源拿到你的本地: GET /index.html http/1.1
  • 我们想把我们的属性字段,提交到远端

提交到远端有两种方法(GET | POST)


GET方法

如下是我们的表单的代码,这里我贴了一份现成的(前端代码,这里只是为了验证现象):

    <form action="/a/b/c.html" method="get">    
——action文件路径; method打开方法(忽略大小写)
        Username: <input type="text" name="user"><br>
——input:渲染成输入框。类型是"text"文本类型。字段名称是"user"
        Password: <input type="password" name="passwd"><br>
——类型是"password"密码类型。字段名称是"passwd"
        <input type="submit" value="Submit">
——按钮类型是"submit",按钮名称是"Submit"
    </form>

html文件总代码如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>3月27号 测试</title>
</head>

<body>
    <h3>hello my server!</h3>
    <p>代码测试完毕</p>
    <form action="/a/b/c.html" method="get">
        Username: <input type="text" name="user"><br>
        Password: <input type="password" name="passwd"><br>
        <input type="submit" value="Submit">
    </form>
</body>

</html>

运行后效果如下:

我输入用户名张三,随机输入一串密码,点击确定,来看如下的一副对照图:

如上在HTTP中GET会以明文方式将我们对应的参数信息,拼接到url中。


POST方法

POST方法提交参数,会将参数以明文的方式,拼接到http的正文中来进行提交。只需把method="get"  改成method="post":

测试如下: 

输入用户名密码后确认,再来看如下的对照图:


GET vs POST 

  1. GET通过url传参
  2. POST通过正文传参
  3. GET方法传参不私密(因为GET会把用户输入的有效信息用户名,密码等回显到浏览器)
  4. POST方法因为通过正文传参,所以,相对比较私密一些(因为一些小白一般不会抓包看正文,所以相对私密)
  5. GET通过url传参,POST通过正文传参,所以一般一些比较大的内容都是通过post方式传参的
  6. HTTP GET请求提交参数有长度限制;HTTP POST请求提交参数没有长度限制。

解释6:Http Get方法提交的数据大小长度并没有限制,HTTP协议规范没有对URL长度进行限制。但是特定的浏览器及服务器对URL有限制,所以还是有限制的; 因为POST方法通过正文传参,理论上讲,POST是没有大小限制的。HTTP协议规范也没有进行大小限制,起限制作用的是服务器的处理程序的处理能力,而并非限制。


4、HTTP的状态码

类型原因短语
1XXInformational(信息性状态码)接受的请求正在处理
2XXSuccess(成功状态码)请求正常处理完毕
3XXRedirection(重定向状态码)需要进行附加操作以完成请求
4XXClient Error(客户端错误状态码)服务器无法处理请求
5XXServer Error(服务器错误状态码)服务器处理请求出错

最常见的状态码,比如200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)。

  • 4XX:客户端错误——>客户请求了不存在的资源,即客户提出了无理的要求,是客户的错。
  • 5XX:服务器错误——>服务器代码中的内容错误,例如fork错误,就会返回5XX

重定向状态码

重定向就是通过各种方法将各种网络请求重新定个方向转到其它位置,此时这个服务器相当于提供了一个引路的服务。且返回的响应response报头里会携带http的响应属性Location:new url。此时就会重定向到新的url网址。

  • 重定向又可分为临时重定向永久重定向,其中状态码301表示的就是永久重定向,而状态码302307表示的是临时重定向

临时重定向:

  • 进行临时重定向时需要用到Location字段,Location字段是HTTP报头当中的一个属性信息,该字段表明了你所要重定向到的目标网站。
  • 我们这里要演示临时重定向,可以将HTTP响应当中的状态码改为302,然后跟上对应的状态码描述,此外,还需要在HTTP响应报头当中添加Location字段,这个Location后面跟的就是你需要重定向到的网页,比如我们这里将其设置为B站的首页。
void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof(buffer));
    if (s > 0)
        cout << buffer;
    string response = "HTTP/1.1 302 Temporarily Moved\r\n";
    response += "Location: https://www.bilibili.com/\r\n";
    response += "\r\n";
    send(sock, response.c_str(), response.size(), 0);
}

此时运行我们的服务器,当我们用telnet命令登录我们的服务器时,向服务器发起HTTP请求时,此时服务器给我们的响应就是状态码302,响应报头当中是Location字段对应的就是B站首页的网址:

现在用浏览器访问我们的服务器,当浏览器收到这个HTTP响应后,还会对这个HTTP响应进行分析,当浏览器识别到状态码是302后就会提取出Location后面的网址,然后继续自动对该网站继续发起请求,此时就完成了页面跳转这样的功能,这样就完成了重定向功能。

永久重定向:

只需要将HTTP响应当中的状态码改为301,然后跟上对应的状态码描述即可:

void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof(buffer));
    if (s > 0)
        cout << buffer;
    // string response = "HTTP/1.1 302 Temporarily Moved\r\n"; // 临时重定向
    string response = "HTTP/1.1 301 Permanently Moved\r\n"; // 永久重定向
    response += "Location: https://www.bilibili.com/\r\n";
    response += "\r\n";
    send(sock, response.c_str(), response.size(), 0);
}

运行结果和临时重定向一样:

临时重定向 VS 永久重定向:

  • 一个网站1如果临时不想被访问就用 302 暂时重定向 重定向到网站2;一个网站1如果永久不想被访问就用 301 永久重定向 重定向到网站2; 

5、HTTP常见Header

  • Content-Type: 数据类型(text/html等,比如 Content-Type: text/html; charset=utf-8 用于表示正文是 text/html文档类型,字符集为utf-8,不区分大小写 charset=UTF-8 也可以)
  • Content-Language:用于表示用户希望采用的语言或语言组合,比如 Content-Language: de-DE 表示该文件为说德语的人提供,但是要注意者不代表文件内容就是德语的。这里理解 Content-Type 和 Content-Language 区别: Content-Language更多表示上层语言的表示, 而Content-Type用于底层数据编码的表示
  • Content-Length: Body的长度
  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent: 声明用户的操作系统和浏览器版本信息;
  • referer: 当前页面是从哪个页面跳转过来的;
  • Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
  • Set-Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

cookie:

HTTP实际上是一种无状态协议,HTTP的每次请求/响应之间是没有任何关系的,但你在使用浏览器的时候发现并不是这样的。

  • 比如当你登录一次B站后,就算你把B站关了甚至是重启电脑,当你再次打开B站时,B站并没有要求你再次输入账号和密码,这实际上是通过cookie技术实现的,点击浏览器当中锁的标志就可以看到对应网站的各种cookie数据。

  • 这些cookie数据实际都是对应的服务器方写的,如果你将对应的某些cookie删除,那么此时可能就需要你重新进行登录认证了,因为你删除的可能正好就是你登录时所设置的cookie信息。
  • cookie是一种保存在客户端的小型文本文件,用于保存服务器通过Set-Cookie字段返回的数据,在下次请求服务器时通过Cookie字段将内容发送给服务器。是HTTP进行客户端状态维护的一种方式。而Set-Cookie以及Cookie字段可以包含有多条信息,也可以由多个Cookie及-Set-Cookie字段进行传输多条信息,并且cookie有生命周期,在超过生命周期后cookie将失效,对应的cookie文件将被删除。

cookie的失效时间:

  • Cookie的Expires属性指定了cookie的生存期,默认情况下coolie是暂时存在的,他们存储的值只在浏览器会话期间存在,当用户退出浏览器后这些值也会丢失,如果想让cookie存在一段时间,就要为expires属性设置为未来的一个过期日期。现在已经被max-age属性所取代,max-age用秒来设置cookie的生存期。当没有设定过期时间时,则退出当前会话时cookie失效

如果没有为Cookie指定失效时间,则设置的Cookie将在何时失效?

  • 如果不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。
  • 如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。

内存级别 vs 文件级别:

cookie就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。cookie文件可以分为两种,一种是内存级别的cookie文件,另一种是文件级别的cookie文件。

  • 将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的。
  • 将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的。

cookie的登陆策略:

因为HTTP是一种无状态协议,如果没有cookie的存在,那么每当我们要进行页面请求时都需要重新输入账号和密码进行认证,这样太麻烦了。

  • 就比如你是爱奇艺的VIP会员,你每次点击一个VIP视频都要重新进行VIP身份认证。而HTTP不支持记录用户状态,那么我们就需要有一种独立技术来帮我们支持,这种技术目前现在已经内置到HTTP协议当中了,叫做cookie。

cookie的登陆策略如下: 

  • 当第一次登陆某网站时,客户端向服务器输入我们的用户名和密码
  • 服务器把 cookie用户名&&密码返回给客户端(在HTTP请求中的Cookie是明文传递的)
  • 客户端下次登录 自动携带浏览器访问该网站对应的cookie文件中的内容,这样就能保持登录

总的来说就是第一次登录时,服务器就会进行Set-Cookie的设置(Set-Cookie也是HTTP报头当中的一种属性信息)。当认证通过并在服务端进行Set-Cookie设置后,服务器在对浏览器进行HTTP响应时就会将这个Set-Cookie响应给浏览器。而浏览器收到响应后会自动提取出Set-Cookie的值,将其保存在浏览器的cookie文件当中,此时就相当于我的账号和密码信息保存在本地浏览器的cookie文件当中。

下面重新编写我们的服务器,使其支持cookie:

  • 当浏览器访问我们的服务器时,如果服务器给浏览器的HTTP响应当中包含Set-Cookie字段,那么当浏览器再次访问服务器时就会携带上这个cookie信息。
  • 因此我们可以在服务器的响应报头当中添加上一个Set-Cookie字段,看看浏览器第二次发起HTTP请求时是否会带上这个Set-Cookie字段。
void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof(buffer));
    if (s > 0)
        cout << buffer;
    string path = getPath(buffer);
    cout << path << endl;
    string recource = ROOT_PATH;
    recource += path;
    cout << recource <<endl;
    string html = readFile(recource);
    // 开始响应
    string response;
    response = "HTTP/1.0 200 OK\r\n";
    response += "Content-Type: text/html\r\n";
    response += ("Content-Length: " + to_string(html.size()) + "\r\n");
    response += "Set-Cookie: this is my cookie content;\r\n";
    response += "\r\n"; // 响应报头
    response += html;   // 正文
    send(sock, response.c_str(), response.size(), 0);
}

运行服务器,用浏览器访问我们的服务器,我们可以在浏览器当中看到这个cookie,这个cookie的值就是我们设置的this is my cookie content,此时浏览器当中就写入了这样的一个cookie。

然后我们输入用户名和密码并提交,此时相当于第二次访问我们的服务器,此时通过云服务器可以看到,由于我们提交表单参数用的是POST方法,因此这里的参数是通过正文的形式提交的,更重要的是第二次的HTTP请求当中会携带上这个cookie信息。

cookie的风险:

仅仅是cookie的登陆策略会存在安全隐患:

  • 当你不小心被迫下载了木马病毒,该病毒会扫描你的浏览器当中的cookie目录,它就会把所有的cookie信息通过网络的方式传给黑客,你的cookie用户名密码可能会被盗取,黑客会拿着你的cookie去登录,更严重的是黑客会修改你的cookie密码对账号产生威胁。
  • 因此单纯的使用cookie是非常不安全的,因为此时cookie文件当中就保存的是你的私密信息,一旦cookie文件泄漏你的隐私信息也就泄漏。

解决办法就是使用cookie + session的登陆策略。


cookie + session

session介绍:

  • session服务器为了保存用户状态而创建的临时会话,或者说一个特殊的对象,保存在服务器中,将会话ID通过cookie进行传输即可,就算会话ID被获取利用,但是session中的数据并不会被恶意程序获取,这一点相对cookie来说就安全了一些,但是session也存在一些缺陷,需要建立专门的session集群服务器,并且占据大量的存储空间(要保存每个客户端信息)

cookie + session的登陆策略:

  • 当前主流的服务器还引入了session_id这样的概念,当我们第一次登录某个网站输入账号和密码后,服务器认证成功后还会服务端生成一个对应的session文件(文件名具备唯一性),用户的临时私密信息,会保存在这个文件中。
  • 此时当认证通过后服务端在对浏览器进行HTTP响应时,仅仅会将生成的session_id值响应给浏览器。浏览器收到响应后会自动提取出session_id的值,将其保存在浏览器的cookie文件当中。后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个session_id。

  • 而服务器识别到HTTP请求当中包含了session_id,就会提取出这个session_id,然后再到对应的集合当中进行对比,对比成功就说明这个用户是曾经登录过的,此时也就自动就认证成功了,然后就会正常处理你发来的请求,这就是我们当前主流的工作方式。 

session是相对安全的:

引入session_id之后,浏览器当中的cookie文件保存的是session_id,此时这个cookie文件同样可能被盗取。此时用户的账号和密码虽然不会泄漏了,但用户对应的session_id是会泄漏的,非法用户仍然可以盗取我的session_id去访问我曾经访问过的服务器,相当于还是存在刚才的问题。

  • 之前的工作方式就相当于把账号和密码信息在浏览器当中再保存一份,每次请求时都自动将账号和密码的信息携带上,但是账号和密码一直在网当中发送太不安全了。
  • 因此现在的工作方式是,服务器只有在第一次认证的时候需要在网络中传输账号和密码,此后在网络上发送的都是session_id。

这种方法虽然没有真正解决安全问题,但这种方法是相对安全的。互联网上是不存在绝对安全这样的概念的,任何安全都是相对的,就算你将发送到网络当中的信息进行加密,也有可能被别人破解。


Connection

Connection: closed —— 短链接(http/1.0)

  • 短链接一次只能处理一条http请求
  • 用户所看到的完整的网页内容——背后可能是无数次http请求,每个图片就是一个文件,就需要一次请求
  • http底层主流采用的就是tcp协议,每处理一次请求就会进行一次 三次握手与四次挥手链接;一个网页有上百次http请求,就要进行上百次的 三次握手与四次挥手。则短链接不再适用。

Connection: keep-aliye —— 长链接(http/1.1)

  • 双方都同意采用长链接方案时,请求和响应中都携带了 Connection: keep-aliye ,客户端建立一个tcp链接,这一个tcp链接发送多次http请求,服务器接收后通过这个链接返回给客户端多次响应,当所有响应全部返回,此链接才断开。不用再向短链接那样重复建立链接了,大大提高了效率。

http协议无连接解释:

  • HTTP定义:超文本传输协议,是一个 无连接,无状态的应用层协议。
  • http协议底层是tcp,tcp是面向连接的,http只是使用了tcp的连接能力,但是http本身是无链接的。

6、源码gitee链接

本篇博文涉及到的代码gitee链接如下:

;