一、认识 HTTP

HTTP 的底层是 TCP socket ;

#pragma once

#include "Socket.hpp"
#include "InetAddr.hpp"
#include <memory>
#include <functional>
#include <iostream>
#include <signal.h>
#include <unistd.h>

using callback_t = std::function<std::string(std::string &)>;

class TcpServer
{
public:
    TcpServer(int port, callback_t cb)
        : _port(port), _cb(cb), _listensocket(std::make_unique<TcpSock>())
    {
        _listensocket->BuildListenSocketMethod(_port);
    }

    void HandlerRequeset(std::shared_ptr<Socket> sockfd, InetAddr addr)
    {
        // 短服务
        std::string inbuffer; // 字节流式的队列
        ssize_t n = sockfd->Recv(&inbuffer);
        if (n > 0)
        {
            std::string send_str = _cb(inbuffer);
            sockfd->Send(send_str);
        }
        else if (n == 0)
        {
            LOG(LogLevel::DEBUG) << addr.ToString() << " quit,me too";
        }
        else
        {
            LOG(LogLevel::DEBUG) << addr.ToString() << " read error,me too";
        }
        sockfd->Close();
    }

    void Run()
    {
        signal(SIGCHLD, SIG_IGN);
        while (true)
        {
            InetAddr addr;
            auto sockfd = _listensocket->Accept(&addr);
            if (sockfd == nullptr)
                continue;
            LOG(LogLevel::DEBUG) << "获取一个新连接: " << addr.ToString()
                                 << ", sockfd: " << sockfd->SockFd();

            if (fork() == 0)
            {
                _listensocket->Close();
                HandlerRequeset(sockfd, addr);
                exit(0);
            }
            sockfd->Close();
        }
    }
    ~TcpServer()
    {
    }

private:
    int _port;
    std::unique_ptr<Socket> _listensocket;
    callback_t _cb;
};
#include "TcpServer.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage : " << proc << "localport" << std::endl;
}

std::string TestHttp(std::string &request)
{
    std::cout << "############################" <<std::endl;
    std::cout << request << std::endl;
    std::cout << "############################" <<std::endl;
    return "你好";
}

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

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

    std::unique_ptr<TcpServer> tsock = std::make_unique<TcpServer>(serverport, TestHttp);
    tsock->Run();

    return 0;
}

HTTP 协议是一个大的字符串,以 \r\n 来分割字符串,所以 HTTP 协议是以 \r\n 来进行序列化和反序列化的,所以:HTTP 序列和反序列化的过程就是一个字符串的拼接和切分的过程,这个过程 http 没有用任何第三方库;

问题:为什么要有空行?

答:区分报头和有效载荷;

问题:什么是协议?

答:协议是一个约定;在编码层面上就是一个结构体或者类;这个类或者结构体被服务端和客户端共同约定好字段、属性、序列和反序列化;

问题:为什么要有正文?

答:人类上网的行为就两种:1.获取信息,2.上传信息,所以我们上网的过程就是:APP、客户端和服务器进行进行间通信的,本质就是 IO 操作;

二、认识 URL

URL就是网址:

新的:

老的:

这个域名会自动通过 DNS 转换成公网 IP;

我们想得到信息,那么在没有得到信息(资源)之前,这个消息在服务器上,所以你要通过这个域名找到这台服务器;不管这个资源是图片还是音频等在Linux下都是文件,因为 Linux 下一切皆文件,那么 Linux 服务器这么多的文件,怎么找到用户要的文件,所以这个文件要路径,就是上门这个 URL 域名后面那一窜;

既然有了公网 IP 和 文件路径,端口号在哪?

答:http、https 、fep 等协议的端口号是固定的,所以浏览器省略了,但是访问服务器时浏览器会自动加上;

因为路径是唯一的,域名也是唯一的,所以 URL 就能表示互联网中唯一的一个文件;所以 URL 称为统一资源定位符;

URI:统一资源标识符,URL 属于 URI 的一种,能标识唯一性的就是 URI。

HTTP 协议的 request,读取时怎么解决粘包问题?

答:1.通过访问到空行来保证读取到一个完整的报头;2.如果有正文,通过  Content-Length(自描述段) 有效载荷的长度(就是正文的字符串的长度)来读取;

状态码:这个报文的可信度,给浏览器看的,不用我们关心;例如:404(状态码):请求错误(状态码描述)

状态码描述:给人看的。

注意:http 请求,必须要有应答文,即便是错误;

uri : / 表明请求目标服务器的首页;

uri : /a/b/c.html表明请求目标服务器的 web 根目录下的指定路径下的文件;

问题:什么是资源?

答:在Linux里面资源就是文件;

问题:什么是 Web 根目录?

答:任意一个路径,按照目录组织形式,把资源文件保存起来;

结论:http 请求获取任何页面的时候,页面是有多个元素构成的,获取完整的首页不等于一次http请求,一个首页的获取,对应的可能是几十上百个 http 的请求;

http 的应答是超文本的,有自己的类型,那么客户端是如何得知正文的类型的?

答:报头有个属性是:Content-Type:,他是用来标识正文的属性的,这就要求客户端有个 Content-Type 对照表,因为类型有很多;根据资源的后缀来对应 Content-Type;

Referer:当前页面是从哪个页面跳转过来的;

Referer 的作用——1.数据统计;2.拦截;

Connection:keep-alive :期望长链接;长链接就是一次链接可以同时发送多个请求,访问端要处理这些请求,就是例如:一次链接中,客户端10次请求,服务端应答 10 次;短链接就是:1 次链接,1 次请求,1 次应答;长连接是 HTTP/1.1 版本新增加的特性;

什么是重定向?

答:因为提高访问的一方,服务的地址(url)发生改变,要求 Client 更改访问位置,去新的地址访问,这就是就是重定向;

临时重定向 VS 永久重定向

答:本质区别就是影不影响客户对地址的认识;

临时重定向:客户端去访问服务端,假设这个服务端要访问的网址是 www.a.com,此时服务端返回:301/302 的状态码和 Location: 新站点的 url:www.b.com,如果是永久重定向,客户端把 www.a.con 替换成 www.b.com 最后跳转到这个新的站点,此时如果客户端再次访问就拿着 www.b.com 去访问;如果是临时重定向客户端直接拿着 www.b.com 去访问这个站点,再次访问服务端还是拿着 www.a.com 去请求;

三、请求方法

客户端访问服务端无非两种行为:1.获取服务端的资源;2.上传数据到服务器;

99%最常见是请求方法是 GET(重点) 和 POST,其他的大部分都被禁掉了,因为防止恶意攻击服务器;

GET:既可以获取信息,又可以上传信息,通过 url 进行参数传递,如果 url 带有 ? 号,就是上传信息,注意:?号后面就是参数,就是要上传的信息;那么怎么处理的这些上传的信息,前端通过 action :处理表单的后端模块,来处理;注意:?号之前的就是处理该函数的后端模块。

结论:HTTP 可以通过链接 url 的方式对外提供服务(其实就是 url 带参);这称为 resful 风格的微服务接口,例如:下面这条网址一看就是使用的 GET 方法

POST 方法通过正文来传参;

GET VS POST:

1.GET 获取静态资源,GET 传参是 form 参数,参数在 url 的后面;

2.POST 传参,参数在正文部分;

优缺点分析:

1.GET 和 POST 都不安全,因为是明文传参,即:都可以通过报文看到参数;

2.POST 更私密一些,POST 可以用来传递参数较多的情况

3.GET 传参,回显到 url 中,不安全,不能运用于:登录、注册、付款等;

fiddler:这是一个工具,他就像一台代理服务器,可以捕捉用户 http 的请求,然后再让 fidder 发送给目标主机,目标主机的应答也是先由 fidder 捕捉,再发给我们的客户端;

我们通过 http 发送的报文经过无数台路由器,理论上这些路由器都可以获取我们的报文,这是非常不安全的,这个原理就跟 fiddler 一样;所以为了安全就有了另外一种协议:HTTPS;

http 是面向无连接的、无状态的,为什么?

答:虽然 http 的底层是 TCP ,TCP 是连接的,但是 TCP 协议不关我 http 的事,所以 http 是无连接的;无状态,在我们的理解中是:不做历史请求、用户行为等记录,但是 http 的无状态和我们的理解是相反的,例如:我们在网上看个使用,要 VIP 才能看,你注册一下你是 VIP ,如果我们把这个视频看完了,看下一个,假设无状态就是我们理解的那种,那么你就要在登录一下;这样不就会失去用户了吗?所以:HTTP 为了保证能够对用户进行一定程度的状态保持功能,服务器 + 浏览器 + HTTP( Cookie + Session);

Cookie : 浏览器向服务器发起请求 + 账号和密码,此时服务器进行认证(我们程序员代码实现认证),认证通过之后,服务器可以往属性里面添加:Set-Cookie: name = ...... 和 password = .....,返回给浏览器,此时浏览器的 Cookie 以内存或者文件的方式保存账号和密码,当浏览器再次请求时,报头里面就有账号和密码了;不用二次认证;

Cookie 技术:1.应答的报头属性之一,客户端要主动保存 cookie 信息,保存方式:1.内存级,2.文件级;2.从此往后,浏览器在向目标服务器发起 n 次 http 请求时,请求报头主动保存当前的 cookie 信息;

Set-Cookie 是由固定格式的,里面有一个:到期时间,如果这个到期时间是浏览会话结束时,Cookie 的保存信息的方式的内存级,如果是有具体的过期时间那么就是文件级;

上面 Cookie 的过程是有风险的,例如:如果有个木马获取到了 Cookie 里面的信息,hei客就能在浏览器设置 Cookie ,填写 Cookie 信息,此时就能假冒别人去访问服务器;造成两种风险:1.冒充,2.个人信息泄漏;所以为了解决这种问题,浏览器向服务器发起首次发起账号密码的登录请求时,服务器内部会给这个用户生成一个 session 文件,每个用户都有,所以每个用户的 session 文件通过 session id 来区分,当然 session 有可能不是文件而是内存,此时服务器往这个用户的 session 填写 账号和密码 ,此时给客户端应答的 Set-cookie 内容是 session id ,当客户端再次发起请求时通过 session id 来认证;所以 session 只能解决个人信息泄漏的问题,因为在中国没有人敢攻击服务器,他们受到法律保护,但是解决不了盗号的问题,但是缓解盗号问题,例如:账号的ID 地址快速跳转,1s 从广西跳到北京,这个账号绝对有问题,所以服务器把他对应的 session 文件删除掉;

四、代码实现

Http.hpp:

#pragma once
#include <string>
#include <unordered_map>
#include <sstream>
#include "Logger.hpp"
#include <iostream>
#include <functional>
#include <fstream>

static const std::string linesep = "\r\n";
static const std::string innersep1 = " ";
static const std::string innersep2 = ": ";
static const std::string webroot = "./wwwroot";
static const std::string defaulthome = "index.html";
static const std::string html_404 = "404.html";
static const std::string suffixsep = ".";
static const std::string argssep = "?";

using namespace NS_LOG_MODULE;
// http 协议
class HttpRequest
{
private:
    std::string ReadOneLine(std::string &reqstr, bool *status)
    {
        auto pos = reqstr.find(linesep);
        if (pos == std::string::npos)
        {
            *status = false;
            return std::string();
        }
        *status = true;
        auto line = reqstr.substr(0, pos);
        reqstr.erase(0, pos + linesep.size());
        return line;
    }

    void ParseReqLine(std::string &reqline)
    {
        std::stringstream ss(reqline);
        ss >> _method >> _uri >> _httpversion;
    }

    void BuildkV(std::string &line, std::string *k, std::string *v)
    {
        auto pos = line.find(innersep2);
        if (pos == std::string::npos)
        {
            *k = *v = std::string();
            return;
        }
        *k = line.substr(0, pos);
        *v = line.substr(pos + innersep2.size());
    }

public:
    HttpRequest() {}
    ~HttpRequest() {}

    bool Derialize(std::string &reqstr)
    {
        bool status = true;
        std::string reqline = ReadOneLine(reqstr, &status);
        if (!status)
            return false;
        LOG(LogLevel::DEBUG) << reqline;
        ParseReqLine(reqline);
        LOG(LogLevel::DEBUG) << "_mehtod: " << _method;
        LOG(LogLevel::DEBUG) << "_uri: " << _uri;
        LOG(LogLevel::DEBUG) << "_httpversion:" << _httpversion;

        while (true)
        {
            status = true;
            reqline = ReadOneLine(reqstr, &status);
            if (status && !reqline.empty())
            {
                std::string k, v;
                BuildkV(reqline, &k, &v);
                if (k.empty() || v.empty())
                    continue;
                _req_headers.insert(std::make_pair(k, v));
            }
            else if (status)
            {
                _blank_line = linesep;
                break;
            }
            else
            {
                // return false;
                break;
            }
        }


        _path = webroot;
        _path += _uri; // ./wwwroot
        if (_uri == "/")
        {
            _path += defaulthome; // ./wwroot/index.heml
        }
        LOG(LogLevel::DEBUG) << "_path: " << _path;
        //处理 GET 方法带参问题,POST 正文带参问题
        if (_method == "GET")
        {
            // 查看 _path 是否待参数
            auto pos = _path.find(argssep); // GET /login?username_name=zhansan&password_name=1111 HTTP/1.1
            if (pos != std::string::npos)   // 带参数处理
            {
                _req_body = _path.substr(pos + argssep.size()); // GET 带参变成 POST 正文带参
                _path = _path.substr(0, pos);
            }
        }
        else if(_method == "POST")
        {
            _req_body = reqstr;

        }

        return true;
    }

    std::string Body()
    {
        return _req_body;
    }

    std::string Path()
    {
        return _path;
    }

    void SetPath(const std::string &path)
    {
        _path = path;
    }

    std::string Suffix() // index.html 提取文件后缀:.html
    {
        if (_path.empty())
        {
            return std::string();
        }
        else
        {
            auto pos = _path.rfind(suffixsep);
            if (pos == std::string::npos)
            {
                return std::string();
            }
            else
                return _path.substr(pos);
        }
    }

private:
    std::string _method;
    std::string _uri;
    std::string _httpversion;
    std::unordered_map<std::string, std::string> _req_headers;
    std::string _blank_line;
    std::string _req_body;
    std::string _path;
};

class HttpResponse
{
private:
    std::string Code2Desc(int code)
    {
        switch (code)
        {
        case 200:
            return "OK";
        case 400:
            return "Bad Request";
        case 404:
            return "Not Found";
        case 301:
            return "Moved Permanently";
        case 302:
            return "See Other";
        default:
            return "";
        }
    }

public:
    HttpResponse() : _httpversion("HTTP/1.1"), _blank_line("\r\n")
    {
    }
    ~HttpResponse() {}

    std::string Derialize()
    {
        return std::string();
    }

    std::string Serialize()
    {
        std::string respstr = _httpversion + innersep1 + std::to_string(_code) +
                              innersep1 + _desc + linesep;

        if (!_resp_body.empty())
        {
            std::string len = std::to_string(_resp_body.size());
            SetHeader("Content-Length", len);
        }

        for (auto &elem : _resp_heads)
        {
            std::string line = elem.first + innersep2 + elem.second + linesep;
            respstr += line;
        }
        respstr += _blank_line;

        respstr += _resp_body;

        return respstr;
    }

    bool ReadContent(const std::string &path)
    {
        // 打开文件,二进制模式
        // 以二进制的方式读取
        std::ifstream file(path, std::ios::binary);
        if (!file.is_open())
        {
            LOG(LogLevel::WARNING) << path << " 资源不存在!";
            return false;
        }

        LOG(LogLevel::DEBUG) << path << " 资源存在!";

        // 定位到文件末尾获取文件大小
        file.seekg(0, std::ios::end);
        std::streampos fileSize = file.tellg(); // 获取文件大小
        file.seekg(0, std::ios::beg);

        // 创建足够大的字符串来保存数据
        _resp_body.resize(static_cast<size_t>(fileSize));

        // 读取文件内容到字符串
        file.read(&_resp_body[0], fileSize);
        file.close();
        return true;
    }

    void SetCode(int code)
    {
        if (code >= 100 && code < 600)
        {
            _code = code;
            _desc = Code2Desc(_code);
        }
        else
        {
            // 资源不存在
            LOG(LogLevel::DEBUG) << "非法的状态码:" << code;
        }
    }

    bool SetHeader(const std::string &key, const std::string &value)
    {
        _resp_heads[key] = value;
        return true;
    }

private:
    std::string _httpversion;
    int _code;
    std::string _desc;
    std::unordered_map<std::string, std::string> _resp_heads;
    std::string _blank_line;
    std::string _resp_body;
};

using func_t = std::function<HttpResponse(HttpRequest &)>;

class Http
{
private:
    std::unordered_map<std::string, func_t> _handlers;

public:
    std::string Suffix2Desc(const std::string &suffix)
    {
        if (suffix == ".html")
            return "text/html";
        else if (suffix == ".css")
            return "text/css";
        else if (suffix == ".png")
        {
            return "image/png";
        }
        return "text/html";
    }

public:
    Http() {}
    // ./login -> ./wwwroot/login
    void Register(const std::string &action, func_t hander)
    {
        std::string key = webroot;
        key += action;
        _handlers[key] = hander;
    }
    std::string HandlerRequest(std::string &requeststr)
    {
        HttpRequest req;
        std::string respstr;
        std::cout << requeststr;
        if (req.Derialize(requeststr))
        {
            std::string target = req.Path();
            HttpResponse resp;
            // 1.交互式处理 -> 处理带参数的 url 和 纯静态网页
            if (_handlers.find(target) != _handlers.end())
            {
                resp = _handlers[target](req);
            }
            else
            {
                // 处理静态网页
                if (resp.ReadContent(target))
                {
                    std::string suffix = req.Suffix();
                    std::string mime_type_value = Suffix2Desc(suffix);
                    resp.SetHeader("Content-Type", mime_type_value);
                    resp.SetCode(200);
                }
                else
                {
                    // 资源不存在
                    resp.SetCode(301);
                    resp.SetHeader("Location", "/404.html");
                }
            }
            respstr = resp.Serialize();
        }
        return respstr;
    }
    ~Http() {}
};

TcpServer.hpp:

#pragma once

#include "Socket.hpp"
#include "InetAddr.hpp"
#include <memory>
#include <functional>
#include <iostream>
#include <signal.h>
#include <unistd.h>

using callback_t = std::function<std::string(std::string &)>;

class TcpServer
{
public:
    TcpServer(int port, callback_t cb)
        : _port(port), _cb(cb), _listensocket(std::make_unique<TcpSock>())
    {
        _listensocket->BuildListenSocketMethod(_port);
    }

    void HandlerRequeset(std::shared_ptr<Socket> sockfd, InetAddr addr)
    {
        // 短服务
        std::string inbuffer; // 字节流式的队列
        ssize_t n = sockfd->Recv(&inbuffer);
        if (n > 0)
        {
            std::string send_str = _cb(inbuffer);
            std::cout <<"发送成功"<< std::endl;
            sockfd->Send(send_str);
        }
        else if (n == 0)
        {
            LOG(LogLevel::DEBUG) << addr.ToString() << " quit,me too";
        }
        else
        {
            LOG(LogLevel::DEBUG) << addr.ToString() << " read error,me too";
        }
        sockfd->Close();
    }

    void Run()
    {
        signal(SIGCHLD, SIG_IGN);
        while (true)
        {
            InetAddr addr;
            auto sockfd = _listensocket->Accept(&addr);
            if (sockfd == nullptr)
                continue;
            LOG(LogLevel::DEBUG) << "获取一个新连接: " << addr.ToString()
                                 << ", sockfd: " << sockfd->SockFd();

            if (fork() == 0)
            {
                _listensocket->Close();
                HandlerRequeset(sockfd, addr);
                exit(0);
            }
            sockfd->Close();
        }
    }
    ~TcpServer()
    {
    }

private:
    int _port;
    std::unique_ptr<Socket> _listensocket;
    callback_t _cb;
};

Main.cc:

#include "TcpServer.hpp"
#include "Http.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage : " << proc << "localport" << std::endl;
}

//处理带参的 url 的参数
HttpResponse Login(HttpRequest& req)//注册
{

}

HttpResponse Register(HttpRequest& req)//登录
{

}

HttpResponse Search(HttpRequest& req)
{

}


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

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

    //HTTP 协议
    std::unique_ptr<Http> http = std::make_unique<Http>();
    http->Register("/login",Login);
    http->Register("/register",Register);
    http->Register("/Search",Search);

    std::unique_ptr<TcpServer> tsock = std::make_unique<TcpServer>(serverport, 
        [&http](std::string& reqstr)->std::string{
            return http->HandlerRequest(reqstr);
        });
    tsock->Run();

    return 0;
}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐