linux网络之Http协议用法及说明

 更新时间:2026年10月30日 10:59:49   作者:吹什么轩  
还在为URL解析和HTTP协议格式头疼吗?本文从URL结构、urlencode编码规则到HTTP请求响应的完整报文格式,逐一拆解每个字段的含义,更用C++代码实例教你实现简易HTTP服务器,轻松掌握协议底层原理,让你彻底搞懂Web通信

一、认识URL

解析

URI,也叫做统一资源定位符!

http://是协议,作用是规定通信的规?开头,&分割多个参数;给资源传入参数,筛选、定位资源则,http默认端口是80,https默认端口是443

user:pass@是登录认证信息,是用户名,密码,用于登录认证,但是可以省略

www.example.jp是服务器地址,叫做主机域名,而浏览器会通过DNS解析,将域名解析为ip地址,定位到唯一的主机上

:80是端口号,协议默认端口可以不写,作用是定位到主机上的唯一网络服务

/dir/index.htm是带层次的文件路径,是服务器上资源的文件路径,定位主机内部的资源文件,/是路径分隔符

?uid=1是查询字符串(URI),?开头,&分割多个参数,给资源传入参数,筛选、定位资源。

#ch1是片段标识符,#后面内容,定位页面内部某个位置,不会发给服务器,只在浏览器本地生效。

五点核心理解

  1. 域名→IP:人记域名,浏览器做 DNS 解析转换成 IP 地址,找到网络中的主机。
  2. 端口号定位服务:IP 找到机器,端口找到机器上运行的网络服务。浏览器会自动补协议对应的默认端口。
  3. 文件路径:Linux 一切皆文件,web 资源本质就是服务器上的文件,路径指明访问哪个文件。
  4. 查询字符串?key=val&key2=val2:同一路径下,靠不同参数区分不同内容,提交参数。
  5. #片段锚点:页面内跳转,不参与网络请求,浏览器本地跳转页面位置。

总结

1.域名 + 路径:定位一台主机上的某个文件资源

2.协议 + 端口:确定使用哪种服务来收发数据

3.查询字符串:进一步筛选资源、传递参数

4.所有可以用 URL 访问到的资源,属于万维网 WWW 资源。

二、urlencode 和 urldecode(了解)

介绍

在URL里面的部分字符有特殊含义:/ ? : & = # @

这些符号会被浏览器解析为URL结构标记,不能够当做普通参数内容

举例:如果我要传递参数 aaa=7/9&bbb,这里的/、&在 URL 里有特殊语义,如果不编码,服务器就分不清哪些是参数分隔符,哪些是传入的数据,会解析错乱。所以含有特殊字符的参数,必须先做urlencode 转义;服务器收到后做urldecode 还原。

urlencode的编码规则

urldecode的解码规则

什么时候才会触发编码?

查询字符串?key=value的value部分,如果包含& / ? + # 空格,必须编码。

总结

注意:URL 结构符号(? &)本身不要编码,只有参数内容编码。

坑:自己拼接 URL 字符串时,如果直接拼接带特殊符号的字符串,会产生错误 URL,必须手动调用 urlencode。 浏览器会自动对表单、搜索框内容做 urlencode;但是我们写代码构造 http 请求,需要自己实现或者调用库做编码。

三、Http协议的格式

在网络中最常见的两个行为:

(1)从网络中将别人的资源拿下来:GET

(2)将自己的资源传送到网络上:POST

http请求一共四个部分:请求行,请求报头,空行,请求正文

http响应一共四个部分:状态行,响应报头,空行,响应正文

底层本质:http1.1是基于TCP的文本协议,所有报文本质就是字符串,使用了\r\n作为行结束标志!!

HTTP REQUST

3.1请求行

                

3.1.1请求方法

代表本次客户端想要对资源进行什么操作,有GET和POST两种操作,GET是获取资源,将参数放在URI字符串,一般不带请求正文;POST是提交资源,将数据放在请求正文。

3.1.2URL(统一资源标识符)

URL:

重点区分:URI ≠ 完整 URL

URI只写:/search?wd=C%2B%2B,包含资源路径 + 查询参数;不带域名、不带端口。

域名 / 端口放在请求头Host字段里面传递给服务器。

URL包括:

  • 协议:http
  • 主机 (host):job.xjtu.edu.cn
  • 资源路径 (_uri):/compony/login.do

3.1.3HTTP版本

HTTP/1.1:最主流,支持长连接、Host头部必填。

HTTP/1.0:短连接,请求完成默认断开。

HTTP/2、HTTP/3:改为二进制帧,不再是这种文本报文格式。

3.2请求报头

每一行的格式必须为key:空格 value\r\n,必须严格按照格式来,否则服务器就会解析失败,每个报头占一行,每一行都以\r\n结尾。

几个核心报头深度理解

Host: job.xjtu.edu.cn:Host 告诉服务器,我这次访问的是哪一个域名。没有 Host 头部,服务器不知道返回哪个网站内容。

Connection: keep‑alive:开启长连接。TCP 连接这次请求完成之后不关闭,可以复用这条 socket 继续发多个 HTTP 请求,减少反复建立断开 TCP 的开销。HTTP1.1 默认就是 keep‑alive。 如果值是close,代表本次响应完成就要断开 TCP。

Content‑Length: 36:代表的是请求正文的总长度为36字节,服务器在读的时候,就知道后面要读取36个字节作为body,我们来看一下底部请求正文的长度,正好为36个字节。

Cache‑Control: max‑age=0:缓存控制,告诉服务器,不要用本地缓存,而是向服务器获取最新的资源,一般登录这种敏感操作才会用这种!

Origin: http://job.xjtu.edu.cn:跨域相关头。标识这个请求来源于哪个源(协议 + 域名 + 端口),浏览器发起跨域请求会带上 Origin,服务器用它判断是否允许跨域访问。本次是同域登录,Origin 和 Host 一致。

Upgrade‑Insecure‑Requests: 1:浏览器告诉服务器:优先希望升级成 HTTPS(https 加密访问),1 代表开启该特性。

Content‑Type: application/x‑www‑form‑urlencoded:作用是说明请求正文的数据格式,application/x‑www‑form‑urlencoded:普通表单编码格式,格式就是key=value&key2=value2,value 会做 urlencode 编码。这就是我们前面学的 urlencode 对应的 Content‑Type 类型。如果是传 JSON,这里是application/json。

User‑Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) Chrome/61.0.3163.100 Safari/537.36:简称 UA。告诉服务器客户端是什么程序:Windows 下 Chrome 浏览器版本信息。 服务器可以根据 UA 适配返回不同页面内容。C++ 写网络爬虫的时候,经常伪造 UA,否则服务器识别是脚本直接拒绝。

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/ajpg,*/*;q=0.8:告诉服务器:客户端能够接受哪些类型的响应数据,text/html:html 网页优先,q=0.9 q=0.8:权重系数,数值越小优先级越低,*/*:所有类型都可以接受。

Referer: http://job.xjtu.edu.cn/companyLogin.do:告诉服务器,我是从哪个页面跳转过来发起本次请求。 这里就是登录页面本身。服务器经常用 Referer 做防盗链判断。

Accept‑Encoding: gzip, deflate:告诉服务器:客户端支持接收压缩数据。服务器可以把返回的 html/json 用 gzip 压缩后返回,减少网络传输流量;客户端收到后解压。 服务器如果支持,响应头会多出 Content‑Encoding: gzip。

Accept‑Language: zh‑CN,zh;q=0.8:告诉服务器客户端偏好的语言:简体中文优先。网站可以根据这个返回对应语言页面。

3.3请求正文

格式依旧是key=value的形式,用&作为分割参数,由于用户名密码都没有特殊字符,所以就不需要编码,字节长度严格和上文的36对应相等!!!

HTTP RESPONSE

3.1状态行

HTTP/1.1 200 OK

  • HTTP/1.1:响应协议版本,和请求保持一致
  • 200:3 位状态码,成功,服务器正常处理完请求
  • OK:人类可读描述,程序解析只看数字200

3.2响应报头

Server: YxlinkWAF:Server响应头告诉客户端服务器软件 / 设备名称。 这里YxlinkWAF是Web 应用防火墙,不是原始 Web 服务器;说明流量经过 WAF 防护设备转发。生产环境常常会伪装 / 删除 Server 头,避免泄露服务器版本信息,防止针对性攻击。

Content‑Type: text/html;charset=UTF‑8:告诉客户端,响应正文是什么类型,什么字符集

Content‑Language: zh‑CN:说明页面内容语言:简体中文。

Transfer‑Encoding: chunked:分块传输编码,当响应体总大小服务器预先不知道的时候(动态 网页、实时输出内容),就不会设置Content‑Length,改用chunked

Date: Fri, 29 Sep 2017 05:10:13 GMT:服务器的GMT 标准时间(格林威治时间,不是北京时间)。 HTTP 协议所有时间头一律使用 GMT 时区。北京时间 = GMT+8。

3.3响应正文

响应正文就是HTML源码了

注意:因为是chunked模式,TCP 流里 HTML 并不是一股脑全部发送,而是被切割成若干 chunk 块传输。抓包工具帮我们把 chunk 块重组,展示成完整 html 文本。

需求1:如何保证正文部分能一字不落的全部读到呢?

报头属性里面有 标注正文部分的长度的属性,所以他只要根据换行规则去读取整个报文,然后一直读到空行就说明报头读完了,然后再根据length字段 直接向后读相应长度的字节 就可以把正文部分也给读完了!

问题1:为什么信息需要有版本号呢?

版本号是便于我们双方标识对应版本号的功能,因为可能有的用户没有更新客户端,所以服务端是有可能会接受到各种不同版本的客户端请求的,而较新的版本可能会存在一些旧版本没有的功能!!(比如有的版本可以发消息,而有的版本还可以发朋友圈)

问题2:状态码有什么含义

 

 

四、用代码去理解HTTP1.0

这是一个基于 C++ + TCP 封装实现的极简静态 HTTP 1.0 Web 服务器

客户类:

#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr() {}
    InetAddr(struct sockaddr_in &addr)
    {
        SetAddr(addr);
    }
    InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
    {
        // 主机转网络
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_port);
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
    }
    InetAddr(uint16_t port) : _port(port), _ip()
    {
        // 主机转网络
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_addr.s_addr = INADDR_ANY;
        _addr.sin_port = htons(_port);
    }
    void SetAddr(struct sockaddr_in &addr)
    {
        _addr = addr;
        // 网络转主机
        _port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
        // _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
        _ip = ipbuffer;
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    const struct sockaddr_in &NetAddr() { return _addr; }
    const struct sockaddr *NetAddrPtr()
    {
        return CONV(_addr);
    }
    socklen_t NetAddrLen()
    {
        return sizeof(_addr);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;

Socket.hpp和Tcpserver.hpp

Socket:抽象基类(模板方法模式),定义服务端套接字完整流程骨架,把底层系统调用封装成纯虚接口。

TcpSocket : public Socket:TCP 具体实现子类,override 全部纯虚函数,内部调用 Linux socket 系统调用。

#pragma once
#include <unistd.h>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

static const int gbacklog = 16;

// 基类
namespace SocketModule
{
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void Sockfd() = 0;            // 创建套接字
        virtual void Bind(uint16_t port) = 0; // 绑定需要端口号
        virtual void Listen(int backlog) = 0;
        virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual void Close() = 0;
        virtual int Recv(std::string *out)=0;
        virtual int Send(const std::string &message)=0;
        virtual int Connect(const std::string &server_ip, uint16_t port)=0;
    public:
        void BuildTcpSocket(uint16_t port)
        {
            Sockfd();
            Bind(port);
            Listen(gbacklog);
        }
    };

    static const int defaultfd = -1;
    class TcpSocket : public Socket
    {
    public:
        TcpSocket() : _sockfd(defaultfd) {}
        TcpSocket(int fd) : _sockfd(fd) {}
        ~TcpSocket() {}
        void Sockfd() override
        {
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::FATAL) << "socket error";
                exit(SOCK_ERR);
            }
            LOG(LogLevel::INFO) << "socket success";
        }
        void Bind(uint16_t port) override
        {
            InetAddr peer(port);
            int n = bind(_sockfd, peer.NetAddrPtr(), peer.NetAddrLen()); // 绑定对象
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "bind error";
                exit(BIND_ERR);
            }
            LOG(LogLevel::INFO) << "bind success";
        }
        void Listen(int backlog) override
        {
            // 监听,继续用_sockfd即可
            int n = listen(_sockfd, backlog);
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "listen error";
                exit(LISTEN_ERR);
            }
            LOG(LogLevel::INFO) << "listen success";
        }
        // unique_ptr:独占所有权,只能有一个指针持有资源,不能拷贝,只能移动。
        // shared_ptr`:共享所有权,引用计数,可以拷贝、复制,多个智能指针同时管理同一个对象;计数到 0 自动释放。
        std::shared_ptr<Socket> Accept(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = accept(_sockfd, CONV(peer), &len); // 接收对象
            if (fd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                return nullptr;
            }
            // 接收成功
            LOG(LogLevel::DEBUG) << "accept success:"<<client->StringAddr();
            client->SetAddr(peer);                  // 用peer把客户端的信息带出来
            return std::make_shared<TcpSocket>(fd); // 返回一个TcpSocket的对象
        }
        void Close() override
        {
            if (_sockfd >= 0)
            {
                ::close(_sockfd);
            }
        }
        int Recv(std::string *out)override
        {
            //缓冲区
            char buffer[4096];
            int n=recv(_sockfd,buffer,sizeof(buffer)-1,0);
            if(n>0)
            {
               buffer[n]=0;
               *out+=buffer;
            }
            return n;
        }
        int Send(const std::string &message)override
        {
            return send(_sockfd,message.c_str(),message.size(),0);
        }
        //因为是客户端一定要绑定服务端的ip地址和port,告诉内核我要连哪一个机器上的哪个程序
        int Connect(const std::string &server_ip, uint16_t port) override
        {
            InetAddr server(server_ip, port);
            return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
        }
    private:
        int _sockfd;
    };
}

有人就有疑问了,服务器与0.0.0.0有关系啊,为啥connect时要传特定的地址呢?

0.0.0.0 是服务端 bind() 的特殊监听地址,不是机器真实 IP

而connect是客户端绑定特定的ip地址,需要服务器本地的机器ip地址!!

服务器类

#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>

using namespace SocketModule;
using namespace LogModule;

using ioservice=std::function<void(std::shared_ptr<Socket> sock,InetAddr&client)>;
class TcpServer
{
public:
    TcpServer(uint16_t port,ioservice service):_port(port),_isrunning(false),_listenptr(std::make_unique<TcpSocket>()),_service(service)
    {
        _listenptr->BuildTcpSocket(_port);
    }
    void Start()
    {
       _isrunning=true;
       while(_isrunning)
       {
            //开始接听
            InetAddr client(_port);
            auto sock=_listenptr->Accept(&client);//这种接收方式客户端信息我们也有了
            if(sock==nullptr)
            {
                continue;
            }
            //创建子进程干活
            pid_t id=fork();
            if(id<0)
            {
                LOG(LogLevel::ERROR)<<"fork error";
                exit(FORK_ERR);
            }
            else if(id==0)
            {
                //子进程干的活是服务,压根不需要监听,所以我们得把监听的_fd给关闭
                _listenptr->Close();
                if(fork()>0)
                {
                    exit(OK);
                }
                //留下孙子进程执行任务
                _service(sock,client);
                exit(OK);
            }
            else
            {
                //先关闭
                sock->Close();
                //再等待
                pid_t pid=waitpid(id,nullptr,0);
                (void)pid;
            }
       }
       _isrunning=false;
    }
     ~TcpServer(){}
private:
    uint16_t _port;
    std::unique_ptr<TcpSocket> _listenptr;
    bool _isrunning;
    ioservice _service;
};

在讲http和工具类之前,我们先解决几个问题:

 问题1:我们返回类型是socket基类,但是可以返回Tcpsocket类型,还有传参时,我们的形参是socket类型,传的实参为什么可以是Tcpsocket类型呢?

这其实是c++的向上转型,只有指针,引用,智能指针才会完成向上转型转化为了socket基类,而值传递只能够完成切片,而当调用函数时,TcpSocket 对象内存的基类子对象部分就有 vptr,智能指针内部保存的是Tcpsocket类型对象的起始地址,然后找到堆上 TcpSocket 对象,对象内部 vptr 去找 TcpSocket 虚表,执行 TcpSocket 重写的 Recv。

延伸:模版方法:

你或许会很好奇,为什么BuildTcpSocketMethod在基类内部,我子类可以直接调用,而且里面的函数是经过重写之后的呢?

因为BuildTcpSocketMethod不是虚函数,他是一个普通成员函数,当继承时,它会被子类调用,所以能够直接使用,不会走虚表查找,对象在堆上创建完整 TcpSocket 对象;对象内存内 vptr 指向 TcpSocket 的虚表,虚表里面存放重写后的:Sockfd、Bind、Listen 的函数地址。进入BuildTcpSocketMethod内部,虽然说在基类的函数内部,但是调用的是虚函数,this 指针是 Socket* 类型,但指向真实的 TcpSocket 对象,通过对象内部的 vptr → TcpSocket 虚表,拿到子类重写的虚函数地址;

问题2:为什么在定义_listenptr时用的是unique_ptr的指针,但是套接字对象却是shared_ptr类型? 

unique_ptr 独占所有权,不允许拷贝,只能移动,同一时刻只能有一个智能指针管理这块堆对象,禁止拷贝构造 / 拷贝赋值,监听 TcpSocket 对象,只有 TcpServer 的 _listenptr 拥有它,没有其他地方拷贝、复制这个_listenptr。没有拷贝,就不会违反 unique_ptr 独占规则。

shared_ptr 是共享所有权,引用计数管理,可以多个指针同时指向同一个对象,每来一个客户端就新建一个通信套接字,fork 之后父子进程都拷贝sock(shared_ptr,引用计数 + 1),父子都可以使用,计数归 0 释放。

问题3:为什么设置回调方法时,形参必须是套接字和客户呢?

回调是事件循环触发的,回调函数本身没有捕获外部变量,不知道是哪一个监听 socket来了新连接;传入它,回调内部可以继续执行 accept 接收后续连接,同时shared_ptr保证回调执行期间对象不会被销毁。

accept()系统调用返回的新文件描述符,代表刚接入的客户端;业务代码拿到这个 fd,才能封装成通信 socket,完成读写交互。

简单总结:

  • sock:标记哪个监听 socket 产生事件;
  • client_fd:拿到新客户端通信句柄。

工具类

#pragma once 

#include <iostream>
#include <fstream>
#include <string>

// 工具类

class Util
{
public:
    static bool ReadFileContent(const std::string &filename , std::string *out)
    {
        // version1: 默认是以文本方式读取文件的. 图片是二进制的.
        // std::ifstream in(filename);
        // if (!in.is_open())
        // {
        //     return false;
        // }
        // std::string line;
        // while(std::getline(in, line))
        // {
        //     *out += line;
        // }
        // in.close();

        // version2 : 以二进制方式进行读取
        int filesize = FileSize(filename);
        if(filesize > 0)
        {
            std::ifstream in(filename);
            if(!in.is_open())
                return false;
            out->resize(filesize);
            in.read((char*)(out->c_str()), filesize);
            in.close();
        }
        else
        {
            return false;
        }

        return true;
    }
    static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep/*\r\n*/)
    {
        auto pos = bigstr.find(sep);
        if(pos == std::string::npos)
            return false;
        *out = bigstr.substr(0, pos);
        bigstr.erase(0, pos + sep.size());
        return true;
    }
    static int FileSize(const std::string &filename)
    {
        std::ifstream in(filename, std::ios::binary);
        if(!in.is_open())
            return -1;
        in.seekg(0, in.end);
        int filesize = in.tellg();
        in.seekg(0, in.beg);
        in.close();
        return filesize;
    }
};

这是个工具类,全部都是static类型的静态变量,说明这就是单纯的方法,没有什么成员变量,就避免了每次调用都要创建对象的麻烦,有效的节省了空间。

我们首先来看看读取文件内容这个工具

我们的第一个版本是以文本方式进行读取的,先打开文件,然后将文件的每一行读到我们的*out里面,但是以文本方式读取没法读取类似于图片的二进制文件,所以我们又采取了版本2,以二进制方式读取文件,我们首先先拿到文件的大小,如果成功拿到,我们就用ifstream来打开文件,然后先把out的大小定义为filesize(目的是直接给输出 string 预先分配filesize字节内存空间),然后再read(),一次性读取filesize个字节,读到缓冲区里,注意c_str()是const char*类型的,所以我们得把它强转成为(char*)类型的,然后关掉文件流,释放操作系统文件句柄。

我们再来看看截取每一行的操作

我们把大的字符串传进来,out是缓冲区,sep是分割符(\r\n),然后没有分割符,就返回false,如果找到了分隔符,就把从开头到pos的内容赋值给我们的*out,然后再删除掉我们截取的内容!!

 

这是filesize的大小,我们用了ifstream,这里先对ifstream做一个简单的介绍

 

很明显,我们这里是以二进制形式打开的文件,如果打开失败,就返回-1,打开成功后,我们用了seekg,是一个移动读指针,我们先把指针移动到文件末尾,然后tellg返回当前指针的字节位置,也就是文件字节大小,然后再把指针返回到开头位置,然后关闭文件流,返回大小!!!!!!!

HTTP

在此处,我们将http请求和http响应单独封装起来作为两个独立的类!!

HTTPREQUST

代码:

#pragma once
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include <unordered_map>
using namespace SocketModule;
using namespace LogModule;
const std::string gspace = " ";
const std::string glinespace = "\r\n";
const std::string glinesep = ": ";
const std::string webroot = "./wwwroot";   // 根目录的路径
const std::string homepage = "index.html"; // 首页
const std::string page_404 = "/404.html";  // 错误页面

class HttpRequest
{
public:
     HttpRequest()
     {
     }
     std::string Serialize()
     {
          return std::string();
     }
     // 分割请求行
     bool Praseline(std::string &reqline)
     {
          std::stringstream ss(reqline);
          ss >> _method >> _uri >> _version;
     }
     // 反序列化
     bool DeSerialize(std::string &reqstr)
     {
          // 获取请求行
          std::string reqline;
          bool res = Util::ReadOneLine(reqstr, &reqline, glinespace);
          if (!res)
          {
               return false;
          }
          LOG(LogLevel::DEBUG) << reqline;
          // 开始分割
          Praseline(reqline);
          if (_uri == "/")
          {
               _uri = webroot + _uri + homepage;
          }
          else
          {
               _uri = webroot + _uri;
          }
          LOG(LogLevel::DEBUG) << "_method: " << _method;
          LOG(LogLevel::DEBUG) << "_uri: " << _uri;
          LOG(LogLevel::DEBUG) << "_version: " << _version;
          // ./wwwroot/XXX.YYY
          return true;
     }
     std::string Uri() { return _uri; }
     ~HttpRequest()
     {
     }

private:
     std::string _method;  // 请求方法
     std::string _uri;     // 资源
     std::string _version; // 版本
     std::unordered_map<std::string, std::string> _headers;
     std::string blackline;
     std::string _text;
};

关于uri的一些冷知识:

HTTPRESPONSE

class HttpResponse
{
public:
     HttpResponse() : blackline(glinespace), _version("HTTP/1.0")
     {
     }
     // 序列化
     std::string Serialize()
     {
          // 拼接成字符串
          std::string repline = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;
          // 拼接响应报头
          std::string repkv;
          for (auto &header : _headers)
          {
               std::string line = header.first + glinesep + header.second + glinespace;
               repkv += line;
          }
          return repline + repkv + blackline + _text;
     }
     // 设置要打开的本地资源文件路径,例如 `./wwwroot/index.html`。
     void SetTargetFile(const std::string &target)
     {
          _targetfile = target;
     }
     // 设置退出码
     void Setcode(int code)
     {
          _code = code;
          switch (_code)
          {
          case 200:
               _desc = "OK";
               break;
          case 404:
               _desc = "Not Found";
               break;
          default:
               break;
          }
     }
     // 设置请求报头
     void SetHeader(const std::string &key, const std::string &value)
     {
          auto iter = _headers.find(key);
          if (iter != _headers.end())
               return;
          _headers.insert(std::make_pair(key, value));
     }
     std::string Uri2Suffix(const std::string &targetfile)
     {
          // 现在开始截取后缀
          auto pos = targetfile.rfind(".");
          if (pos == std::string::npos)
          {
               return "text/html";
          }
          std::string suffix = targetfile.substr(pos); // 从pos接到最后
          if (suffix == ".html" || suffix == ".htm")
               return "text/html";
          else if (suffix == ".jpg")
               return "image/jpeg";
          else if (suffix == "jpg")
               return "image/jpg";
          else
               return "";
     }
     bool MakeResponse()
     {
          if (_targetfile == "./wwwroot/favicon.ico")
          {
               LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";
               return false;
          }
          int filesize = 0;
          bool res = Util::ReadFileContent(_targetfile, &_text); // 浏览器请求的资源,一定会存在吗?出错呢?
          if(!res)
          {
               //走404页面,首先清空文本
               _text="";
               LOG(LogLevel::WARNING) << "client want get : " << _targetfile << " but not found";
               Setcode(404);
               _targetfile=webroot+page_404;//重新给_targetfile
               filesize=Util::FileSize(_targetfile);
               Util::ReadFileContent(_targetfile,&_text);
               std::string suffix=Uri2Suffix(_targetfile);
               //重读
               SetHeader("Content-type",suffix);
               SetHeader("Content-length",std::to_string(filesize));
          }
          else
          {
               //读取_text
               LOG(LogLevel::DEBUG)<<"read success:"<<_targetfile;
               Setcode(200);
               filesize=Util::FileSize(_targetfile);
               std::string suffix=Uri2Suffix(_targetfile);
               SetHeader("Content-length",std::to_string(filesize));
               SetHeader("Content-type",suffix);
          }
          return true;
     }
     bool Deserialize(std::string &reqstr)
     {
          return true;
     }
     ~HttpResponse() {}

private:
     std::string _version; // HTTP版本默认 "HTTP/1.0"
     int _code;
     std::string _desc;
     std::unordered_map<std::string, std::string> _headers;
     std::string blackline;
     std::string _text;       // 响应body,读取的html/图片文件内容
                              // 其他属性
     std::string _targetfile; // 要读取的本地磁盘文件路径 ./wwwroot/xxx.html
};

HTTP处理请求

class Http
{
public:
    Http(uint16_t port) : tsvrp(std::make_unique<TcpServer>(port))
    {
    }
    void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
        // 收到请求
        std::string httpreqstr;
        // 假设:概率大,读到了完整的请求
        // bug!
        int n = sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串, 其实我们的recv也是有问题的。tcp是面向字节流的.
        if (n > 0)
        {
            std::cout << "##########################" << std::endl;
            std::cout << httpreqstr;
            std::cout << "##########################" << std::endl;
            // 对报文完整性进行审核 -- 缺
            // 所以,今天,我们就不在担心,用户访问一个服务器上不存在的资源了.
            // 我们更加不担心,给用户返回任何网页资源(html, css, js, 图片,视频)..., 这种资源,静态资源!!
            HttpRequest req;
            req.DeSerialize(httpreqstr);
            HttpResponse resp;
            resp.SetTargetFile(req.Uri());
            if (resp.MakeResponse())
            {
                std::string response_str = resp.Serialize();
                sock->Send(response_str);
            }
            // HttpResponse resp;
            // resp._version = "HTTP/1.1";
            // resp._code = 200; // success
            // resp._desc = "OK";
            // //./wwwroot/a/b/c.html
            // LOG(LogLevel::DEBUG) << "用户请求: " << filename;
            // bool res = Util::ReadFileContent(filename, &(resp._text)); // 浏览器请求的资源,一定会存在吗?出错呢?
            // (void)res;
        }

// #ifndef DEBUG
// #define DEBUG
#ifdef DEBUG
        // 收到请求
        std::string httpreqstr;
        // 假设:概率大,读到了完整的请求
        sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串, 其实我们的recv也是有问题的。tcp是面向字节流的.
        std::cout << httpreqstr;

        // 直接构建http应答. 内存级别+固定
        HttpResponse resp;
        resp._version = "HTTP/1.1";
        resp._code = 200; // success
        resp._desc = "OK";

        std::string filename = webroot + homepage; // "./wwwroot/index.html";
        bool res = Util::ReadFileContent(filename, &(resp._text));
        (void)res;
        std::string response_str = resp.Serialize();
        sock->Send(response_str);
#endif
        // 对请求字符串,进行反序列化
    }
    void Start()
    {
        tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client)
                     {this->HandlerHttpRquest(sock, client); });
    }
    ~Http()
    {
    }

private:
    std::unique_ptr<TcpServer> tsvrp;
};

结果:

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 将你的Apache速度提高十倍的经验分享

    将你的Apache速度提高十倍的经验分享

    你知道吗,如果你的web server是采用apache那你就有机会通过安装一个软件模块提高你的网站访问速度,最大能有10倍之多!这可不是吹牛,你看看原理就明白了
    2013-04-04
  • centos7搭建hadoop2.10高可用(HA)

    centos7搭建hadoop2.10高可用(HA)

    这篇文章主要介绍了centos7搭建hadoop2.10高可用(HA),本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-02-02
  • Ubuntu14.04安装、配置与卸载QT5的步骤详解

    Ubuntu14.04安装、配置与卸载QT5的步骤详解

    这篇文章主要介绍了Ubuntu14.04安装、配置与卸载QT5详细步骤,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05
  • linux或windows环境下pytorch的安装与检查验证(解决runtimeerror问题)

    linux或windows环境下pytorch的安装与检查验证(解决runtimeerror问题)

    这篇文章主要介绍了linux或windows环境下pytorch的安装与检查验证(解决runtimeerror问题),需要的朋友可以参考下
    2019-12-12
  • Apache的fork模式和worker模式判断方法

    Apache的fork模式和worker模式判断方法

    这篇文章主要介绍了Apache的fork模式和worker模式判断方法,文中给出的方法基于http,需要的朋友可以参考下
    2015-07-07
  • Linux下Matlab的安装全过程

    Linux下Matlab的安装全过程

    本文介绍了在Manjaro系统上安装MATLAB的步骤,包括从官网下载安装包、解压、运行安装程序、选择安装位置、选择组件、输入注册信息、同意许可协议、选择安装选项、使用凭证进行安装、开始安装、安装完成以及创建桌面快捷方式
    2025-11-11
  • ubuntu16.0.4 设置固定ip地址的方法

    ubuntu16.0.4 设置固定ip地址的方法

    这篇文章主要介绍了ubuntu16.0.4 设置固定ip地址的方法,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-07-07
  • 详解git中配置的.gitignore不生效的解决办法

    详解git中配置的.gitignore不生效的解决办法

    这篇文章主要介绍了详解git中配置的.gitignore不生效的解决办法的相关资料,这里提供解决办法希望能帮助到大家,需要的朋友可以参考下
    2017-09-09
  • Linux下安装或升级Python 2.7的操作方法

    Linux下安装或升级Python 2.7的操作方法

    下面小编就为大家带来一篇Linux下安装或升级Python 2.7的操作方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • linux如何无损调整分区大小

    linux如何无损调整分区大小

    本篇文章主要介绍了linux如何无损调整分区大小,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05

最新评论