Linux网络的应用层协议HTTP

目录

1、HTTP协议

[1.1 HTTP的请求与响应格式](#1.1 HTTP的请求与响应格式)

[1.2 URL](#1.2 URL)

[1.3 HTTP的常用请求方法](#1.3 HTTP的常用请求方法)

[1.3.1 GET](#1.3.1 GET)

[1.3.2 POST](#1.3.2 POST)

[1.4 HTTP的常见报头](#1.4 HTTP的常见报头)

[1.5 HTTP的常用状态码及其描述](#1.5 HTTP的常用状态码及其描述)

[1.6 HTTP协议的介绍](#1.6 HTTP协议的介绍)

[2、Hello Http](#2、Hello Http)

[2.1 大致思路](#2.1 大致思路)

[2.2 Http.hpp](#2.2 Http.hpp)

[2.3 Util.hpp](#2.3 Util.hpp)

[2.4 Main.cc](#2.4 Main.cc)

[2.5 Tcpserver.hpp](#2.5 Tcpserver.hpp)

[2.6 示例及完整代码](#2.6 示例及完整代码)


1、HTTP协议

1.1 HTTP的请求与响应格式

  • HTTP版本不同的版本 提供 不同的服务
  • HTTP协议如何做到报头和有效载荷的分离 ?以空行为分隔符。
  • HTTP协议如何序列化和反序列化成熟的 HTTP协议,不依赖于任何第三方库直接序列化为字节流再反序列化 。为什么不是字符流 ?因为**'\0'可能不是结尾**。
  • 传输 交给传输层TCP/UDPHTTP应用层 ,只是解析请求和应答

1.2 URL

  • URL,Uniform Resource Locator,统一资源定位符。
  • HTTP是基于TCP的协议;HTTPSHTTP经过加密 的协议;HTTPS下一节讲解
  • 域名 :对应着服务器的IP
  • 端口号HTTP默认80****端口号HTTPS默认443****端口号
  • 路径定位服务器里的 "具体文件 / 功能 "。/s是百度 "搜索功能" 的专属路径标识。
  • 参数 (可选):向服务器传递 "具体需求 "。wd 是百度 "搜索关键词 " 的参数名天气 就是你要搜索的内容
  • 注意:
  1. 浏览器不写 https://www.端口号 ,会自动补全
  2. / : ?,这样的符号,被当作是特殊字符如需使用 ,需要进行转义UrlEncode编码/UrlDecode解码

1.3 HTTP的常用请求方法

  • 访问资源(数据),无非就是从远端拿 (GET)数据将本地数据上传 (POST)到远端 。其实就是I/O
1.3.1 GET
  • 用于请求URL指定的资源
  • 可以通过URL的方式提交参数,进行交互式的动态服务。
  • 注意:
  • URL中,参数在?的后面 ,以key=value的显示,可用&连接多个参数,但是不建议太长。
  • 因为客户端发起请求前 ,已经通过 TCP 连接到了服务器的 IP 和端口 ,所以请求报文中的URL只剩下了路径+参数
1.3.2 POST
  • 向服务器提交数据让服务器对数据进行处理

  • 可以通过请求正文的方式提交参数不显示 ,可以传长数据

  • 注意:

  • GET,POST,无论显不显示参数,都不安全,因为,如Fiddler,可以抓取报文信息。所以HTTPS,进行加密。

1.4 HTTP的常见报头

  • Content-Type: 数据类型 (text/html等)。根据请求资源的后缀指定数据类型,才能正确显示。
  • Content-Length: 正文的长度。知道了有效载荷的长度。
  • Connection: 是否支持长连接 (一条连接,可以发更多的请求)。keep-alive支持长连接close****不支持长连接
  • Host: 客户端告知服务器 ,所请求的资源是在哪个主机的哪个端口上
  • User-Agent: 声明用户的操作系统和浏览器版本信息
  • Referer: 当前页面是从哪个页面跳转过来的
  • Location: 搭配3xx状态码(重定向 )使用,告诉客户端接下来要去哪里访问
  • Cookie: 用于在客户端存储少量信息session****对这些信息进行了加密 ,但是整体的加密程度有限。如:账号密码经过session加密放到Cookie中,但是黑客依然可以使用这些加密的信息进行登录。

1.5 HTTP的常用状态码及其描述

  • 1xx:信息性状态码(请求已接收,继续处理)

    • 100 Continue:服务器已接收请求头部,允许客户端继续发送请求体(常用于客户端发送大型数据前的 "预请求",如文件上传)。
  • 2xx:成功状态码(请求已被成功处理)

    • 200 OK:最常见的成功状态,请求被正常处理并返回响应体(如网页、接口数据等)。
  • 3xx:重定向状态码(需要客户端进一步操作)

    • 301 Moved Permanently:永久重定向。请求的资源已被永久迁移到新地址,客户端应更新本地缓存的 URL(如域名更换,旧域名跳转到新域名)。
    • 302 Found:临时重定向。资源临时迁移到新地址,客户端下次请求仍应使用原 URL(如网站维护时临时跳转到通知页)。
  • 4xx:客户端错误状态码(请求存在错误,服务器无法处理)

    • 400 Bad Request:请求格式错误(如参数缺失、格式不正确,服务器无法解析)。 401 Unauthorized:未认证。请求需要身份验证(如登录),但客户端未提供或验证失败(常见于需要登录的页面)。
    • 401 Unauthorized(未认证):请求需要身份验证(如登录),但客户端未提供有效凭证(如 token 过期、未携带 cookie)。场景:未登录时访问需要权限的接口(如/api/orders),服务器返回 401,提示用户登录。
    • 403 Forbidden:服务器拒绝处理。客户端已认证,但没有权限访问该资源(如普通用户访问管理员后台)。 404 Not Found:最常见的错误之一,请求的资源不存在(如网址输错、页面已删除)。
    • 404 Not Found:服务器无法找到请求的资源(URL 不存在)。
  • 5xx:服务器错误状态码(服务器处理请求时出错)

    • 一般不设置错误状态码 ,因为企业不能让别人"乘虚而入"
  • 注意:

  • 状态码 :必须严格遵循标准,不能自定义

  • 状态描述可以自定义 ,但建议遵循标准

  • 为什么浏览器不显示状态码和状态描述?普通用户不需要关心 "404""Not Found" 这些技术符号,浏览器会把它们翻译成 "页面加载成功""页面找不到" 等易懂信息;而开发者可以通过工具随时查看原始的状态码和描述,用于调试问题。

1.6 HTTP协议的介绍

  • HTTP为了获取"资源"而采取的协议
  • HTTP,HyperText Transfer Protocol, "超文本传输协议"。因为正文传的是字节,显示器以对应的数据类型(Content-Type)显示。
  • HTTP无连接 (传输层TCP来连接,HTTP是应用层),无状态(不保存客户端的信息,所以报头里使用了Cookie保存了一些信息)。

2、Hello Http

2.1 大致思路

  • 浏览器 作为客户端向服务器 (myhttp )发起请求服务器 (myhttp )向浏览器发送应答
  • 客户端 (浏览器)默认发送**'/'** ,访问首页 。如果访问其他页面 ,就临时重定向到404页面。后续可以添加更多服务。

2.2 Util.hpp

  • 一些工具类的方法。
cpp 复制代码
#pragma once

#include <fstream>
#include <string>

class Util
{
public:
    static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep_line /*"\r\n"*/)
    {
        auto pos = bigstr.find(sep_line);
        if (pos == std::string::npos)
            return false;

        *out = bigstr.substr(0, pos);
        bigstr.erase(0, pos + sep_line.size());
        return true;
    }

    static int FileSize(const std::string &file_name)
    {
        // 按文本方式读, 读到'\0'可能会结束, 所以按照二进制的方式读
        std::ifstream in(file_name, std::ios::binary);
        if (!in.is_open())
            return -1;

        in.seekg(0, in.end);        // 将文件读指针移动到文件末尾
        int file_size = in.tellg(); // 获取文件的字节数

        in.seekg(0, in.beg); // 将指针移回文件开头
        in.close();
        return file_size;
    }

    static bool ReadFileContent(const std::string &file_name, std::string *out)
    {
        int file_size = FileSize(file_name);
        if (file_size > 0)
        {
            std::ifstream in(file_name, std::ios::binary);
            if (!in.is_open())
                return false;

            out->resize(file_size);
            in.read(out->data(), file_size);  // C++11后 data() 可返回 char*(非const)

            in.close();
            return true;
        }

        return false;
    }
};

2.3 Http.hpp

  • 如何读到完整的请求?
    1. 空行 -> 读取到请求行 +请求报头
    1. 请求报头 中的Content-Length -> 请求正文
cpp 复制代码
#pragma once

#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include "Log.hpp"
#include <string>
#include <unordered_map>

const static std::string sep_space = " ";
const static std::string sep_line = "\r\n";
const static std::string sep_kv = ": ";

const static std::string wwwroot = "./wwwroot";
const static std::string home_page = "/home.html";
const static std::string page_404 = "/404.html";

class HttpRequest
{
public:
    void ParseReqLine(std::string &req_line)
    {
        // GET / HTTP/1.1
        std::stringstream ss(req_line);
        ss >> _method >> _url >> _http_version;
    }

    bool Deserialize(std::string& req_str)
    {
        // 请求行
        std::string req_line;
        bool res = Util::ReadOneLine(req_str, &req_line, sep_line);
        if(!res)
            return false;
        ParseReqLine(req_line);
        LOG(LogLevel::DEBUG) << "reqline: " << req_line;

        if(_url == "/")
            _url = wwwroot + home_page; // "./wwwroot/home.html";
        else 
            _url = wwwroot + _url; // "./wwwroot/..."

        // 假设没有?后面的参数...

        LOG(LogLevel::DEBUG) << "_method: " << _method;
        LOG(LogLevel::DEBUG) << "_url: " << _url;
        LOG(LogLevel::DEBUG) << "_http_version: " << _http_version;

        // 请求报头 请求正文 ...

        return true;
    }

    std::string GetUrl()
    {
        return _url;
    }

private:
    std::string _method;
    std::string _url;
    std::string _http_version;

    std::unordered_map<std::string, std::string> _headers;

    std::string _blank_line;

    std::string _text;
};

class HttpResponse
{
public:
    HttpResponse()
    :_blank_line(sep_line)
    ,_http_version("HTTP/1.1")
    {}

    // 实现: 成熟的http,应答做序列化,不要依赖任何第三方库!
    std::string Serialize()
    {
        std::string status_line = _http_version + sep_space + std::to_string(_code) + sep_space + _code_desc + sep_line;
        std::string headers;
        for(auto& header : _headers)
        {
            headers += header.first + sep_kv + header.second + sep_line;
        }

        return status_line + headers + _blank_line + _text;
    }

    void SetCode(int code)
    {
        _code = code;
        switch(code)
        {
            case 200:
                _code_desc = "OK";
                break;
            case 301:
                _code_desc = "Found";
                break;
            case 404:
                _code_desc = "Not Found";
                break;
            default:
                break;
        }
    }

    void SetHeaders(const std::string& key, const std::string& value)
    {
        auto it = _headers.find(key);
        if(it == _headers.end())
            _headers.insert({key,value});
    }
    
    // 默认访问的是.htm/.html文件

    void SetTargetFile(const std::string& file)
    {
        _target_file = file;
    }

    void SetText(const std::string text)
    {
        _text = text;
    }

    bool MakeResponse(uint16_t port)
    {
        // 浏览器要访问图标, 忽略他
        if(_target_file == "./wwwroot/favicon.ico")
        {
            LOG(LogLevel::INFO) << "用户请求图标: " << _target_file << "忽略他";
            return false;
        }
        
        bool res = Util::ReadFileContent(_target_file, &_text);
        if(!res)
        {
            LOG(LogLevel::WARNING) << "没有文件: " << _target_file << " , 404";

            SetCode(302); // 没有这个文件, 重定向到404

            SetHeaders("Location","http://124.221.189.63:"+std::to_string(port)+"/404.html");
        }
        else
        {
            LOG(LogLevel::WARNING) << "读取文件: " << _target_file; 

            SetCode(200); // 成功

            int file_size = Util::FileSize(_target_file);
            SetHeaders("Content-Length", std::to_string(file_size));
        }

        return true;
    }
    
private:
    std::string _http_version;
    int _code;
    std::string _code_desc;

    std::unordered_map<std::string, std::string> _headers;

    std::string _blank_line;

    std::string _text;

    // 其他属性
    std::string _target_file;
};

class Http
{
public:
    Http(uint16_t port)
    :_tsvrp(std::make_unique<TcpServer>(port))
    ,_port(port)
    {}

    void HandleHttpRequest(std::shared_ptr<Socket>& sockfd, const InetAddr& client)
    {
        LOG(LogLevel::DEBUG) << "收到新连接,准备读取请求"; 
        
        std::string http_req;
        int n = sockfd->Recv(&http_req); // 大概率一次读到完整的请求报文
        if(n > 0)
        {
            std::cout << "##########################" << std::endl;
            std::cout << http_req;
            std::cout << "##########################" << std::endl;

            HttpRequest req;
            HttpResponse resp;

            req.Deserialize(http_req);
            resp.SetTargetFile(req.GetUrl());
            if(resp.MakeResponse(_port))
            {
                sockfd->Send(resp.Serialize());
            }
        }
    }

    void Start()
    {
        _tsvrp->Start([this](std::shared_ptr<Socket>& sockfd, const InetAddr& client){
            this->HandleHttpRequest(sockfd, client);
        });
    }

private:
    std::unique_ptr<TcpServer> _tsvrp;
    uint16_t _port;
};

2.4 Main.cc

cpp 复制代码
#include "Http.hpp"
#include <memory>

// ./myhttp server_port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " server_port" << std::endl;
        exit(USAGE_ERROR);
    }

    Enable_Console_Log_Strategy();

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

    std::unique_ptr<Http> httpsvrp = std::make_unique<Http>(server_port);
    httpsvrp->Start();
    
    return 0;
}

2.5 Tcpserver.hpp

cpp 复制代码
#pragma once

#include "Common.hpp"
#include "Log.hpp"
#include <memory>

using namespace LogModule;

const static int default_sockfd = -1;
const static int default_backlog = 16;

class Socket
{
public:
    virtual void SocketOrDie() = 0; // = 0, 不需要实现 
    virtual void BindOrDie(uint16_t port) = 0;
    virtual void ListenOrDie(int backlog) = 0;
    virtual void ConnectOrDie(std::string &server_ip, uint16_t server_port) = 0;
    virtual std::shared_ptr<Socket> Accept(InetAddr* client) = 0;
    virtual int Recv(std::string* out) = 0;
    virtual int Send(const std::string& in) = 0;
    virtual void Close() = 0;
public:

    void BuildTcpServer(uint16_t port)
    {
        SocketOrDie();
        BindOrDie(port);
        ListenOrDie(default_backlog);
    }

    void BuildTcpClient(std::string &server_ip, uint16_t server_port)
    {
        SocketOrDie();
        ConnectOrDie(server_ip,server_port);
    }
};

class TcpSocket : public Socket
{
public:
    TcpSocket(int sockfd = default_sockfd)
        : _sockfd(sockfd)
    {
    }
    virtual void Close() override
    {
        if (_sockfd != default_sockfd)
            ::close(_sockfd); // ::表示调用 全局作用域 中的 close 函数
    }
    virtual void SocketOrDie() override
    {
        _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(SOCKET_ERROR);
        }
        LOG(LogLevel::INFO) << "socket success, socket: " << _sockfd;
    }

    virtual void BindOrDie(uint16_t port) override
    {
        InetAddr local(port);
        int n = ::bind(_sockfd, CONST_CONV(local.Addr()), local.AddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(BIND_ERROR);
        }
        LOG(LogLevel::INFO) << "bind success, socket: " << _sockfd;
    }

    virtual void ListenOrDie(int backlog) override
    {
        int n = ::listen(_sockfd, default_backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error!";
            exit(LISTEN_ERROR);
        }
        LOG(LogLevel::INFO) << "listen success, sockfd: " << _sockfd;
    }

    virtual void ConnectOrDie(std::string &server_ip, uint16_t server_port) override
    {
        InetAddr server(server_ip, server_port);
        int n = ::connect(_sockfd, CONST_CONV(server.Addr()), server.AddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "connect error!";
            exit(CONNECT_ERROR);
        }
        LOG(LogLevel::INFO) << "connect success, sockfd: " << _sockfd;
    }

    virtual std::shared_ptr<Socket> Accept(InetAddr* client) override
    {
        std::cout << std::endl;

        struct sockaddr_in addr;
        socklen_t len = sizeof(addr);
        int fd = ::accept(_sockfd, CONV(addr), &len);
        if (fd < 0)
        {
            LOG(LogLevel::WARNING) << "accept failed";
            return nullptr;
        }
        client->SetAddr(addr);
        LOG(LogLevel::INFO) << "accept success, client: " << client->StringAddr();

        return std::make_shared<TcpSocket>(fd); // 这个server的sockfd就可以调用Recv和Send方法。
    }

    virtual int Recv(std::string* out) override
    {
        char buf[1024*8];
        ssize_t n = ::recv(_sockfd,buf,sizeof(buf)-1,0);
        if(n > 0)
        {
            buf[n] = 0;
            *out += buf; // += 可能要不断的读 
        }

        return n;
    }

    virtual int Send(const std::string& in) override
    {
        return ::send(_sockfd,in.c_str(),in.size(),0);
    }

private:
    int _sockfd; // 既可以是listen_sockfd,也可以是sockfd,复用代码。
};

2.6 示例及完整代码

  • 示例:
  • 默认 请求首页 '/',还会请求图标 '/favicon.ico'(忽略)。获取首页时,如果首页中有n个资源,会再次发起n次请求。注意资源有类型,服务器指定Content-Type,浏览器才能正确显示。当然现在的浏览器已经非常智能了,会自动识别。
  • 完整代码:Http
  • 注意:
  • 有些服务器没有开放一些端口 (如:8080),我用的是腾讯云 ,在设置了防火墙模板 (开放一些端口),应用在我的实例上 ,但是会覆盖之前的模板,所以如果ssh连不上了,就再开放22端口
  • 可以通过w3school,学习一些前端知识。
相关推荐
大数据张老师3 小时前
数据结构——冒泡排序
数据结构·算法·排序算法·1024程序员节
susu10830189113 小时前
FAT32/VFAT 文件系统不支持 Linux 文件权限,cp文件总是异常
linux·运维·服务器
赵_|大人3 小时前
Ubuntu开启SSH
1024程序员节
xie_zhr3 小时前
【PB案例学习笔记】-46在数据窗口中编辑数据
数据库·his·1024程序员节·干货分享·pb·powerbuilder
七夜zippoe3 小时前
压缩与缓存调优实战指南:从0到1根治性能瓶颈(六)
1024程序员节
Web3_Daisy3 小时前
冷换仓的隐性代价:从安全策略到地址信誉体系的重新思考
大数据·安全·web3·区块链·比特币·1024程序员节
狡猾大先生3 小时前
ESP32S3-Cam实践(OLED表情动画-手搓)
笔记·1024程序员节
絔离3 小时前
Linux下查看系统启动时间、运行时间
linux·运维·服务器
lpfasd1233 小时前
第三章-Tomcat请求处理原理
1024程序员节