网络 :HTTP

发布于:2025-06-25 ⋅ 阅读:(23) ⋅ 点赞:(0)

一、预备知识

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

1.1 认识URL

平时我们俗称的 “网址” 其实就是说的 URL
在这里插入图片描述

1.2 encode和urldecode

像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出。但是,某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。如下:
在这里插入图片描述

转义的规则如下:

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

urldecode就是urlencode的逆过程;

二、HTTP协议格式

HTTP 协议由 Request 请求 和 Response 响应 两部分组成

2.1.请求(Request)格式

从宏观角度来看,HTTP 请求 分为这几部分:

  • 请求行,包括请求方法(GET / POST)、URL、协议版本(http/1.0 http/1.1 http/2.0)
  • 请求报头,表示请求的详细细节,由多组 k: v 结构所组成
  • 空行,区分报头和有效载荷
  • 有效载荷(可以没有)
    在这里插入图片描述

在 HTTP 协议中是使用 \r\n 作为 分隔符 的。

2.2 响应(Response)格式

从宏观角度来看,HTTP 响应 分为这几部分:

  • 状态行,协议版本、状态码、状态码描述
  • 响应报头,表示响应的详细细节,由多组 k: v 结构所组成
  • 空行,区分报头和有效载荷
  • 有效载荷,即客户端请求的资源

在这里插入图片描述

三、HTTP协议内容

3.1 HTTP的请求方法

在这里插入图片描述
请求方法有很多种,但是 GETPOST 是最常用的方法。
都是发送一个请求给服务端,两者的区别是什么呢?

  • Get 请求是 HTTP 协议中的一种请求方法,通常用于从服务器获取资源。使用 Get 请求时,参数会附加在 URL 的末尾,多个参数之间用 & 符号分隔。
  • Post 请求是 HTTP 协议中的一种请求方法,通常用于向服务器提交数据,或者创建新的资源。使用 Post 请求时,数据会放在请求体(body)中传输,而不是暴露在 URL 里。

3.2 HTTP的状态码

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

重定向状态码

当浏览器(客户端)访问的目标网站地址发生改变时,浏览器会返回 3xx 重定向错误码,用于引导浏览器访问正确的网址,常见的重定向状态码如下:

  • 永久重定向:301、308
  • 临时重定向:302、303、307
  • 其他重定向:304

最具有代表性的重定向状态码为 301 和 302

3.3 协议版本

  • http/1.0是短连接:一次请求响应一个资源,关闭连接。
  • http/1.1是长连接:建立一个TCP连接,可以发送和返回多个http的request和response。

报文信息中有一个Connection: keep-alive的信息,表示的是服务端和客户端都是长连接。
在这里插入图片描述

3.4 HTTP请求报头属性

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

Cookie属性

在HTTP协议中,Cookie是一种由服务器发送到客户端浏览器的小型文本数据,用于在用户访问网站时存储状态信息。它允许服务器识别用户会话、保存用户偏好或跟踪用户行为,从而提供个性化的Web体验。Cookie通过HTTP头(如Set-Cookie)传输,并在后续请求中由浏览器自动发送回服务器。

在这里插入图片描述

cookie有文件级和用户级。当cookie文件放到文件级中的时候,就是放到磁盘中了,所以下次打开浏览器依旧可以免登录了。而设置了自动消除时间,cookie就会自动消亡了。内存级的则浏览器不会免登录。

http协议是无状态的,http对登录用户的会话保持功能,就比如我们访问浏览器登录一次以后隔10分钟再访问一次不用登录了。
在这里插入图片描述

这种情况cookie被盗取并且个人信息泄露。

session

那么这就存在安全问题,其他人只要得到我们的Cookie就可以登录我们的账号了??
为了应对这种问题而有了一个session方法。

  • 这种做法是将客户端信息统一放到服务端去进行管理,这样子个人信息泄露的风险比较小,但仍旧避免不了cookie被盗取的风险。而为了避免风险,那么就需要服务端那边对session id进行管理来减少风险。

  • 当我们第一次登录网站后,服务器会将与用户相关的信息打包成一个session文件,并且为这个文件分配一个独有的session ID,将这个session ID返回到Cookie中。

当用户再次登录网站后则通过这个session ID来确认用户的身份,即使如此还是存在安全问题的,其他人拿到这个ID也可以用你的身份登录网站,但是他没办法直接拿到用户的个人信息,不过还是存在风险。

服务器可以制定安全策略,识别是否为异常登录

  • IP比对:识别登录用户的IP在短时间内是否发生了改变
  • 行为检测:识别用户是否存在异常信息,比如QQ突然大面积发生消息、添加好友

当服务器判定异常登录后,就会释放服务器中存储的 session id,这就意味着原本的 session id 失效了,需要重新输入密码登录

  • 如果是用户,重新使用 账号&密码 登录后,获取服务器重新生成的 session id 即可
  • 其他人则无法登录,因为没有 账号&密码

四、一个简易的http服务器

Log.hpp 日志

#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// PrintMethod
#define Screen 1
#define Onefile 2
#define Muchfile 3

// leve,指的是日志等级,等级不同处理的方式也不同
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define LogFile "log.txt"
class Log
{
public:
    Log()
    {
        path = "./log/";
        _PrintMethod = Screen;
    }

    // 用户指定打印方式
    void AppontPrint(int PrintMethod)
    {
        _PrintMethod = PrintMethod;
    }

    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    void printLog(int level, const std::string &logtxt)
    {
        switch (_PrintMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Muchfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }

    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string filename = path + logname;
        int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if(fd < 0)
        {
            return;
        }

        int n = write(fd,logtxt.c_str(),logtxt.size());
        close(fd);
    }

    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level);   // "logtxt.Info/Fatal"

        printOneFile(filename,logtxt);
    }

    void operator()(int level, const char *format, ...)
    {
        // 自定义部分
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[1024];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        // 默认添加部分
        va_list s;
        va_start(s, format);
        char rightbuffer[1024];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        char logtxt[2024];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        printLog(level, logtxt);
    }

    ~Log()
    {

    }

private:
    std::string path; // 将路径信息打印到某个路径文件下
    int _PrintMethod; // 打印的方法(打印到屏幕或文件或多个文件等)
};


Socket.hpp 封装套接字接口

#pragma Once
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#include "Log.hpp"

Log lg;

enum
{
    SOCK_ERR = 1,
    BIND_ERR,
    LISTEN_ERR,
    S
};

class Sock
{
public:
    void Socket()
    {
        socketfd = socket(AF_INET, SOCK_STREAM, 0);
        if (socketfd < 0)
        {
            lg(Fatal, "socket errno : %d ,%s", errno, strerror(errno));
            exit(SOCK_ERR);
        }
    }

    void Bind(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(socketfd, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind errno : %d ,%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
    }

    void Listen()
    {
        int n = listen(socketfd, 10);
        if (n < 0)
        {
            lg(Fatal, "listen errno : %d ,%s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
    }

    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in remote;
        socklen_t len = sizeof(remote);

        int newfd = accept(socketfd, (struct sockaddr *)&remote, &len);
        if (newfd < 0)
        {
            lg(Warning, "accept errno : %d ,%s", errno, strerror(errno));
            return -1;
        }

        char buffip[64];
        inet_ntop(AF_INET, &remote.sin_addr, buffip, sizeof(buffip));
        *clientip = buffip;
        *clientport = ntohs(remote.sin_port);

        return newfd;
    }

    bool Connect(std::string &serverip, uint16_t &serverport)
    {
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(serverport);
        inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
        int n = connect(socketfd, (const struct sockaddr *)&server, sizeof(server));
        if (n < 0)
        {
            lg(Warning, "connect errno : %d ,%s", errno, strerror(errno));
            return false;
        }

        return true;
    }

    int Getfd()
    {
        return socketfd;
    }

    void Close()
    {
        close(socketfd);
    }

private:
    int socketfd;
};

HttpServer.cc

#include"HttpServer.hpp"
#include <memory>`在这里插入代码片`
#include <iostream>
int main()
{
    uint16_t port = 9999;
    std::unique_ptr<HttpServer> svr(new HttpServer(port));
    svr->start();
    return 0;
}

HttpServer.hpp

#include "Socket.hpp"
#include <pthread.h>
#include <vector>
#include <sstream>
#include <fstream>
static const int defaultport = 8080;

const std::string sep = "\r\n";
const std::string wwwroot = "./wwwroot"; // web 根目录
class HttpServer;

// 用于给线程函数传参
class ThreadData
{
public:
    ThreadData(int sockfd, HttpServer *svr) : sockfd_(sockfd), svr_(svr)
    {
    }

public:
    int sockfd_;
    HttpServer *svr_;
};

//用于对http请求内容做反序列
class HttpRequest
{
public:
    void Deserialize(std::string req)
    {
        while (true)
        {
            std::size_t pos = req.find(sep); // 将每一行的信息都插入到vector中
            if (pos == std::string::npos)
                break;
            std::string tmp = req.substr(0, pos);
            if (tmp.empty())
                break;
            req_header.push_back(tmp);
            req.erase(0, pos + sep.size()); // 一行中也有 '\r\n' ,所以需要加上这个长度
        }
    }

    // 解析请求行
    void Parse()
    {
        std::stringstream ss(req_header[0]);
        ss >> method >> url >> http_version;
        file_path = wwwroot; // 所有的文件都在 wwwroot中
        if (url == "/" || url == "/index.html")
        {
            // 当用户不指明访问路径的时候,给用户默认访问./wwwroot/index.html
            file_path += "/index.html";
        }
        else
        {
            file_path += url;
        }
    }

public:
    std::vector<std::string> req_header; // 将用户发送的报文放进到vector中

    std::string method;       // 请求方法
    std::string url;          // 路径
    std::string http_version; // http版本
    std::string file_path;    // http服务器所有的访问路径
};
class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport)
        : port_(port)
    {
    }

    void start()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();

        while (1)
        {
            // 建立连接
            std::string clientip;
            uint16_t clientport;
            int socketfd = listensock_.Accept(&clientip, &clientport);
            if (socketfd < 0)
            { // 重新连接
                continue;
            }
            lg(Info, "get a new connect, sockfd: %d", socketfd);
            // 建立连接成功后,对用户发来的请求做处理
            // 创建线程来处理
            pthread_t tid;
            ThreadData *td = new ThreadData(socketfd, this);
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }

    // 线程处理用户请求
    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);

        // 处理细节另设函数
        td->svr_->HandlerHttp(td->sockfd_);
        delete td;
        return nullptr;
    }

    // 处理细节
    static void HandlerHttp(int sockfd)
    {
        // 接受信息recv,向用户发送信息send
        char buffer[10240];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0; // 当作字符串来读取
            std::cout << buffer << std::endl;

            HttpRequest res;
            // 反序列化
            res.Deserialize(buffer);
            res.Parse();

            // 处理用户请求的内容
            std::string text;
            bool path_right = true;
            text = HandlerContent(res.file_path);
            if (text.empty())   //如果用户访问路径根本不存在,则返回一个错误界面
            {
                path_right = false;
                std::string err_html = wwwroot;
                err_html += "/";
                err_html += "err.html";
                text = HandlerContent(err_html);
            }

            std::string response_line;   //res首行 ,状态行
            if (path_right)
                response_line = "HTTP/1.0 200 OK\r\n";
            else
                response_line = "HTTP/1.0 404 Not Found\r\n";

            std::string response_header = "Content-Length: ";    //相应报文
            response_header += std::to_string(text.size()); // Content-Length: 11
            response_header += "\r\n";
            response_header += "Set-Cookie: username=R_L&&passwd=123";
            response_header += "\r\n";
            std::string blank_line = "\r\n"; //空白行
            // 将得到的结果返回给用户

            std::string response = response_line;
            response += response_header;
            response += blank_line;
            response += text;

            send(sockfd, response.c_str(), response.size(), 0);
            // respond
        }

        // 处理完后关掉sockfd
        close(sockfd);
    }

    //  处理用户请求的内容
    static std::string HandlerContent(std::string htmlpath)
    {
        // 以二进制的方式读数据
        std::ifstream in(htmlpath, std::ios::binary);
        if (!in.is_open())
            return "";

        in.seekg(0, std::ios_base::end);
        auto len = in.tellg();
        in.seekg(0, std::ios_base::beg);

        std::string content;
        content.resize(len);

        in.read((char *)content.c_str(), content.size());
        // std::string content;
        // std::string line;
        // while(std::getline(in, line))
        //{
        //     content += line;
        // }

        in.close();

        return content;
    }

private:
    Sock listensock_; // 套接字相关接口
    uint16_t port_;   // 端口号
};

在HttpServer类中调调用start()函数 开始整个服务器的运行,通过线程的方式来处理用户发来的http请求,由于是在类中实现线程函数,所以线程函数ThreadRun是static的,而为了传递socketfd(因为线程函数需要fd来实现接受http请求和发送响应)到线程函数中,所以我创造了一个类ThreadData专门来传递socketfd。

而在线程函数处理用户请求的内容中我创造了一个HttpRequest类来处理,该类通过http请求分离请求行中的信息,并且给URL路径加上web路径。