HTTP协议介绍

一、认识HTTP协议

HTTP协议是应用层协议,它叫超文本传输协议。应用层协议一般是我们自己自定义的,HTTP协议是计算机大佬们定义的一套现成的协议,因为它以及HTTPS使用的场景非常多,现在已经成为一套协议标准,我们可以学习这套标准直接拿来使用。

当我们在使用浏览器上网时,比如查阅文档、看视频听音乐等,这些都是以网页的形式呈现出来的,网页本质也是文件,它是以.html为后缀的文件,当我们用浏览器浏览网页时,浏览器作为客户端会向服务器发起HTTP请求,服务器会将网页文件响应回给客户端。所以HTTP协议就是向特定的服务器申请特定资源,把这些资源获取到本地进行展示或操作的。这里所说的资源指的是比如网页资源、视频资源、音频资源,其实它们的本质也是文件。

HTTP协议叫超文本传输协议其实是非常形象的,它可以传输文本比如网页上我们看到的一大段一大段的文字,也可以传输类似于图片、音频、视频等超文本的内容。

1.认识URL

URL(Uniform Resoure Locator)叫作统一资源定位符,它就是我们平时所俗称的网址,一般由下面这几部分组成:协议方案名、登录信息、服务器地址、服务器端口号、带层次的文件路径、查询字符串、片段标识符。

在这里插入图片描述

  1. 协议方案名:表示采用的是什么协议。
  2. 登录信息:表示用户的登录信息,这个字段一般可以不需要我们自己输入,现在的网页一般只需要我们输入账号密码,或者快捷登录,浏览器客户端拿到我们的登录信息之后自动帮我们做转换并填充URL中的这一个字段,所以这一字段一般是省略的,不需要我们显式地去写在URL上。
  3. 服务器地址:这个字段是必须有的,它表示的是服务器的IP地址,虽然我们平时浏览的网页看到的这一字段并不是IP地址的形式,但这是因为网页用域名代替了它的IP地址,域名方便记忆,浏览器客户端自动会帮我们将域名转换成对应的IP地址。
  4. 服务器端口号:服务器地址后冒号紧跟的是服务器端口号。使用网络服务必须有IP地址和端口号,因为网络通信的本质就是套接字通信。一般网页的URL我们是看不到端口号的,原因是使用确定协议的时候,端口号是缺省的,虽然URL上没有显示出来,但浏览器客户端在发起网络请求的时候会自动帮我们添加上缺省的端口号。特定的众所周知的服务,它的端口号必须是确定的比如HTTP服务的端口号是80,HTTPS服务的端口是443。
  5. 带层次的文件路径:表示的是服务器上要返回给客户端的资源的路径,比如服务器要返回网页文件给浏览器,这里就要填上该网页文件在服务器上对应的路径。
  6. 查询字符串和片段标识符:这两个都是在问号后面的,在URL中问号后面的代表的是一些参数,参数的类型非常多样。

urlencode和urldecode

在URL中像/?:等这样的字符是有特殊含义的,因此这些字符不能随意地出现,如果我们的参数中出现了这些字符,就必须对其进行转义。urlencode就可以对URL进行编码,将特殊含义的字符进行转义。urldecode可以对URL进行解码。转义的规则是:将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。

比如我们在百度浏览器搜索关键词C++和关键词CPP,复制出来它们的URL进行对比可以发现:

在它们的URL中都有一个wd字段,wd是word的简写,代表的就是我们搜索的关键词,当关键词是C++的时候,由于加号字符在URL中是有特殊含义的,所以它被转义了,当关键词是CPP的时候就没有被转义。

在这里插入图片描述

二、HTTP的协议格式

1.HTTP协议的请求格式

HTTP是基于行格式的一套协议,第一个部分也是第一行,叫作请求行,包括三个字段,每个字段以空格分开,分别是请求方法、URL和HTTP版本,请求方法常见的是GET方法和POST方法,URL这里一般是去掉域名和端口号的URL,最后请求行以\r\n结尾。

第二部分由很多行构成,叫作HTTP的请求报头,这里面放的都是请求报头的属性。这些属性都是key: value格式的,前面是key值,key值后面紧跟一个冒号,冒号后跟一个空格,空格后跟value值。

第三部分的内容只有\r\n,它其实是一个空行,这行其实起到的是类似于分隔符的作用,在别人解析HTTP请求的时候,只要读到了这个空行,就代表读取完了HTTP请求前半部分的请求行和报头数据。

第四部分是在空行以后,代表的是有效载荷。也叫作请求正文,这部分的内容一般比如包括用户的登录账号和密码,用户的个人信息,音频资源、视频资源等等。

在这里插入图片描述

我们可以用代码演示一下HTTP协议的请求报文,我们写一个TCP服务器,让浏览器作为客户端以HTTP协议请求服务端,服务端读取客户端发送过来的信息并把它打印出来即可。

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>

using namespace std;

class TcpServer;

struct ThreadData
{
    int _sock;
    TcpServer *_server;

    ThreadData(int sock, TcpServer *server)
        : _sock(sock), _server(server)
    {
    }
};

class TcpServer
{
public:
    TcpServer(uint16_t port, const string &ip = "")
        : _listen_socket(0), _port(port), _ip(ip)
    {
    }

    ~TcpServer()
    {
    }

public:
    void init()
    {
        // 1.创建套接字
        _listen_socket = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_socket < 0)
        {
            cerr << "socket error" << endl;
            exit(1);
        }
        cout << "socket success" << endl;

        // 2.bind
        // 2.1填充网络信息
        sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        // 2.2bind网络信息
        if (bind(_listen_socket, (const sockaddr *)&local, sizeof(local)) < 0)
        {
            cerr << "bind error" << endl;
            exit(2);
        }
        cout << "bind success" << endl;

        // 3.listen
        if (listen(_listen_socket, 5) < 0)
        {
            cerr << "listen error" << endl;
            exit(3);
        }
        cout << "listen success" << endl;
    }

    void start()
    {
        while (true)
        {
            // 1.accept
            sockaddr_in peer;
            memset(&peer, 0, sizeof(peer));
            socklen_t peer_len = sizeof(peer);
            int accept_socket = accept(_listen_socket, (sockaddr *)&peer, &peer_len);
            if (accept_socket < 0)
            {
                continue;
            }

            // 获取连接成功,开始提供服务
            // 创建多线程,主线程负责accept获取连接,新线程负责提供服务
            pthread_t tid;
            ThreadData *td = new ThreadData(accept_socket, this);
            pthread_create(&tid, nullptr, httpServer, (void *)td);
        }
    }

public:
    static void *httpServer(void *args)
    {
        ThreadData *td = (ThreadData *)args;
        while (true)
        {
            char buffer[10240];
            ssize_t readRes = read(td->_sock, buffer, sizeof(buffer) - 1);
            if (readRes > 0)
            {
                buffer[readRes] = '\0';
                // 将请求打印出来
                cout << buffer << endl;
            }
        }
    }

private:
    int _listen_socket;
    uint16_t _port;
    string _ip;
};

// ./server port ip
int main(int argc, char *argv[])
{
    // 命令行参数的格式输入错误
    if (argc != 2 && argc != 3)
    {
        cerr << "argc error,usage:./server port ip";
        exit(4);
    }
    uint16_t server_port = (uint16_t)atoi(argv[1]);
    string server_ip = "";
    if (argc == 3)
    {
        server_ip = argv[2];
    }

    TcpServer svr(server_port, server_ip);
    svr.init();
    svr.start();

    return 0;
}

运行代码启动服务器以后,我们让浏览器以HTTP协议请求我们的服务器,就可以看到它发送过来的请求报文:

我们可以看到它是以Get方式发起请求的,HTTP版本是HTTP1.1,Host指的是服务器主机的地址和端口号,Connection指的是请求是所采用的连接方式,
在这里插入图片描述

2.HTTP协议的响应格式

HTTP的响应格式也是以行为单位的,并且相应格式也是包含四个部分:

第一部分也是第一行,叫作状态码字段,它用来描述这一次HTTP响应的状态是什么,这一行的具体内容首先是HTTP协议的版本,然后紧跟一个空格,空格后面是状态码,状态码后面紧跟一个空格,空格后面是状态码描述,描述该状态码代表什么含义。这里的HTTP协议的版本和请求格式中的HTTP协议版本不一定一样,举个例子,如果客户端是老的客户端,但我们服务器更新了内容,我们需要保证的是如果客户端没有更新,依然能正常使用但是不能看到新的内容,只有更新了客户端才能看到新的内容。这种操作其实就是在版本号这里做判断,如果客户端发来的请求中,版本号是老的版本号,我们就提供对应老版本号的服务给它,如果版本号是新的,我们就提供新版本好的服务给它。

第二部分也是由很多行构成,叫作HTTP的响应报头,这里和请求格式中的请求报头类似,放的都是响应报头的属性,这些属性都是key: value格式的,前面是key值,key值后面紧跟一个冒号,冒号后跟一个空格,空格后跟value值。

第三部分与HTTP协议的请求格式一样,也是空行,起到分隔符的作用。

第四部分也是有效载荷,也叫作响应正文,这里的响应正文对应的是URL中请求的资源路径下的资源,比如html网页文件,图片文件,音视频文件等。

在这里插入图片描述

我们可以再在上面的代码基础上增加响应代码,首先我们不弄特别负责的响应,做一个简单的响应报文演示一下即可:

static void *httpServer(void *args)
{
    ThreadData *td = (ThreadData *)args;
    while (true)
    {
        char buffer[10240];
        ssize_t readRes = read(td->_sock, buffer, sizeof(buffer) - 1);
        if (readRes > 0)
        {
            buffer[readRes] = '\0';
            // 将请求打印出来
            cout << buffer << endl;
        }

        // 开始响应回给客户端
        // 添加状态码行
        string response_package = "HTTP/1.0 200 OK\r\n";
        // 添加空行
        response_package += "\r\n";
        // 添加响应正文
        response_package += "hello world, test for http";

        // 将响应发回给客户端
        send(td->_sock, response_package.c_str(), response_package.size(), 0);
    }
}

运行代码启动服务器以后,我们用两种方法来查看服务端发送回来的响应:

第一种是采用telnet工具发起http的请求,然后获得响应正文:

  1. 输入指令telnet IP 端口号
  2. 输入ctrl+]
  3. 再按一次回车
  4. 输入get / http/1.0发起http的get请求

在这里插入图片描述

第二种方法是采用浏览器来访问我们的服务器,让浏览器作为客户端发起HTTP请求,浏览器来接收HTTP响应正文:

在这里插入图片描述

HTTP是如何保证自己的报头和有效载荷被全部读取的呢?

  1. 要想读取到完整的报头,只需要按行读取,直到读取到空行为止即可。
  2. 要想读取到完整的有效载荷,就需要用到报头属性中的正文长度,根据正文长度来读取。

三、利用HTTP协议返回网页

我们写一个简单的代码来演示一下如何将一个网页信息给客户端返回回去,上面我们演示的代码都是在响应报文中添加字符串作为响应,所以客户端访问服务端也能看到对应的字符串。那么我们可以利用这个思路,将网页的html文件用字符串提取出来,然后将整个html的文件内容以字符串的形式响应回去,这样客户端就能获取到我们的网页资源了。

1.getPath函数

客户端给服务器发送过来的请求中,在请求的第一行就包含了资源路径,我们可以从客户端的请求中提取出资源路径。

由于HTTP协议请求格式是以\r\n 作为分隔符的,那么我们查找第一个\r\n分隔符就一定能提取到第一行内容。第一行内容是形如 GET /a/b.html HTTP/1.0\r\n这样的格式,资源路径在第二部分,所以我们先正向查找第一个空格的位置,再逆向查找最后一个空格的位置,两个空格之间的子串就是资源路径。

但是我们还需要对提取出来的资源路径做判断,如果提取上来的web根目录,我们就在资源路径上添加首页文件再返回。

// GET /a/b.html HTTP/1.0\r\n
string getPath(const string &request)
{
    // 查找第一个\r\n,就能查找到第一行
    size_t pos = request.find(CRLF);
    if (pos == string::npos)
    {
        return "";
    }

    // 截取第一行
    string line = request.substr(0, pos);
    // 在第一行中查找第一个空格
    size_t first = line.find(SPACE);
    if (first == string::npos)
    {
        return "";
    }
    // 在第一行中查找最后一个空格
    size_t second = line.rfind(SPACE);
    if (second == string::npos)
    {
        return "";
    }

    // 截取路径
    string path = line.substr(first + SPACE_LEN, second - (first + SPACE_LEN));
    cout << "path: " << path << endl;
    // 如果是web根目录,则加上首页路径
    if (path.size() == 1 && path[0] == '/')
    {
        cout << "web根目录" << endl;
        path += "index.html";
    }

    return path;
}

2.readFile函数

提取到资源路径之后,我们需要到对应的路径下去读取文件内容,再将文件内容以字符串的形式返回。

string readFile(const string &path)
{
    // 打开文件
    ifstream in(path);
    if (!in.is_open())
    {
        return "open file error";
    }

    // 按行读取文件
    string content;
    string line;
    while (getline(in, line))
    {
        content += line;
    }

    // 关闭文件
    in.close();
    return content;
}

3.回调函数

static void *httpServer(void *args)
{
    ThreadData *td = (ThreadData *)args;
    while (true)
    {
        char buffer[10240];
        string request_package;
        ssize_t readRes = read(td->_sock, buffer, sizeof(buffer) - 1);
        if (readRes > 0)
        {
            buffer[readRes] = '\0';
            request_package = buffer;
            // 将请求打印出来
            cout << buffer << endl;
        }

        // 提取请求中的资源路径
        if (request_package.empty())
        {
            continue;
        }
        string recourse_path = td->_server->getPath(request_package);

        string file_path = "./wwwroot";
        file_path += recourse_path;

        cout << file_path << endl;

        // 读取资源路径下的文件
        string html = td->_server->readFile(file_path);

        // 开始响应回给客户端
        // 添加状态码行
        string response_package = "HTTP/1.1 200 OK\r\n";
        response_package += "Content-Type: text/html\r\n";
        // 添加空行
        response_package += "\r\n";
        // 添加响应正文
        response_package += html;

        // 将响应发回给客户端
        send(td->_sock, response_package.c_str(), response_package.size(), 0);
    }
}

四、HTTP协议的请求方法

我们利用网络上网的行为可以分为两种:

  1. 把远端的资源拿到本地,比如我们上面演示的获取网页资源、获取图片资源等。
  2. 把我们的属性字段提交到远端,比如把我们的搜索关键字提交到远端,把我们的登录信息提交到远端。

网站一般被分为静态网站和动态网站,静态网站就是只能把远端的东西拿到本地,不能提交资源到远端,没有交互式的网站;相反的,动态网站就是既能把远端的东西拿到本地,也可以提交资源到远端,就是具有交互式的网站,比如我们写博客,CSDN官网就是一个交互式的动态网站。

将远端资源拿到本地可以采用GET方法,将本地资源提交到远端可以采用GET方法或者POST方法。

GET方法和POST方法有什么区别?

  1. GET方法是通过URL进行传参的,客户端的参数会以明文的形式显示在URL中。
  2. POST方法是通过正文进行传参的,客户端的参数不会显示在URL中,但是会出现在请求正文中。
  3. 所以GET方法是不私密的,它也是不安全的,比如我们在网页输入账号密码的时候,这些会作为参数显示在URL中。
  4. POST方法因为是通过正文传参,所以相对比较私密,但它也是不安全的,比如我们在网页输入的账号密码,虽然URL中不会显示出来,但如果别人用一些抓包工具抓取到了你的请求正文,也是可以轻而易举地获取你的账户信息的。

GET方法和POST方法是HTTP协议最常用的两种方法,除了GET方法和POST方法之外,还有一些不太常用的方法,下面表格是对所有方法的汇总介绍:

其中有一些方法通常是会被服务器禁止掉的,比如DELETE方法,必须被禁止,否则别人通过客户端发起一个DELETE请求就可以随意删除我们服务端的文件,是非常危险的。

方法说明支持的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
UNLINK断开连接关系1.0

五、HTTP的状态码

HTTP协议的状态码有五类,分别以数字1、2、3、4、5开头,每一类都有其特殊的含义:

  1. 1XX:以数字1开头的这一类状态码是信息性状态码,表示当前服务器正在处理接收的请求,所以如果等待时间比较长的话,服务器可以响应这一类状态码表示请求正在处理,耐心等待。
  2. 2XX:以数字2开头的这一类状态码是成功状态码,表示当前服务器已经正常处理完毕客户端的请求。
  3. 3XX:以数字3开头的这一类状态码是重定向状态码,表示需要附加操作以完成请求。目前浏览器主流接受的重定向状态码就只有两个,一个是301另一个是302。301是永久重定向,它的应用场景一般是某些网站因为域名更改了,为了让老用户从老域名也能访问到新网站,就使用永久重定向,当别人访问老域名时会自动跳转到新域名。302是临时重定向,它的应用场景一般是某些网站要更新资源时,为了不让网站停机,会创建一个临时重定向让用户访问原来的资源,当新资源更新好了以后再撤销这个临时重定向。
  4. 4XX:以数字4开头的这一类状态码是客户端错误状态码,表示服务器无法处理请求。比如我们经常见到的404状态码。
  5. 5XX:以数字5开头的这一类状态码是服务器错误状态码,表示服务器处理请求时服务器出错了。

六、HTTP常见的报头属性

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

七、cookie和session

HTTP协议有一个特点就是,用户的访问行为,HTTP协议不会记录。但是我们会发现,我们在使用浏览器上网的时候,浏览器是会有记录我们的访问行为的,再比如说我们用一些网站在登录的时候,第一次登录需要我们输入账号密码,但是第二次登录就不需要了。

这种功能是通过cookie来实现的,它的原理是:当我们用浏览器作为客户端访问服务器的时候,如果需要登录,第一次登录我们需要在客户端输入用户名和密码,客户端再将用户名和密码发送回给服务端,服务端拿到用户名和密码后去数据库中比对,如果查找到了该用户且密码正确就可以成功登录,成功登录以后服务端会将成功登录的信息响应回给客户端,客户端拿到这些信息就会生成对应的cookie文件,该文件中就保存着用户名和密码。在下一次客户端发起HTTP请求的时候,它会自动携带cookie文件的内容,也就不再需要我们输入用户名和密码了。

但是cookie策略是有安全隐患的,如果cookie文件保存了过于私密的信息,一旦cookie文件泄露出去,可能会对我们的数据安全或者财产安全产生很大影响。所以现在主流的会话保持策略是cookie+session方案。

cookie+session的方案是当用户第一次登录输入用户名和密码时,服务端会帮我们生成一份session文件用来保存用户的私密信息,并且会为这个session文件生成唯一的文件名,也就相当于生成了一份唯一的文件编号。然后服务端再将成功登录的信息响应回给客户端,并且会将session文件的对应编号值发送给客户端,客户端收到之后就会生成cookie,此时客户端的cookie文件就不再保存用户的私密信息了,而是将session文件的编号值保存在cookie文件中。下一次访问服务器的时候,客户端发送cookie文件中的编号值即可。也就是说,这种方案是将原来保存在客户端本地的用户私密信息保存到了远端的服务器上。

八、Connection字段

在HTTP协议的请求和响应报文中,有一个Connection字段,它表示HTTP协议是长链接还是短链接,在HTTP/1.0版本中是基于短链接的,所谓的短链接(closed)就是客户端发起一次请求,服务端响应之后就关闭链接了,短链接一次只处理一个HTTP请求。我们日常看到的网页,它可能这一张网页就发起了几十次上百次的HTTP请求,HTTP协议底层采用的就是TCP协议,每一次链接都需要在底层经历三次握手,断开链接都需要经历四次挥手,如果现在主流的网站还是采用短链接的话,一个HTTP请求处理完之后就断开,下一个HTTP请求来的时候再建立链接,那一张网页加载出来可能要几十次上百次的发起HTTP请求,在底层就要经历很多次的三次握手和四次挥手,这样的成本是很高的效率也是很低的。

所以短链接就不再适合现在主流的HTTP请求了,我们采用主流的长链接。采用长链接的方案,客户端和服务端在建立网络连接之前先要进行HTTP版本和连接方式协商,如果版本号一致并且Connection字段是keep-alive,代表双方采用长链接的方式。双方建立链接之后,服务端不会在处理完一个请求之后就断开链接,而是接收客户端的多次HTTP请求,按顺序处理之后再响应回给客户端。当所有的请求完毕时才会断开链接,比如说我们在加载一张网页时,它可能会发起多次HTTP请求,当一张完整的网页被加载出来时,双方的链接才会被断开。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值