Linux网络:应用层之HTTP协议

一、应用层

我们程序员写的一个个解决实际问题,满足日常需求的网络程序,都是在应用层。

1.协议

协议是一种约定。网络协议是计算机网络中通信双方都必须遵守的一组约定。

在网络通信中,都是以 “字符串” 的方式来发送和接收数据的。
如果要发送和接收一些结构化的数据,就需要序列化和反序列化。

  • 什么是序列化和反序列化?
    ① 序列化:将结构化的数据按照一个规则转化成字符串。
    ② 反序列化:将字符串按照相同的规则转化回结构化的数据。

  • 为什么要进行序列化和反序列化?
    ① 为了方便应用层的网络通信。
    ② 为了方便上层使用内部成员,将应用和网络进行解耦。

结构化的数据,是不便于网络传输的,而字符串便于网络传输。
结构化的数据,以二进制的方式进行收发会有问题,因为客户端和服务器对结构体大小的认识可能不一样,比如每个字段的大小,内存对齐的方式。

结构化的数据,本质就是协议的表现。

2.网络版计算器

程序说明:client 发送一个计算请求给 server ,server 接收到后进行计算,再将响应结果返回给 client 。

我们约定:
 ① 定义结构体来表示我们需要交互的信息,在这里就是请求和响应。
 ② 对收发的数据进行序列化和反序列化。

我们会用到 json 组件对结构化的数据进行序列化和反序列化。

Linux 平台下,下载 json 组件的命令:sudo yum install -y jsoncpp-devel

下面的程序包含四个文件:
 ① Sock.hpp:基本通信函数的实现。
 ② Protocol.hpp:协议的定制、序列化和反序列化的实现。
 ③ CalServer.cc:服务端。
 ④ CalClient.cc:客户端。

  • Sock.hpp:
#pragma once

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

using namespace std;

class Sock
{
public:
    static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if(sock < 0){
            cerr << "socket error!" << endl;
            exit(2);
        }

        return sock;
    }

    static void Bind(int sock, uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        
        if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
            cerr << "bind error!" << endl;
            exit(3);
        }
    }

    static void Listen(int sock)
    {
        if(listen(sock, 5) < 0)
        {
            cerr << "listen error!" << endl;
            exit(4);
        }
    }

    static int Accept(int sock)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(sock, (struct sockaddr*)&peer, &len);
        if(fd >= 0){
            return fd;
        }
        return -1;
    }

    static void Connect(int sock, std::string ip, uint16_t port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));

        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        server.sin_addr.s_addr = inet_addr(ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
        {
            cout << "Connect Success!" << endl;
        }
        else
        {
            cout << "Connect Failed!" << endl;
            exit(5);
        }
    }
};
  • Protocol.hpp:
#pragma once

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

using namespace std;

// 定制协议的过程,目前就是定制结构化数据的过程
// 我们自己定义的协议,client && server 都必须遵守!这就叫做自定义协议
// 请求格式
typedef struct request
{
    int x;
    int y;
    char op;  // "+-*/%"
} request_t;

// 响应格式
typedef struct response
{
    int code;
    // 只有计算结果,
    // 不能区分是正常的计算结果,还是异常的退出结果
    // server运算完毕的计算状态: 
    // code(0:success), code(-1: div 0) ...
    
    int result;  
} response_t;

// request_t -> string
std::string SerializeRequest(const request_t &req)
{
    // 序列化
    Json::Value root;  // 可以盛装任何对象,json是一种kv式的序列化方案
    root["datax"] = req.x;
    root["datay"] = req.y;
    root["operator"] = req.op;

    // FastWriter, StyledWriter
    // Json::StyledWriter writer;
    Json::FastWriter writer;
    std::string json_string = writer.write(root);
    return json_string;
}

// string -> request_t
void DeserializeRequest(const std::string &json_string, request_t &out)
{
    // 反序列化
    Json::Reader reader;
    Json::Value root;

    reader.parse(json_string, root);

    out.x = root["datax"].asInt();
    out.y = root["datay"].asInt();
    out.op = (char)root["operator"].asInt();
}

std::string SerializeResponse(const response_t &resp)
{
    Json::Value root;
    root["code"] = resp.code;
    root["result"] = resp.result;

    Json::FastWriter writer;
    std::string res = writer.write(root);
    
    return res;
}

void DeserializeResponse(const std::string &json_string, response_t &out)
{
    Json::Reader reader;
    Json::Value root;

    reader.parse(json_string, root);
    out.code = root["code"].asInt();
    out.result = root["result"].asInt();
}
  • CalServer.cc:
#include <pthread.h>
#include "Protocol.hpp"
#include "Sock.hpp"

static void Usage(string proc)
{
    cout << "Usage: " << proc << " port" << endl;
}

void *HandlerRequest(void *args)
{
    int sock = *(int *)args;
    delete (int *)args;

    pthread_detach(pthread_self());

    // 业务逻辑,做一个短服务
    // request -> 分析处理 -> 构建response -> send(response) -> close(sock)
    // 1. 读取请求
    char buffer[1024];
    request_t req;
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0;
        cout << "get a new request: " << buffer << endl;
        std::string str = buffer;
        DeserializeRequest(str, req);  // 反序列化请求

        // 2. 分析请求 && 3. 计算结果
        response_t resp = {0, 0};
        switch (req.op)
        {
        case '+':
            resp.result = req.x + req.y;
            break;
        case '-':
            resp.result = req.x - req.y;
            break;
        case '*':
            resp.result = req.x * req.y;
            break;
        case '/':
            if (req.y == 0)
                resp.code = -1; // 代表除0
            else
                resp.result = req.x / req.y;
            break;
        case '%':
            if (req.y == 0)
                resp.code = -2; // 代表模0
            else
                resp.result = req.x % req.y;
            break;
        default:
            resp.code = -3; // 代表请求方法异常
            break;
        }
        cout << "request: " << req.x << req.op << req.y << endl;

        // 4. 构建响应,并进行返回
        std::string send_string = SerializeResponse(resp);  // 序列化之后的字符串
        write(sock, send_string.c_str(), send_string.size());

        cout << "服务结束: " << send_string << endl;
    }

    // 5. 关闭连接
    close(sock);
}

// ./CalServer port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }    

    uint16_t port = atoi(argv[1]);

    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for (;;)
    {
        int sock = Sock::Accept(listen_sock);
        if (sock >= 0)
        {
            cout << "get a new client..." << endl;
            int *pram = new int(sock);
            pthread_t tid;
            pthread_create(&tid, nullptr, HandlerRequest, pram);
        }
    }

    return 0;
}
  • CalClient.cc:
#include "Protocol.hpp"
#include "Sock.hpp"

void Usage(string proc)
{
    cout << "Usage: " << proc << " server_ip server_port" << endl;
}

// ./CalClient server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int sock = Sock::Socket();
    Sock::Connect(sock, argv[1], atoi(argv[2]));

    // 业务逻辑
    request_t req;
    memset(&req, 0, sizeof(req));
    cout << "Please Enter Data One# ";
    cin >> req.x;
    cout << "Please Enter Data Two# ";
    cin >> req.y;
    cout << "Please Enter Operator# ";
    cin >> req.op;

    std::string json_string = SerializeRequest(req);  // 序列化请求

    ssize_t s = write(sock, json_string.c_str(), json_string.size());

    char buffer[1024];
    s = read(sock, buffer, sizeof(buffer)-1);
    if(s > 0)
    {
        response_t resp;
        buffer[s] = 0;
        std::string str = buffer;
        DeserializeResponse(str, resp);  // 反序列化

        cout << "code[0:success]: " << resp.code << endl;
        cout << "result: " << resp.result << endl;
    }

    return 0;
}

运行测试:
在这里插入图片描述

我们上面写的 C/S 模式的在线版本计算器,本质就是一个应用层网络服务:基本通信代码、结构化数据等约定、业务逻辑、序列化和反序列化,这些都是由我们自己实现的。

二、HTTP 协议

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

网络通信、序列化和反序列化、协议细节,这些在 HTTP 协议的内部都实现了。

1. URL

我们请求的图片、视频、音频、文档等,都称之为资源。
服务器的后台是 Linux 。

公网 IP 唯一确认一台主机,而网络资源一定是存在网络中的一台 Linux 机器上!
Linux 保存资源的方式,都是以文件的方式保存的。单 Linux 系统,标识一个唯一资源的方式,是通过路径!

所以,IP + Linux 路径,就可以唯一地确认一个网络资源!
IP:通常以域名的方式呈现,路径可以通过目录名 + / 确认。

例子:
在这里插入图片描述

我们所说的 “网址” ,其实就是 URL(Uniform Resource Locator,统一资源定位符)。

在这里插入图片描述

 ① 协议方案名:想要通过什么协议来获得资源。
 ② 登录信息:现在主流的 URL 已经很少用它,一般是通过表单的方式去提交。
 ③ 服务器地址:IP 通常以域名的方式呈现,实际上是建立了一个域名和 IP 地址的映射关系。
 ④ 服务器端口号:要访问服务器的端口,一般是省略的。因为协议方案名和端口号的关系是十分紧密的,在 URL 中指明了协议方案,端口号就不用呈现了。
 ⑤ 带层次的文件路径:基于 Linux 的文件系统目录结构。

一个基本的 URL 的构成方式是:协议 + 域名 + 资源路径,可能带参。

URL 的作用是,确认全网中唯一的一个资源。

2. HTTP 协议格式

HTTP 协议的序列化和反序列化格式,是 HTTP 协议自身定制的。

HTTP 请求和响应,基本上都是以行(\n)为单位进行构建的。
无论是请求还是响应,基本上都是由 3 或 4 部分组成。


在这里插入图片描述

说明:
 ① 请求行/状态行只有一行内容。
 ② 请求报头/响应报头有多行内容,是请求/响应属性,这些属性都以冒号分割的键值对的形式按行陈列(即每组属性之间使用 \n 分隔)。
 ③ 空行就是一个 \n 。
 ④ 请求正文/响应正文允许为空字符串。若请求正文/响应正文存在,则在请求报头/响应报头中会有一个 Content-Length 属性来标识请求正文/响应正文的长度。

HTTP request 和 HTTP response ,其实就是一个长字符串!它们的读取和发送实际上就是字符串的读取和发送。上面的形式是打印出来之后呈现的结果。

  • HTTP 的解包:读字符串时只要读到空行,就是把报头读完了,剩下的部分就是有效载荷。HTTP 的报头和有效载荷,是通过一个特殊字符(空行)来区分的。
  • HTTP 的封装:构建 HTTP 请求时,在请求行和请求报头后加上空行,然后再加上请求正文即可。构建 HTTP 响应同理。
  • HTTP 的分用:不是 HTTP 解决的,是具体的应用代码解决的。HTTP 需要有接口来帮助上层获取参数。

HTTP 协议的底层是 TCP 协议。

3.查看 HTTP 请求

程序说明:服务端只读取并打印 HTTP 请求,不构建和发送 HTTP 响应。

下面的程序包含两个文件:
 ① Sock.hpp:基本通信函数的实现。
 ② Http.cc:服务端。

  • Sock.hpp:
#pragma once

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

using namespace std;

class Sock
{
public:
    static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if(sock < 0){
            cerr << "socket error!" << endl;
            exit(2);
        }

        return sock;
    }

    static void Bind(int sock, uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        
        if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
            cerr << "bind error!" << endl;
            exit(3);
        }
    }

    static void Listen(int sock)
    {
        if(listen(sock, 5) < 0)
        {
            cerr << "listen error!" << endl;
            exit(4);
        }
    }

    static int Accept(int sock)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(sock, (struct sockaddr*)&peer, &len);
        if(fd >= 0){
            return fd;
        }
        return -1;
    }

    static void Connect(int sock, std::string ip, uint16_t port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));

        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        server.sin_addr.s_addr = inet_addr(ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
        {
            cout << "Connect Success!" << endl;
        }
        else
        {
            cout << "Connect Failed!" << endl;
            exit(5);
        }
    }
};
  • Http.cc:
#include "Sock.hpp"
#include <pthread.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void *HandlerHttpRequest(void *args)
{
    int sock = *(int*)args;
    delete (int*)args;
    pthread_detach(pthread_self());

#define SIZE 1024*10
    char buffer[SIZE];
    memset(buffer, 0, sizeof(buffer));

    // 这种读法是不正确的,只不过在现在问题没有暴露出来
    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);  // 跟read函数等价
    if(s > 0)
    {
        buffer[s] = 0;
        std::cout << "--------------------http request begin--------------------" << std::endl;
        std::cout << buffer;  // 查看http的请求格式!for test
        std::cout << "---------------------http request end---------------------" << std::endl;
    }

    close(sock);
    return nullptr;
}

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

    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for( ; ; )
    {
        int sock = Sock::Accept(listen_sock);
        if(sock > 0)
        {
            pthread_t tid;
            int *parm = new int(sock);
            pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
        }
    }
}

运行测试:
使用浏览器连接服务器(浏览器会给服务器发送 HTTP 请求):在这里插入图片描述

服务器接收到 HTTP 请求后,将其打印:
在这里插入图片描述
由于我们没有提交数据,所以这里的 HTTP 请求没有请求正文,只有三部分。

4.发送 HTTP 响应

HTTP 请求的 / 并不是根目录,而是叫做 web 根目录。web 根目录下放置的内容,都叫做资源!

我们一般要请求的一定是一个具体的资源:
 ① 若请求是 / ,意味着我们要请求的是该网站的首页,即 index.html 或 index.htm(一般所有的网站,都要有默认首页)。其实是服务器内部做判断,将访问资源的路径由 / 改成 /index.html 或 /index.htm 。
 ② 若请求是完整路径,意味着我们要请求的是一个特定的资源。

程序说明:服务端先读取并打印 HTTP 请求,然后发送一个 HTTP 响应。这里我们写死了,无论给服务器发送什么HTTP请求,服务器都只会把首页信息返回回去。

下面的程序包含三个文件:
 ① Sock.hpp:基本通信函数的实现,跟上面的一样,这里就不贴了。
 ② Http.cc:服务端。
 ③ index.html:一个简单的网站首页。

  • Http.cc:
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>

// 其中我们这里的wwwroot目录,就叫做web根目录
// 理论上,web根目录可以放在Linux系统的任何目录下,只要能找到就行

// web根目录下的index.html就叫做网站的首页

#define WWWROOT "./wwwroot/"
#define HOME_PAGE "index.html"

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void *HandlerHttpRequest(void *args)
{
    int sock = *(int*)args;
    delete (int*)args;
    pthread_detach(pthread_self());

#define SIZE 1024*10
    char buffer[SIZE];
    memset(buffer, 0, sizeof(buffer));

    // 这种读法是不正确的,只不过在现在问题没有暴露出来
    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);  // 跟read函数等价
    if(s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer;  // 查看http的请求格式!for test

        // std::string http_response = "HTTP/1.0 200 OK\n";
        // http_response += "Content-Type: text/plain\n";  // text/plain,正文是普通的文本
        // http_response += "\n";  // 空行
        // http_response += "hello world!";
        // send(sock, http_response.c_str(), http_response.size(), 0);  // 跟write函数等价

        // 这里我们写死了,无论给服务器发送什么HTTP请求,服务器都只会把首页信息返回回去
        std::string html_file = WWWROOT;
        html_file += HOME_PAGE;  // html_file表示文件路径
        // stat系统调用:获取指定路径下文件的属性(第二个参数是输出型参数)
        struct stat st;
        stat(html_file.c_str(), &st);
        // 返回的时候,不仅仅是返回正文网页信息,还要包括http响应
        std::string http_response = "HTTP/1.0 200 OK\n";
        http_response += "Content-Type: text/html; charset=utf8\n";  // 正文部分的数据类型
        http_response += "Content-Length: ";
        http_response += std::to_string(st.st_size);  // 内容长度,就是文件的大小
        http_response += "\n";
        http_response += "\n";  // 空行
        // 接下来才是正文
        std::ifstream in(html_file);  // 使用文件流对象打开文件
        if(!in.is_open())
        {
            std::cerr << "open html error!" << std::endl;
        }
        else{
            std::string content;
            std::string line;
            while(std::getline(in, line)){  // 用getline一行一行读取文件内容
                content += line;
            }
            http_response += content;
            in.close();

            send(sock, http_response.c_str(), http_response.size(), 0);  // 跟write函数等价
        }
    }

    close(sock);
    return nullptr;
}

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

    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for( ; ; )
    {
        int sock = Sock::Accept(listen_sock);
        if(sock > 0)
        {
            pthread_t tid;
            int *parm = new int(sock);
            pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
        }
    }
}
  • index.html:
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h3>hello world!</h3>
    </body>
</html>

运行测试:
浏览器给服务器发送 HTTP 请求,并接收到服务器的 HTTP 响应:
在这里插入图片描述

服务器接收到 HTTP 请求后,将其打印:
在这里插入图片描述

HTTP 协议,如果我们自己写的话,本质是我们要根据协议内容来进行文本分析。

  • 读取 HTTP request/response 时,我们要做到的事情是:
     ① 保证每次读取都是读取完整的一个 HTTP request/response 。
     ② 保证每次读取都不要将下一个 HTTP request/response 的一部分读到。

以 HTTP request 为例:在这里插入图片描述
读到空行,即可判定已经将报头部分读完。决定请求后面有没有正文,和请求方法有关。
Q:如果有正文,如何保证把正文全部读取完成,而且不要把下一个 HTTP 请求的部分数据读到呢?
A:读到空行,表明已把报头读完,就能提取报头中的一个属性Content-Length: len(如果有正文,就会有这个属性),该属性表明正文部分有多少个字节,然后再根据 len 决定读取多少个字节的正文。
正文就是有效载荷,换言之,Content-Length 表明了有效载荷的长度。

空行能够做到将报头和有效载荷进行分离(解包)。
同时,若存在 Content-Length ,它会帮助我们读取到完整的 HTTP 请求/响应;若不存在 Content-Length ,就是没有正文的时候,此时的 HTTP 请求/响应就只有三部分,只要读到空行,就读到了完整的 HTTP 请求/响应。

5. HTTP 的方法

在这里插入图片描述

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

实际上,管理服务器的人会把 GET 方法和 POST 方法暴露出来,一般不会把其它的方法暴露出来(主要是为了防止恶意操作,虽然协议支持)。


GET 方法和 POST 方法:

这里只修改了 index.html 的文件内容,其它文件内容不变。

  • GET 方法:如果提交参数,是通过 URL 的方式进行提交的。
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h5>hello 我是首页!</h5>

        <h5>hello 我是表单!</h5>
        
        <!-- /a/b/handler_form并不存在,也不处理 -->
        <form action="/a/b/handler_from" method="GET">
            姓名:<input type="text" name="name"><br/>
            密码:<input type="password" name="passwd"><br/>
            <input type="submit" value="登录">
        </form>
    </body>
</html>

运行测试:
在这里插入图片描述
在这里插入图片描述

  • POST 方法:如果提交参数,是通过正文的方式进行提交的。
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h5>hello 我是首页!</h5>

        <h5>hello 我是表单!</h5>
        
        <!-- /a/b/handler_form并不存在,也不处理 -->
        <form action="/a/b/handler_from" method="POST">
            姓名:<input type="text" name="name"><br/>
            密码:<input type="password" name="passwd"><br/>
            <input type="submit" value="登录">
        </form>
    </body>
</html>

运行测试:
在这里插入图片描述这里需要刷新一下浏览器,才能在请求正文中看到 POST 方法提交的参数:
在这里插入图片描述

从上面的实验我们可以看到,GET 方法和 POST 方法都可以向服务器提交参数,它们的区别是提交参数时参数所放的位置不同。


总结:

  1. GET 方法:叫做获取,是最常用的方法。默认一般获取所有的网页,都是 GET 方法,如果要提交参数,它是通过 URL 来进行参数拼接,从而提交给服务端。
  2. POST 方法:叫做推送,是提交参数比较常用的方法。如果提交参数,一般是通过正文部分提交的(Content-Length: xxx表示参数的长度)。
  3. 如果想获得某种资源,大部分情况下用的是 GET 方法。当提交参数时,既可以使用 GET 方法也可以使用 POST 方法。
  4. 它们的区别:
    ① 提交参数时参数所放的位置不同,POST 方法比较私密(私密 != 安全),因为不会回显到浏览器的 URL 输入框。而 GET 方法不私密,因为会将重要信息回显到浏览器的 URL 输入框。
    ② GET 方法是通过 URL 传参的,而 URL 是有大小限制的,和具体的浏览器有关。POST 方法是通过正文部分传参的,一般没有大小限制。
  5. 如何选择:
    ① GET 方法:如果提交的参数不敏感,数量非常少,可以采用 GET 方法。
    ② POST 方法:否则,就使用 POST 方法。

HTTP 协议处理,本质是文本分析。
所谓的文本分析:
 ① HTTP 协议本身的字段。
 ② 提取参数,如果有的话。
GET 或者 POST ,其实是前后端交互的一个重要方式。

6. HTTP 的状态码

在这里插入图片描述最常见的状态码,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),502(Bad Gateway) 。

应用层是人要参与的,人的水平参差不齐,HTTP 的状态码,很多人根本就不清楚如何使用,又因为浏览器的种类太多了,导致大家可能对状态码的支持并不是特别好。
类似于 404 的状态码,对浏览器没有任何指导意义,浏览器就是正常地显示网页,不会根据状态码做处理。

3XX 的状态码是有特殊含义的,重定向:
 ① 永久重定向:301 ,常用于网站搬迁、域名更换。
 ② 临时重定向:302 or 307 ,比如注册或登录成功后自动跳转到首页。

重定向的例子:
 ① 当访问某一个网站的时候,会让我们跳转到另一个网址。
 ② 当我们访问某种资源的时候,提示我们登录,跳转到了登录页面,登录成功后,会自动跳转回来。

重定向是需要浏览器给我们提供支持的,前提是浏览器必须识别重定向状态码。
要完成重定向,需要服务器的 HTTP 的响应报头存在这么一个属性,Location: 新的地址,告诉客户端接下来要去哪里访问。


重定向演示:
下面的程序包含两个文件:
 ① Sock.hpp:基本通信函数的实现,跟上面的一样,这里就不贴了。
 ② Http.cc:服务端。

  • Http.cc:
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>

#define WWWROOT "./wwwroot/"
#define HOME_PAGE "index.html"

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void *HandlerHttpRequest(void *args)
{
    int sock = *(int *)args;
    delete (int *)args;
    pthread_detach(pthread_self());

#define SIZE 1024 * 10
    char buffer[SIZE];
    memset(buffer, 0, sizeof(buffer));

    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
    if (s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer; // 查看http的请求格式!for test

        std::string response = "HTTP/1.0 301 Permanently moved\n";
        response += "Location: https://www.qq.com/\n";
        response += "\n";
        send(sock, response.c_str(), response.size(), 0);
    }

    close(sock);
    return nullptr;
}

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

    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for (;;)
    {
        int sock = Sock::Accept(listen_sock);
        if (sock > 0)
        {
            pthread_t tid;
            int *parm = new int(sock);
            pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
        }
    }
}

运行测试:
在浏览器的 URL 输入框中输入服务器的 IP 和 port 后,自动跳转到重定向的网站。
在这里插入图片描述在这里插入图片描述

7. HTTP 的版本

  • HTTP/1.0:短连接。建立连接 -> 请求 -> 响应 -> 关闭连接,每次 HTTP 请求返回一个资源。
    一个大型的网页,是由非常多个资源组成的,于是访问一个大型网页时,就需要进行多次 HTTP 请求。HTTP 协议是基于 TCP 协议的,所以 TCP 要通信,就要建立连接 -> 传输数据 -> 关闭连接,每一次 HTTP 请求都要执行上面的过程。所以,频繁地建立 TCP 连接,带来的开销就会很大,降低了效率。

  • HTTP/1.1 之后:支持长连接。长连接的请求/响应报头中会有Connection: keep-alive字段。一个连接可以进行多次请求/响应,不需要频繁地建立 TCP 连接,提高了效率。

8. HTTP 常见 Header

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

9. Cookie 与 session

HTTP 协议本身是一种无状态的协议,它并不记录发起 HTTP 请求的上下文信息。

当我们在进行各种页面跳转时,本质就是在进行各种 HTTP 请求,但是不管怎么跳转,网站照样认识我们(比如帐号身份信息)。

HTTP 协议主要帮我们解决网络资源获取的问题。要网站认识我们,并不是 HTTP 协议本身要解决的问题,但 HTTP 可以提供一些技术支持,来保证网站具有 “会话保持” 的功能,会话管理的功能由 Cookie 技术支持。

  • Cookie:
     ① 浏览器:Cookie 其实是一个文件,在浏览器中,有两种存在形式:文件版和内存版。该文件里面保存的是用户的私密信息。
     ② HTTP 协议:如果有该网站对应的 Cookie ,在发起任何请求的时候,都会自动在 request 中携带该 Cookie 信息。

Cookie 演示:
下面的程序包含三个文件:
 ① Sock.hpp:基本通信函数的实现,跟上面的一样,这里就不贴了。
 ② Http.cc:服务端。
 ③ index.html:一个简单的网站首页。

  • Http.cc:
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>

#define WWWROOT "./wwwroot/"
#define HOME_PAGE "index.html"

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void *HandlerHttpRequest(void *args)
{
    int sock = *(int *)args;
    delete (int *)args;
    pthread_detach(pthread_self());

#define SIZE 1024 * 10
    char buffer[SIZE];
    memset(buffer, 0, sizeof(buffer));

    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
    if (s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer; // 查看http的请求格式!for test

        std::string html_file = WWWROOT;
        html_file += HOME_PAGE; // html_file表示文件路径

        // 接下来才是正文
        std::ifstream in(html_file); // 使用文件流对象打开文件
        if (!in.is_open())
        {
            std::string http_response = "HTTP/1.0 404 Not Found\n";
            http_response += "Content-Type: text/html; charset=utf8\n"; // 正文部分的数据类型
            http_response += "\n";
            http_response += "<html><p>你访问的资源不存在</p></html>";
            send(sock, http_response.c_str(), http_response.size(), 0);
        }
        else
        {
            // stat系统调用:获取指定路径下文件的属性(第二个参数是输出型参数)
            struct stat st;
            stat(html_file.c_str(), &st);
            // 返回的时候,不仅仅是返回正文网页信息,还要包括http响应
            std::string http_response = "HTTP/1.0 200 OK\n";
            http_response += "Content-Type: text/html; charset=utf8\n"; // 正文部分的数据类型
            http_response += "Content-Length: ";
            http_response += std::to_string(st.st_size); // 内容长度,就是文件的大小
            http_response += "\n";

            // Set-Cookie:服务器向浏览器设置一个Cookie
            // 告诉浏览器把Set-Cookie后面的内容写到浏览器的Cookie文件里
            // 从此往后,浏览器再向服务器请求时,都会把这个Cookie信息带上
            http_response += "Set-Cookie: id=1111\n";
            http_response += "Set-Cookie: password=2222\n";
            http_response += "\n";  // 空行

            std::string content;
            std::string line;
            while (std::getline(in, line))
            { // 用getline一行一行读取文件内容
                content += line;
            }
            http_response += content;
            in.close();

            send(sock, http_response.c_str(), http_response.size(), 0);
        }
    }

    close(sock);
    return nullptr;
}

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

    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for (;;)
    {
        int sock = Sock::Accept(listen_sock);
        if (sock > 0)
        {
            pthread_t tid;
            int *parm = new int(sock);
            pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
        }
    }
}
  • index.html:
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h5>hello 我是首页!</h5>

        <h5>hello 我是表单!</h5>
        
        <!-- /a/b/handler_form并不存在,也不处理 -->
        <form action="/a/b/handler_from" method="POST">
            姓名:<input type="text" name="name"><br/>
            密码:<input type="password" name="passwd"><br/>
            <input type="submit" value="登录">
        </form>
    </body>
</html>

运行测试:
在这里插入图片描述
在这里插入图片描述

刷新几次浏览器:浏览器检测到要访问网站的 Cookie 是存在的,于是在以后的请求中自动携带上 Cookie 。
在这里插入图片描述
若我们把该网站的 Cookie 移除掉,再刷新一下浏览器:
在这里插入图片描述

我们就会发现浏览器发送请求时就不再携带 Cookie 了,因为浏览器发现没有该网站对应的 Cookie 信息。在这里插入图片描述

服务器要给浏览器设置 Cookie ,需要在响应报头中加上Set-Cookie: key=value字段。
我们发现,浏览器每次向目标网站发起请求时,都会自动把曾经给浏览器写入的目标网站的 Cookie 信息携带上。

我们在任何网站登录后,一定会存在对应网站的 Cookie ,如果不想让网站认识我们,就可以把 Cookie 直接移除掉,再刷新网页,于是这个网站就不认识我们了;如果把 Cookie 一直保留着,浏览器在向目标网站发起任何请求时都会携带上 Cookie 字段,支持服务后端对我们的身份进行各种验证,所以在用户看来,只要在某个网站登录过一次,后面再次访问该网站时已无需登录,它已经认识我们了。

 ① 文件版 Cookie:在浏览器的安装目录以及浏览器使用的某些相关的用户级目录下,Cookie 信息保存在文件里,关闭浏览器甚至电脑重启再打开,都不会影响 Cookie 信息,再打开网站,还认识我们。
 ② 内存版 Cookie:Cookie 信息在内存中,浏览器一关闭,Cookie 信息就没有了,再打开网站,就不认识我们了。

这就是 Cookie 技术。

别人如果盗取我们的 Cookie 文件,别人就可以以我们的身份进行认证,访问特定的资源。如果保存的是我们的用户名和密码,那么就非常糟糕了。
单纯使用 Cookie 是具有一定安全隐患的。

  • session:核心思路是,将用户的私密信息保存在服务端。
    在这里插入图片描述

因为客户端的 Cookie 文件里面不再保存用户的任何私密信息,所以,即使 Cookie 文件泄露了,也不会导致用户的私密信息被泄露。

仍然存在 Cookie 文件被泄露的风险,这是无法杜绝的。如果 Cookie 文件泄露了,别人就可以拿着它,冒充我们的身份去访问对应的网站。虽然这个问题没法彻底解决,但是有一些衍生的防御方案,比如异地登录、短信认证等。

Cookie + session ,本质:增强用户访问网站或者平台的体验。

三、HTTP 与 HTTPS

HTTP 对数据是没有经过任何加密的,在网络中,数据其实是 “裸” 着的,是不安全的。

因此,才有了 HTTPS 的出现。

HTTPS = HTTP + TLS/SSL 。S 是 Secure 。TLS/SSL 可以理解为 HTTP 数据的加密解密层,也在应用层。HTTPS 对数据是经过加密的,是安全的。

目前大多数网站使用的是 HTTPS 协议。

什么叫做安全?安全不是让别人拿不到,而是别人拿到了也没法处理。
什么情况下是比较安全的呢?就是解密的成本远远超过解密之后带来的收益。

在这里插入图片描述
简单理解一下加密方式:

  • 对称加密,密钥(只有一个)X
    用 X 加密,也要用 X 解密。
    data ^ X = result
    result ^ X = data

  • 非对称加密,有一对密钥:公钥和私钥。
    用公钥加密,但只能用私钥解密,或者用私钥加密,但只能用公钥解密。
    一般而言,公钥是全世界公开的,私钥是自己必须私有保存的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值