当前位置: 首页 > article >正文

网络应用层之HTTP

现成的应用层协议

实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一

一. HTTP

HTTP协议中文名为超文本传输协议, 既是最经典的应用层协议, 也是应用最广泛的协议. 它可以将服务器上的任意类型的数据拉取到本地浏览器, 浏览器对其进行解释可以得到网页, 文本 , 图片 , 视频, 音频等资源

1.1 认识URL

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

  •  http是协议方案名, 默认端口号是80; https是加密式的http, 默认端口号是443
  • 登录信息这种直接在URL中包含用户名和密码的方式不安全, 容易被窃取, 通常不推荐使用
  • www.example.ip 是服务器的地址, 可以是域名或IP地址;  域名会通过DNS解析为IP地址
  • 80 是HTTP协议的默认端口号, 如果协议方案名是http默认都是省略为80, https省略为443. 此外, 重要的协议端口号必须是众所周知的, 不可随意修改的.
  • /dir/index.htm 表示在服务器(一般是linux)上资源的路径, /是web根目录
  • ? 后面的部分是参数,用于向服务器传递数据; 多个参数可以用&连接
  • # 后面的部分是片段标识符, 通常用于指向页面内的特定部分(如锚点)。ch1 可能是页面内的某个章节或元素的ID。

总结:

1. URL必须要有ip+port

2. 我们平时上网, 本质是在进行进程间的通信

3. 我们的上网行为分为两种: a.获取资源 b.上传资源 , 分别对应进程间通信的IO; 而这些资源可能是网页, 图片, 视频, 音频等, 具体来说可以是我们使用浏览器客户端进程去申请Linux服务器的资源, 浏览器将请求得到的资源进行解释展示给我们.

1.2 urlencode和urldecode

如果我们在搜索引擎中搜索一串内容, 这个内容会被当成一个参数写入URL中 

把上面的URL复制下来:

https://cn.bing.com/search?q=%3A%26%2B%3Dhello&qs=n&form=QBRE&sp=-1&lq=0&pq=%3A%26%2B%3Dhello&sc=17-9&sk=&cvid=2E427B71C4F740498C636AC3938A7217&ghsh=0&ghacc=0&ghpl=

可以发现有一个变量为q=%3A%26%2B%3Dhello, 这就是我们搜索的内容. 

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

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

这个转化的过程就被称之为URL的encode编码, 由浏览器(客户端)完成; 而decode就是encode的逆过程.服务器在收到客户端发来的请求时会进行decode编码,将原来的数据恢复过来,继续处理请求.

在网上也有这种编码方式的转译工具.

1.3 HTTP报文格式

先实现一个简单的HttpServer, 通过浏览器访问该服务器后, 将浏览器的http请求打印出来, 可以得到这样一串请求报文:

GET / HTTP/1.1
Host: 47.120.78.228:8889
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

http请求其实本质是一行字符串, 尽管它打印出来很多行, 是因为它的子串是用\r\n分割的.因此可以发现http协议的序列化和反序列化是设计者自己实现的, 并不依赖其它第三方库(如json等).

接下来就介绍HTTP请求响应报文的格式:

1. 请求/响应行:

格式: [方法] + [url] + [版本] / [版本] + [状态码] + [状态码描述]

HTTP协议请求/响应结构的第一行被称为请求/响应行, 以空格为分隔符分割对应的三种属性

比如: 以请求行为例, GET / HTTP/1.1.

  • 其中GET是请求方法,
  • / 表示请求地址, 也就是我需要哪个目录下的文件, 这里就表示web根目录.
  • HTTP/1.1是协议版本, HTTP常用的有三个版本http/1.0、http/1.1和http/2.0, 最常用的是1.1

2. 请求/响应报头(Header): 

请求报头是由多个Key:Value结构构成的多行结构, 以冒号分割键值对, 每组属性之间使用\r\n分隔;

3. 空行: 表示Header部分结束.

4. 有效载荷(Body): 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性.

1.3.1 思考 

对于任何的协议, 我们都要考虑两个问题:

对于请求:

1. 报头和有效载荷如何分离的问题

2. 有效载荷如何交付的问题

问题1: 可以通过空行分离, 如上图的红色部分.  

问题2: 在 HTTP 协议中, 有效载荷最终是交给Web应用程序处理的, 所以在学习 HTTP 作为应用层协议时, 不需要再考虑它如何被上层解析, 因为 HTTP 本身已经是通信的顶层协议.

对于序列化和反序列化, 我们也要考虑两个问题:

对于请求:

1. http如何做到读到完整的报文

2. 如何对http进行反序列化

问题1, 需要分两步来解决:

1. HTTP只需要一直读取报文, 直到读到一个如上图所示的空行(红色),  就能明确知道我读完了http请求的报头字段, 此时接收方就具备了解析报头字段key:value字段的能力.

2. Http请求报头字段一定有一个属性Content-Length, 其内容表示有效载荷部分的长度是多少, 因此就可以得到完整的http报文.

问题2: http是自己实现了序列化和反序列化, 并且按行划分内容, 因此根据\r\n进行反序列化即可.

1.3.2 报文字段

请求行和响应行

http版本 

在介绍各个字段之前, 先来谈一下为什么请求和相应报文的头部都需要带上http协议的版本, 准确来说, 请求报文的版本是浏览器的http版本, 响应报文的版本是服务器http版本.

对于一个应用来说, 假如有功能需要更新, 它不可能让所有用户(比如几亿)同时更新, 而是先挑一部分用户(比如几万)更新, 假如这部分用户测试的结果很稳定则再陆续普及更新, 也就是灰度上线. 

所以客户端就可能有多种版本同时存在, 服务器根据客户端的版本提供合适的服务(响应报文)给客户端.

Http的版本有三种: 1.0 1.1 和 2.0

  • HTTP1.1中,默认支持长连接Connection: keep-alive),即在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟 
  • HTTP 1.0 浏览器与服务器只保持短暂的连接,每次请求都需要与服务器建立一个TCP连接服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。简单来讲,每次与服务器交互,都需要新开一个连接
  • HTTP2.0在相比之前版本,性能上有很大的提升,如添加了一个特性:多路复用,二进制分帧,首部压缩,服务器推送

面试官:说说 HTTP1.0/1.1/2.0 的区别? | web前端面试 - 面试官系列

 URL

1. 上面的请求中URL资源目录是/即web根目录, 其中默认访问web根目录, 规定: 访问/表示我们默认访问该网站的首页, 一般是 index.html, 也就是服务器把 / 转化为 /index.html

如何理解web根目录?

 就是服务器指定的一个已知目录, 未来所有的http资源都在该路径之下, 本质是Linux下的一个指定的目录.

举个例子, 假如该服务器下web根目录名称为wwwroot, 则对于资源/a/b/x.html的访问在本地会转换为/wwwroot/a/b/x.html

2. 这个请求中有一个.ico文件, 这其实是一个图标文件, 百度是一个熊掌形状.  

3. 在搜索框中输入:

可以看到URL被直接拼接了过去:

 用代码去解析一下对URL的处理过程:

httpServer.cc:

 这个httpserver的主要处理浏览器发来的http请求, 对其进行序列化然后解析(Parse)各个字段保存起来, 然后根据请求的内容做出不同的响应, 这里主要有 200 OK 和 404 Not Found. 主要的http协议实现部分在下面展开.

#include "HttpServer.hpp"
#include "HttpProtocol.hpp"
#include <iostream>
#include <fstream>
#include <memory>
#include <vector>
#include <errno.h>
#include <string.h>

std::string suffixToString(const std::string& suffix)
{
    std::string file_type;
    if(suffix == "html" || suffix == "htm")
        file_type = "text/html";
    else if(suffix == "jpg")
        file_type = "image/jpeg";
    else
        file_type = "unknown";
    return file_type;
}

std::string codeToDesc(int code)
{
    switch(code)
    {
        case 200:
            return "OK";
            break;
        case 404:
            return "Not Found";
            break;
        case 301:
            return "Found";
            break;
        case 307:
            return "";
            break;
    }
    return "";
}

std::string handlerRequest(const std::string& message)
{
    HttpRequest req;
    req.Deserialize(message);
    req.Parse();
    req.Debug();

    int code = 200;
    HttpResponse resp;

    std::string content = req.getNormalContent();
    if(content.empty())
    {
        code = 404;
        content = req.get404Content();
    }
    //1. 响应内容
    if(!content.empty())
    {
        resp.setCode(code);
        resp.setDesc(codeToDesc(code));
        resp.setContent(content);
        //2. 响应行
        resp.createRespLine();
        //3. 响应属性
        std::string s = ("Content-Length: " + std::to_string(content.size()));
        resp.addHeader(s);
        s = ("Content-Type: " + suffixToString(req.GetSuffix()));
        resp.addHeader(s);
        // std::cout << resp.Serialize();
        return resp.Serialize();
    }
    
    return "";
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        std::cout << "Usage: ./HttpServer port" << std::endl;
        exit(Usage_Err);
    }
    uint16_t port = std::stoi(argv[1]);
    // std::unique_ptr<TcpServer> tcpServer = std::make_unique<TcpServer>(port, handlerRequest);
    std::unique_ptr<HttpServer> tcpServer(new HttpServer(port, handlerRequest));
    tcpServer->Loop();
    return 0;
}

HttpProtocol.hpp: 

1. 这里具体实现了对Http请求和响应 的处理, HttpRequest中通过\r\n手动序列化提取出响应行, header和content.

2. Parseline借助stringstream以空格分离: Method URL 和 Content

3. 对于URL部分值得我们注意: 我们的http是超文本传输协议, 我们要去给浏览器响应文字图片音频等资源, 但是在响应之前我们这些资源都在哪里? 保存在在Linux服务器的web根目录下, 这个目录可以有各种类型的结构.

如果我们的请求中是一个web根目录, 我们在函数内部把它处理成./wwwroot/index.html; 其它情况就直接在./wwwroot后拼接即可.

4. 我们还要设计一个将指定文本读取为字符串的的函数, 利用seekg和tellg获取文本大小, 然后利用文件流的read函数将特定大小的文本内容读取到string或vector<char>底层的缓冲区中.

#pragma once
#include <sstream>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>

std::string lineSep = "\r\n";
std::string blankSep = " ";
std::string homepage = "index.html";
std::string webroot = "./wwwroot";
class HttpRequest
{
public:
    HttpRequest()
        : _spaceLine(lineSep), _path(webroot)
    {
    }

    void Deserialize(const std::string &request)
    {
        std::cout << "Raw HTTP Request:\n" << request << std::endl;
        int left = 0, right = 0;

        // 1. 解析请求行
        right = request.find(lineSep);
        if (right != std::string::npos)
        {
            _req_line = request.substr(left, right);
        }
        else
        {
            return; // 没有换行符,说明格式错误
        }
        // 2. 解析请求头
        left = right + lineSep.size();
        right = request.find(lineSep, left);
        while (right != std::string::npos)
        {
            std::string line = request.substr(left, right - left);
            if (line.empty()) // 空行表示请求头结束
            {
                left = right + lineSep.size(); // 确保 left 指向请求体的开头
                break;
            }

            _values.push_back(line);
            left = right + lineSep.size();
            right = request.find(lineSep, left);
        }
        // 3. 解析请求体
        if (left < request.size())
            _content = request.substr(left);
        else
            _content = "";
    }

    void ParseLine()
    {
        std::stringstream ss(_req_line);
        ss >> _method >> _url >> _version;
    }

    void ParseURL()
    {
        if (_url == "/")
        {
            _path += (_url + homepage); // ./wwwroot/index.html
        }
        else
        {
            int left = _url.find("?");
            if (left == std::string::npos)
            {
                _path += _url; // ./wwwroot/a/b/test.html
            }
            else
            {
                // 获取 URL 的路径部分(去掉查询参数)
                _path += _url.substr(0, left); // ./wwwroot/a/b/test.html

                // 解析查询参数部分
                std::string queryString = _url.substr(left + 1);
                int paramStart = 0;
                int paramEnd = queryString.find("&");

                // 处理所有参数
                while (paramStart != std::string::npos)
                {
                    if (paramEnd != std::string::npos)
                    {
                        // 提取当前的参数(key=value)
                        std::string param = queryString.substr(paramStart, paramEnd - paramStart);
                        _parameter.push_back(param);
                        paramStart = paramEnd + 1;
                        paramEnd = queryString.find("&", paramStart);
                    }
                    else
                    {
                        // 处理最后一个参数(没有&符号)
                        std::string param = queryString.substr(paramStart);
                        _parameter.push_back(param);
                        break;
                    }
                }
            }
        }
        // 调试输出
        // std::cout << "Resolved Path: " << _path << std::endl;
    }

    void ParseSuffix()
    {
        int pos = _url.rfind(".");
        if (pos != std::string::npos)
            _suffix = _url.substr(pos + 1);
        else
        {
            if (_url == "/")
                _suffix = "html";
            else
                _suffix = "unknown";
        }
    }

    void Parse()
    {
        // 分析请求行
        ParseLine();
        // 解析URL
        ParseURL();
        // 解析后缀
        ParseSuffix();
    }

    void DebugHttp()
    {
        std::cout << "_req_line: " << _req_line << std::endl;
        for (auto &line : _values)
        {
            std::cout << "---> " << line << std::endl;
        }
        std::cout << "_req_blank: " << lineSep << std::endl;
        std::cout << "_req_content: " << _content << std::endl;
    }

    std::string get404Content()
    {
        return getPathContent("./wwwroot/404.html");
    }

    std::string getNormalContent()
    {
        return getPathContent(_path);
    }

    std::string getPathContent(const std::string &path)
    {
        std::ifstream in(path, std::ios::binary);
        if (!in)
            return "";

        in.seekg(0, in.end);
        int filesize = in.tellg();
        in.seekg(0, in.beg);

        std::string content;
        content.resize(filesize);
        in.read((char *)content.c_str(), filesize);
        // std::vector<char> content(filesize);
        // in.read(content.data(), filesize);

        in.close();

        return content;
    }

    std::string GetPath() const
    {
        return _path;
    }

    std::string GetSuffix() const
    {
        return _suffix;
    }

private:
    // 按行分割
    std::string _req_line;
    std::vector<std::string> _values;
    std::string _spaceLine; // 没什么用,只是为了让格式清晰
    std::string _content;
    // 请求行属性
    std::string _method;
    std::string _url;
    std::string _version;
    std::string _path;
    std::string _suffix;
    std::vector<std::string> _parameter;
};

这样在浏览器访问服务器就可以得到响应了.

1. 请求一个不存在的资源:

2. 默认web根目录:

在一个典型的网页加载过程中, HTTP 请求会分为多个步骤. 首先, 浏览器发送请求获取主页面的 HTML 文件, 然后根据 HTML 中的资源引用(如图片、CSS、JavaScript 文件等)发出额外的请求.

再比如我们在这个页面点击登录, 提交html表单(form),  会把这里的name属性以name:value的形式通过参数传递给服务器, 而根据HTTP方法的不同, 参数在报文中的位置也不同, 所以下面要介绍HTTP方法:

 HTTP方法

我们上网的行为分为两种:

1. 获取资源, 实际上我们无时无刻一直都在获取资源 

2. 我们也可能向服务器传参--把我们的数据上传给服务器, 比如登录, 注册, 搜索等等, 通常是通过GET/POST配合HTML的表单完成的.

HTML的form的method属性默认是get

 我们最常见的方法就是GET和POST:

GET与POST 

GET:

  • 目的:通常用于获取资源, 也可以用于传递参数
  • 场景:适用于获取资源、查看数据、检索页面内容等操作, 通常是无副作用的(即不会修改服务器上的数据)
  • 示例:请求网页、获取图片或其他资源

POST:

  • 目的:用于向服务器提交数据, 通常用于创建或更新服务器上的资源
  • 场景:适用于提交表单数据、上传文件、创建或更新资源等操作. POST 会对服务器的数据状态产生影响, 通常是有副作用的
  • 示例:提交用户注册表单、上传图片文件、提交评论等

这两种方法的区别之一就是数据的传输方式:

GET方法

1. 数据通过 URL 传递, 即 URL 中的 ? 后面部分传递.

2. URL 长度有最大限制(通常约为 2048 个字符, 具体限制因浏览器和服务器而异). 因此, GET 不适合传输大量数据

POST方法

1. 数据通过请求体传递, 数据放在 HTTP 请求的 body 部分, 用户无法直接在 URL 中看到

2. 没有大小限制, 理论上, POST 请求的数据大小只受服务器配置的限制. 而不像 GET 方法那样受 URL 长度限制.

关于GET和POST的安全性与隐私性

其实GET和POST都不安全, 信息都是暴露在外的, 这些封装后的数据还可以通过抓包的方式被他人获取, 只是说POST相对于GET隐私性稍好一些. 而能保证数据安全的方法只有对数据进行加密, 使报文以密文的形式进行传输. 比如https协议才能实现真正的安全信息传输. 

其它方法

HEAD方法

  • 作用HEAD 请求与 GET 请求非常相似, 但服务器只会返回响应头, 而不返回实际的内容体. 用于获取响应的元数据(如内容长度、类型、修改时间等), 通常用于检查资源的存在性或获取文件的相关信息

PUT方法

  • 作用PUT 请求用于上传和替换指定资源. 如果资源存在, PUT 会替换该资源;如果资源不存在, PUT 会创建该资源.

DELETE方法

  • 作用: DELETE 请求用于删除服务器上的资源, 如删除某个用户或某个文件.

PATCH方法

  • 作用: PATCH 请求用于部分更新资源. 与 PUT 不同, PATCH 只更新资源的一部分而不是替换整个资源.

OPTIONS方法

  • 作用: OPTIONS 请求用于获取指定资源支持的 HTTP 方法, 或获取服务器的相关信息(OPTIONS 不要求服务器返回资源, 只返回允许的 HTTP 方法)

TRACE方法

  • 作用: TRACE 请求是诊断方法, 它允许客户端查看服务器接收到的请求数据, 服务器会原样返回请求数据

CONNECT方法

  • 作用: CONNECT 请求用于建立一个到目标资源的隧道连接, 通常用于 HTTP 代理服务器来建立加密的 HTTPS 连接.
状态码 与 状态码描述

状态码 与 状态码描述是HTTP响应报文响应行的内容:

1xx是信息性状态码表示请求已被接收, 正在处理,  比如服务器还没处理完请求, 又需要立即响应, 那么返回1xx表示请求已经被接收, 正在处理. (不常见)

  • 100 Continue:客户端应继续发送请求的剩余部分。
  • 101 Switching Protocols:服务器已理解客户端的请求,并同意根据请求切换协议(例如 HTTP/1.1 到 HTTP/2)。
  • 102 Processing:服务器正在处理请求,但尚未完成。

2xx表示请求已成功处理:

  • 200 OK:请求成功, 响应的正文中包含请求的内容(通常是 HTML、JSON 或图片等)

3xx表示重定向:

  • 301 Moved Permanently:请求的资源已经永久移动到新位置, 之后的请求应使用新 URL
  • 302 Found:请求的资源临时移动到新位置,客户端应继续使用原 URL 进行后续请求
  • 307 Temporary Redirect:与 302 相似, 但要求客户端必须使用相同的 HTTP 方法进行重定向请求

4xx表示客户端请求有错误, 通常是客户端发送的请求无效:

  • 403 Forbidden:服务器理解请求,但拒绝执行它,客户端无权访问资源
  • 404 Not Found:请求的资源不存在

5xx表示服务器在处理请求时出现了问题, 无法完成请求:

  • 500 Internal Server Error:服务器内部错误, 无法完成请求。
  • 502 Bad Gateway:服务器作为网关或代理时, 从上游服务器接收到无效的响应。
  • 503 Service Unavailable:服务器当前无法处理请求, 通常是由于过载或正在维护。
  • 504 Gateway Timeout:服务器作为网关或代理时, 未能及时从上游服务器获取响应。

 谈到响应报文的属性, 那就要实现一个对应的响应报文来说明了, 之前已经用到过, 这里展示出来说明一下结构.

class HttpResponse
{
public:
    HttpResponse()
    :_httpVersion("HTTP/1.1"),
    _code(200),
    _code_desc("OK")
    {
    }

    ~HttpResponse()
    {
    }

    std::string Serialize()
    {
        std::string resp;
        resp += _resp_line + lineSep;
        for(const auto& s : _resp_header)
            resp += s + lineSep;
        resp += lineSep;
        resp += _content;
        return resp;
    }

    void createRespLine()
    {
        _resp_line += _httpVersion + blankSep + std::to_string(_code) + blankSep + _code_desc;
    }

    void addHeader(const std::string& s)
    {
        _resp_header.push_back(s);
    }

    void setCode(int code)
    {
        _code = code;
    }

    void setDesc(const std::string& s)
    {
        _code_desc = s;
    }

    void setContent(const std::string& content)
    {
        _content = content;
    }
private:
    std::string _resp_line;
    std::vector<std::string> _resp_header;
    std::string _blankLine;
    std::string _content;

    std::string _httpVersion;
    int _code;
    std::string _code_desc;
};

之前已经演示过了200 OK 和 404 Not Found, 现在再来说明一下3xx系列的响应码:

1. 307 Temporary Redirect, 其作用是进行页面之间的跳转, 让用户跳转到目标的网页, 并且要求客户端在重定向时保持原始的 HTTP 方法不变.

重定向我们需要结合一个选项Location: 新的URL, 这里我们把code改为307, 添加一个Location字段: 

 浏览器访问我的http服务器之后, 直接跳转到了百度的首页:

2. 301 Permanent Redirect:会告诉客户端这是一个永久性的重定向, 并且客户端(比如浏览器)应该记住新的 URL. 对于 301,浏览器可能会自动将所有未来的请求都转发到新的 URL, 并且有时会改变请求方法(POST 会变成 GET).

301一般出现在网站的永久重构或迁移, 域名更改. 例如 你的网站 www.oldsite.com 迁移到 www.newsite.com,你可以对所有访问旧域名的请求返回 301 状态码, 将它们引导到新域名. 这对 SEO (搜索引擎优化)非常重要. 搜索引擎会理解这是一个永久性变更, 并将原 URL 的排名、权重等 SEO 信息转移到新 URL. 因为搜索引擎就意味着流量, 这样做不仅确保了访问者能正常访问新页面, 也避免了丢失原有页面的排名和流量。

3. 302 Found 表示资源临时被移动到新的 URL, 客户端应该继续使用原 URL 进行请求. 它适用于短期或临时性变动, 例如页面维护、负载均衡或临时内容替换等场景. 302 重定向不会强制保持原始方法. 客户端可能会将 POST 请求自动转换为 GET

 1.3.3 报头字段

目前我们接触过三个报头字段, 分别是:

1. Content-Lenghth: 表示响应体的大小, 单位是字节(bytes).它告诉客户端返回的数据有多大。

2. Content-Type: 表示响应体的媒体类型(即 MIME 类型)它告诉客户端如何解析和处理响应体的数据, 在我们的服务器代码中.

如果不指明Content-Type, 假如服务器返回的是一个图片文件, 但没有指定 Content-Type: image/jpeg, 浏览器可能无法正确显示图片, 或者可能以文本的方式打开它, 导致显示为乱码或错误内容. 但也可能可以正确显示, 所以之前我们的代码中对于不同的文件后缀标明了此属性.

3. Location: Location 头主要出现在 3xx 系列的响应中, 它指示客户端需要重定向到的新的 URL. 当状态码是 3xx 时, 客户端会根据 Location 头重新发起请求.

此外还有:

4. Host: 客户端请求的目标主机名(域名)(一般填的都是目标服务器的ip地址)

5. User-Agent: 声明用户(客户端)的操作系统和浏览器版本信息;

6. referer: 当前页面是从哪个页面跳转过来的; 可用于网络安全, 如果某个服务器想禁止用户从某个网址跳转而来, 如果referer是禁止的网址, 则可以进行对其处理(比如丢弃).

7. Cookie和Set-Cookie下面介绍.

1.4 cookie和session

首先需要说明两个特点: http是无连接, 无状态

1.4.1 无连接与无状态

无连接

无连接意味着每个 HTTP 请求和响应之间没有持续的连接. 每个请求都在自己的 TCP 连接中完成, 之后这个连接会立即关闭. 也就是说, 客户端发送请求到服务器, 服务器响应请求后就关闭了这个连接。客户端和服务器不需要保持一个持续的会话。注意这里的无连接是HTTP协议本身的行为, 而不是底层的 TCP 连接. 

具体来说是: 1. 在HTTP请求发送时, 客户端会与服务器建立一个新的TCP连接. 2. 在请求被处理完并响应返回后,这个连接就会关闭, 没有任何进一步的保持或持久化的动作. 3. 如果客户端需要发送另一个HTTP请求, 它会重新建立一个新的TCP连接, 完全独立于之前的请求

假设你访问一个网页:

第一次请求

  • 浏览器(客户端)通过 TCP 连接向服务器发送 HTTP 请求,请求页面 index.html
  • 服务器返回响应,并且在传输完毕后,TCP连接关闭。

第二次请求

  • 假设页面中有图片(比如 image.jpg)浏览器再发送另一个 HTTP 请求
  • 为了请求这个图片, 浏览器会再次通过 TCP 连接发送请求, 获取响应, 并在数据传输完成后关闭连接.

在这个过程中, 每个 HTTP 请求/响应周期都是独立的, 而不是长期保持一个持久的连接. 这就是“无连接”特性的体现.

无状态

无状态意味着每个 HTTP 请求都是独立的, 并且服务器不保存任何关于客户端请求的状态信息. 每个请求的信息必须在请求本身中提供, 服务器在处理一个请求时不会依赖之前的请求.

例如, 当你发送一个 HTTP 请求向服务器请求一个网页时, 即使你之前已经请求过这个网页, 服务器也不会记住你上次请求的内容. (但不排除有些情况下浏览器会缓存该网页的图片和视频到本地, 方便下次请求时减少流量, 不过这都是客户端的行为, 并非HTTP协议的行为)

 但比较反直觉的例子是, 当我们用浏览器上网时, 登录某个网站请求网站的资源时, 我们都需要注册/登录, 但是往往我们登录过一次之后, 下次登录该网站就不需要重复登陆了, 这为什么是有状态的行为呢? 通俗来说是, 为什么网站在我登录时一直认识我呢?

这样其实恰恰是更合理的行为, 我们并不希望在每一次资源请求时都登录一遍, 这样体验太差, 但这是如何实现的呢?

正常来说如果你在一次请求中通过GET/POST方法提交给服务器我的用户名和密码信息, 但服务器并不会记住你已经登录过, 下一次请求仍然需要重新提供身份验证信息. 这里就要使用Cookie 或 Session!

1.4.2 Cookie

使用cookie技术后, 在我们登录成功一次之后, server会在响应报文中添加若干个包含了用户信息的Set-Cookie字段, 浏览器会把这些内容保存起来. 当下次进行资源请求时, 浏览器会自动在请求报文中添加Cookie字段, 服务器会自动进行认证! 

cookie也分为内存级文件级:

内存级Cookie: 内存级cookie指将cookie保存到浏览器进程的内存上下文中. 当浏览器被关闭时,进程结束, 保存的信息也失效了, 重新打开浏览器后还需要重新登录. 
文件级Cookie: 文件级cookie指将cookie保存到浏览器自己配置的保存cookie的文件(磁盘等存储设备上). 无论浏览器怎么打开关闭, 只要cookie文件没有被清理, 则每次发送HTTP请求时, 浏览器都会从该文件中读取信息并加到请求报头中. (此外, 我们也可以给cookie设有效时间, 7天 30天等, 这里暂不展开.)

大部分情况下的Cookie都是文件级别的, 而且这些文件是可以从我们的计算机中找到的

代码测试

我们在代码中添加Set-Cookie字段, 然后在浏览器中访问, 可以看到Cookie被 

 可以看到浏览器记录下了Cookie信息:

查看客户端的请求, 正如之前所示, 第一次请求时无cookie, 往后的每一次请求中报文都自动携带了Cookie字段:

1.4.3 Session

但是我们也遇到或听说过账号被盗的情况, 有的木马病毒会搜集电脑的cookie文件, 从而可以以被盗者的身份去登录同一个网站享受同一个服务(比如看付费电影), 甚至伪装为我们的身份进行一些非法操作.

但是关键在于, 即使我们的cookie信息被盗, 他以我的身份去看电影, 访问好友列表等我还暂可以接受, 但是cookie在我们本地客户端存放的是实实在在的真实隐私数据(账号密码等), 所以就有了session.

什么是session:

服务器给每个用户在服务器创建一个Session文件储存信息, 每个session都有一个唯一的sessionid, 此后服务器不再把真实信息设置进cookie中, 而是将这个Sessionid返回给用户, 此时用户浏览器的Cookie中保存的就是这个id值而不再是用户真实信息的.

虽然 Cookie 中的 Session ID 可以被盗取, 从而使攻击者冒充用户进行认证, 但服务器可以采取多种安全措施来降低风险, 尽管无法完全杜绝此类攻击:

比如:

  • 使用HTTPS加密, 攻击者无法直接读取内容。
  • IP 地址和设备信息校验
  • Cookie 设置 HttpOnlySecure 属性
  • 使用 CSRF Token保护重要操作(如修改密码、转账等), 即使攻击者获得了 Session ID,也无法伪造请求
  • 多因素认证(如短信验证码、邮件验证等), 即使攻击者盗取了 Session ID, 也需要通过其他身份验证才能进行操作
  • 设置 Session 过期时间: 短时间 Session 过期, 定期重新认证

此外还有很多的安全策略.

1.4.4 WebView

WebView 是一个内嵌的浏览器引擎, 用于在应用内部渲染网页内容. 它的原理类似于 Chrome, Safari, Edge 等浏览器, 只不过它是在 App 里运行. 既然 WebView 是浏览器引擎, 它当然使用 HTTP/HTTPS 协议加载网页, 就像普通浏览器访问网页一样. 

而WebView 直接复用 App 内的 Cookie / Session,  因此如果攻击者劫持了 WebView 的 HTTP 请求, 就可能获取用户的 Session ID, 从而冒充用户访问服务器.

当用户在 WebView 里访问网页时, 背后会发生一个 完整的 HTTP 交互, 示例如下:

用户访问 QQ 登录页面

假设用户打开 WebView, 访问 https://qq.com/login,WebView 发送 HTTP 请求

POST /login HTTP/1.1
Host: www.qq.com
User-Agent: Mozilla/5.0 (Linux; Android 10; WebView)
Content-Type: application/x-www-form-urlencoded
Content-Length: 64

username=johndoe&password=supersecretpassword

服务器响应一个包含 Session ID 的 HTTP 响应:

HTTP/1.1 200 OK
Set-Cookie: sessionid=1234567890abcdef; Path=/; HttpOnly; Secure
Content-Type: text/html; charset=UTF-8
Content-Length: 1024

攻击者在用户和服务器之间劫持流量(例如通过 Fiddler等工具), 并通过中间人攻击获取到 sessionid, 构造一个新的 HTTP 请求, 冒充被盗用的用户, 随后服务器向攻击者返回用户数据, 获取用户信息:

GET /user/profile HTTP/1.1
Host: www.qq.com
User-Agent: Mozilla/5.0 (Linux; Android 10; WebView)
Cookie: sessionid=1234567890abcdef  # 恶意用户使用盗取的 sessionid


http://www.kler.cn/a/560823.html

相关文章:

  • Android Audio实战——音频相关基础概念(附)
  • CentOS的ssh复制文件
  • 强化学习笔记(一)
  • 游戏引擎学习第120天
  • 鸿蒙-canvas-刮刮乐
  • 122页PPT!企业数字化IT架构蓝图规划设计方案:总体框架、IT治理全景图、IT治理管控框架、蓝图架构、演进路线、实施治理
  • 计算机网络与通讯知识总结
  • springcloud跟dubbo有什么区别
  • 设计模式教程:备忘录模式(Memento Pattern)
  • Grok 3.0 Beta 版大语言模型评测
  • 修改与 Git 相关的邮箱
  • 自动驾驶两个传感器之间的坐标系转换
  • imutils opencv-python 的一些操作
  • [杂学笔记]工厂模式、多态、内存空间区域划分、cp指令破坏软连接问题、UDP如何实现可靠传输、滑动窗口的原理、进程与线程、线程之间的通信
  • Java数据结构第十三期:走进二叉树的奇妙世界(二)
  • 发现问题 python3.6.13+django3.2.5 只能以asgi启动server 如何解决当前问题
  • Linux中的date命令
  • JavaSE学习笔记26-集合(Collection)
  • 【DeepSeek-R1背后的技术】系列十一:RAG原理介绍和本地部署(DeepSeekR1+RAGFlow构建个人知识库)
  • 数据结构:哈希表(unordered_map)