欢迎光临
我们一直在努力

【Linux笔记】网络部分——基于Socket套接字实现最简单的HTTP协议服务器

38.基于Socket套接字实现最简单的HTTP协议服务器

文章目录

  • 38.基于Socket套接字实现最简单的HTTP协议服务器
      • 项目整体框架
        • 文件依赖图
        • 请求流程图
        • 类关系图
      • Socket套接字的封装
        • Socket父类
        • TcpSocket子类中的各种方法
      • Http协议结构实现(HttpProtocol.hpp)
        • 全局字段
        • HTTP请求结构
        • HTTP响应结构
      • TCP服务器封装
      • HTTP服务器核心
        • 全局字段
        • class HttpServer
      • 主程序入口—Httpserver.cc
      • 运行结果
        • 服务器日志打印
        • 浏览器客户端

项目整体框架

这个项目是一个基于C++的HTTP服务器,使用了多线程和线程池来处理并发请求。下面我将梳理整个项目的框架结构:

  • 日志系统 (Log.hpp)
    • 提供了日志记录功能,支持控制台和文件两种输出方式。
    • 定义了日志级别(DEBUG, INFO, WARNING, ERROR, FATAL)和日志策略(ConsoleLogStrategy, FileLogStrategy)。
    • 通过宏LOG(Level)来记录日志,使用方便。
  • 线程模块 (Thread.hpp)
    • 封装了线程类,可以创建线程并执行指定的函数。
    • 提供了线程的启动、连接、分离、停止等方法。
  • 线程池 (ThreadPool.hpp)
    • 使用单例模式管理线程池,可以指定线程数量。
    • 将任务(函数对象)加入队列,由线程池中的线程执行。
    • 使用了互斥锁和条件变量来同步任务队列。
  • 网络地址类 (InetAddr.hpp)
    • 封装了网络地址,包括IP和端口,提供了网络字节序和主机字节序的转换。
  • Socket类 (Socket.hpp)
    • 封装了TCP Socket,提供了创建socket、绑定、监听、接受连接、发送和接收数据等方法。
    • 使用智能指针管理Socket对象。
  • TCP服务器 (TcpServer.hpp)
    • 基于Socket类,封装了TCP服务器。
    • 使用线程池来处理每个客户端连接,每个连接由一个独立的线程处理。
  • HTTP协议解析 (HttpProtocol.hpp)
    • 定义了HttpRequest和HttpResponse类,用于解析HTTP请求和构建HTTP响应。
    • HttpRequest:解析请求行、请求头、请求体。
    • HttpResponse:设置状态码、头部字段、响应体,并序列化为字符串。
  • HTTP服务器 (HttpServer.hpp)
    • 基于TcpServer,实现了HTTP服务器。
    • 可以注册路由处理函数(如Login、Register等)。
    • 对于带参数的请求(GET带查询字符串或POST带正文),调用相应的路由处理函数;否则,直接返回请求的静态文件。
  • 条件变量 (Cond.hpp)
    • 封装了条件变量,用于线程同步。
  • 互斥锁 (Mutex.hpp)
    • 封装了互斥锁和锁守卫,用于资源互斥访问。
  • 公共头文件 (Common.hpp)
    • 定义了一些宏和错误码。
  • 主程序 (Httpserver.cc)
    • 创建HttpServer对象,注册路由,启动服务器。
  • 项目的工作流程:

  • 主函数中创建HttpServer对象,并注册路由处理函数(如Login、Register等)。
  • 启动HTTP服务器,开始监听指定端口。
  • 当有客户端连接时,TcpServer接受连接,并将处理任务(HanderRequest)提交给线程池。
  • 在HanderRequest中,读取HTTP请求,解析请求。
  • 如果请求的路径是注册的路由,则调用相应的处理函数;否则,尝试返回静态文件。
  • 处理函数生成响应,发送给客户端。
  • 项目实现之后,我们可以使用任意浏览器访问我们的服务器,读取网页内容以及操作。

    文件依赖图

    graph TB
    %% 主程序入口
    A[Httpserver.cc<br/>主程序入口] –> B[HttpServer.hpp<br/>HTTP服务器核心]

    %% HTTP服务器层
    B –> C[HttpProtocol.hpp<br/>HTTP协议解析]
    B –> D[TcpServer.hpp<br/>TCP服务器封装]

    %% 网络层
    D –> E[Socket.hpp<br/>Socket抽象层]
    E –> F[InetAddr.hpp<br/>地址管理]
    E –> G[Log.hpp<br/>日志系统]

    %% 并发层
    D –> H[ThreadPool.hpp<br/>线程池管理]
    H –> I[Thread.hpp<br/>线程封装]
    H –> J[Mutex.hpp<br/>互斥锁]
    H –> K[Cond.hpp<br/>条件变量]

    %% 工具层
    E –> L[Common.hpp<br/>公共定义]
    G –> J
    K –> J

    %% 样式定义
    classDef main fill:#e1f5fe
    classDef http fill:#f3e5f5
    classDef network fill:#e8f5e8
    classDef concurrency fill:#fff3e0
    classDef utility fill:#fce4ec

    class A main
    class B,C http
    class D,E,F network
    class H,I,J,K concurrency
    class G,L utility

    请求流程图

    flowchart TD
    %% 请求处理流程
    Start[客户端请求] –> Accept[TcpServer::Accepter<br/>接受连接]

    Accept –> CreateSocket[创建客户端Socket<br/>Socket.hpp]
    CreateSocket –> Enqueue[任务入队<br/>ThreadPool.hpp]

    Enqueue –> WorkerThread[工作线程处理]

    subgraph WorkerThread [工作线程处理流程]
    direction
    WT1[获取任务] –> WT2[HttpServer::HanderRequest<br/>请求处理]
    WT2 –> WT3[HttpRequest::Deserialize<br/>协议解析]
    WT3 –> WT4{路由判断}

    WT4 –>|静态请求| WT5[ReqBuildResp<br/>静态文件服务]
    WT4 –>|动态请求| WT6[路由方法调用<br/>如Login/Register]

    WT5 –> WT7[HttpResponse::Serialize<br/>响应构建]
    WT6 –> WT7

    WT7 –> WT8[Socket::Send<br/>发送响应]
    end

    WT8 –> End[请求完成]

    %% 日志记录点
    Accept -.->|记录连接日志| Log1[Log.hpp]
    WT2 -.->|记录处理日志| Log1
    WT3 -.->|记录解析日志| Log1
    WT7 -.->|记录响应日志| Log1

    类关系图

    classDiagram
    %% HTTP服务器核心类
    class HttpServer {
    -unique_ptr~TcpServer~ _tcpserver
    -unordered_map~string, route_hander_t~ _route
    +Start()
    +SetRouteMethod()
    +GetContent()$
    -HanderRequest()
    -ReqBuildResp()$
    }

    class TcpServer {
    -unique_ptr~Socket~ _socket
    -int _port
    -hander_t _hander
    +InitServer()
    +Loop()
    -HanderRequest()
    }

    class Socket {
    <<abstract>>
    +SocketOrDie()* bool
    +BindOrDie()* bool
    +ListenOrDie()* bool
    +Accepter()* SockPtr
    +Recv()* ssize_t
    +Send()* ssize_t
    +GetSockfd()* int
    }

    class TcpSocket {
    -int _sockfd
    -int _listensockfd
    +SocketOrDie() bool
    +BindOrDie() bool
    +ListenOrDie() bool
    +Accepter() SockPtr
    +Recv() ssize_t
    +Send() ssize_t
    }

    %% HTTP协议类
    class HttpRequest {
    -string _method, _uri, _path, _args
    -unordered_map~string, string~ _heardkv
    +Deserialize()
    +GetPath()
    +GetArgs()
    +GetIsexec()
    }

    class HttpResponse {
    -string _ver, _status_code, _body
    -unordered_map~string, string~ _heardkv
    +Serialize()
    +SetStatusCode()
    +SetBody()
    +SetHeardKV()
    }

    %% 并发相关类
    class ThreadPool~T~ {
    -vector~thread_t~ _threads
    -queue~T~ _ptasks
    -Mutex _lock
    -Cond _cond
    +getinstance()$ ThreadPool~T~*
    +Equeue()
    +Start()
    +Stop()
    -HanderTask()
    }

    class Thread {
    -string _name
    -pthread_t _tid
    -func_t _func
    +Start()
    +Jion()
    +Stop()
    }

    class Logger {
    -shared_ptr~LogStrategy~ _strategy
    +operator()() LogMessage
    +EnableConsoleLog()
    +EnableFileLog()
    }

    %% 关系定义
    HttpServer –> TcpServer
    TcpServer –> Socket
    Socket <|– TcpSocket
    HttpServer –> HttpRequest
    HttpServer –> HttpResponse
    TcpServer –> ThreadPool
    ThreadPool –> Thread
    ThreadPool –> Logger

    注:在之前的博客中,我们详细讲解了其他基础功能的封装,这里不做额外解释。

    Socket套接字的封装

    在学习Linux的过程中,我们逐步实现和封装了各种系统和网络接口。比如互斥锁Mutex,条件变量Cond,日志Log,线程Thread,线程池ThreadPool,以及IP和端口号的主机序列与网络序列的转换类InetAddr。在前面用套接字实现Tcp服务器时我们直接使用Socket套接字的系统调用,现在,我们对Socket套接字也进行封装。

    Socket父类

    class Socket
    {
    public:
    virtual ~Socket() = default;
    virtual bool SocketOrDie() = 0;
    virtual bool BindOrDie(int port) = 0;
    virtual bool ListenOrDie() = 0;
    virtual SockPtr Accepter(InetAddr *cliaddr) = 0;
    virtual ssize_t Recv(std::string &out) = 0;
    virtual ssize_t Send(const std::string &in) = 0;
    virtual int GetSockfd() = 0;
    virtual void Close() = 0;

    void BuildTcpSocket(int port)
    {
    if(!SocketOrDie()) return;
    if(!BindOrDie(port)) return;
    if(!ListenOrDie()) return;
    }

    private:
    };

    • 封装不只是简单封装成类,而是要实现高扩展性。套接字有UDP和TCP之分,也有Linux和Windows版本差异。虽然TCP和UDP套接字系统调用接口基本相同,但Windows需要加载win32对应的socket库和自定义类型。当前不封装Windows接口,但保留未来扩展可能。采用模板方法模式,因为创建套接字过程具有明显套路性。

    • 创建Socket基类,规定所有创建套接字的方法,包括创建、绑定、监听、接受连接和关闭等操作。基类提供固定模式的套接字创建方法,具体实现由子类完成。在基类中定义一系列虚方法,之后再具体实现Linux系统下的TCP套接字继承父类Socket。

    TcpSocket子类中的各种方法
    • 框架、构造析构函数

      //Socket.hpp

      #define RECV_BUFFER_SIZE 1024
      #define SEND_BUFFER_SIZE 1024

      using namespace My_Log;

      namespace My_Socket
      {
      class Socket; // 声明
      using SockPtr = std::shared_ptr<Socket>;

      class TcpSocket : public Socket
      {
      public:
      // 支持传参构造,用于Accepter返回含有指定客户端的文件描述符(_sockfd)的智能指针
      TcpSocket(int sockfd = 1)
      : _sockfd(sockfd), _listensockfd(1)
      {
      }
      //…….其他功能函数

      int GetSockfd() override { return _sockfd; }
      ~TcpSocket() {}

      private:
      int _sockfd; // accept返回的套接字用于recv和send
      int _listensockfd; // 监听套接字
      };
      }

      在前面用套接字实现Tcp服务器时我们知道,socket()创建的套接字不直接提供发送和接收数据的服务,这里定义了两个套接字。

    • 创建套接字

      bool SocketOrDie() override
      {
      _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
      if (_listensockfd < 0)
      {
      LOG(LogLevel::ERROR) << "socket fail!";
      return false;
      }
      LOG(LogLevel::INFO) << "socket success, listensockfd: " << _listensockfd;
      return true;
      }

    • 绑定套接字

      bool BindOrDie(int port) override
      {
      InetAddr addr(port);
      int n = ::bind(_listensockfd, addr.GetSockaddr(), addr.GetSockaddrLen());
      if (n < 0)
      {
      LOG(LogLevel::ERROR) << "bind fail!";
      return false;
      }
      LOG(LogLevel::INFO) << "bind success!";
      return true;
      }

    • 监听套接字

      bool ListenOrDie() override
      {
      int n = ::listen(_listensockfd, 8);
      if (n < 0)
      {
      LOG(LogLevel::ERROR) << "listen fail!";
      return false;
      }
      LOG(LogLevel::INFO) << "listen success!";
      return true;
      }

    前面三个函数是创建套接字的固定流程,因此可以在基类中直接定义一个函数BuildTcpSocket依次执行三个函数的方法.

    • 创建连接——连接客户端

      SockPtr Accepter(InetAddr *cliaddr) override
      {
      if (cliaddr == nullptr)
      {
      return nullptr;
      }

      // 建立连接
      struct sockaddr_in cliaddr_in;
      socklen_t cliaddrlen;
      _sockfd = ::accept(_listensockfd, CONV(&cliaddr_in), &cliaddrlen);
      if (_sockfd < 0)
      {
      LOG(LogLevel::WARNING) << "accept fail!";
      return nullptr;
      }
      LOG(LogLevel::WARNING) << "accept success!, sockfd: " << _sockfd;

      // 返回参数
      cliaddr->SetAddr(cliaddr_in, cliaddrlen);

      return std::make_shared<TcpSocket>(_sockfd);
      }

    Accepter函数中,它返回一个SockPtr(即std::shared_ptr<Socket>),这样我们就可以将每个接受的客户端连接封装成一个独立的Socket对象,并通过智能指针管理其生命周期。为什么要这么做后面会有解释。

    • 接受数据

      ssize_t Recv(std::string &out)
      {
      char recbuff[RECV_BUFFER_SIZE] = {0};
      int n = ::recv(_sockfd, &recbuff, RECV_BUFFER_SIZE 1, 0);
      if (n < 0)
      {
      out = std::string();
      return n;
      }
      recbuff[n] = 0;
      out = recbuff;
      return n;
      }

    • 发送数据

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

    • 关闭套接字

      void Close() override
      {
      if (_sockfd == 1)
      return;
      ::close(_sockfd);
      }

    Http协议结构实现(HttpProtocol.hpp)

    全局字段

    #pragma once

    #include <iostream>
    #include <string>
    #include <vector>
    #include <unordered_map>
    #include <sstream>

    #include "Log.hpp"

    using namespace My_Log;

    const std::string sepline = "\\r\\n";
    const std::string sepheard = ": ";
    const std::string sepblank = " ";
    const std::string defver = "HTTP/1.0";

    • 包含需要的头文件
    • 根据HTTP协议结构化数据定义标识符,比如每一行结尾的换行符sepline,请求包头中KV值之间的分隔符sepheard,以及请求行、状态行不同内容分开的空格sepblank。
    • 定义一些不会经常修改的字段,比如响应字段中的HTTP协议
    HTTP请求结构
    • 框架、构造析构函数

      class HttpRequest
      {
      public:
      HttpRequest()
      {
      }

      void LOGPrint()
      {
      LOG(LogLevel::DEBUG) << "method: " << _method;
      LOG(LogLevel::DEBUG) << "uri: " << _uri;
      LOG(LogLevel::DEBUG) << "path: " << _path;
      LOG(LogLevel::DEBUG) << "args: " << _args;
      LOG(LogLevel::DEBUG) << "ver: " << _ver;

      for (auto &h : _heardkv)
      {
      LOG(LogLevel::DEBUG) << h.first << "—–" << h.second;
      }
      LOG(LogLevel::DEBUG) << "empline: " << _empline;
      LOG(LogLevel::DEBUG) << "body: " << _body;
      }
      std::string GetMethod() const { return _method; }
      std::string GetUri() const { return _uri; }
      std::string GetPath() const { return _path; }
      std::string GetArgs() const { return _args; }
      std::string GetVer() const { return _ver; }
      std::string GetBody() const { return _body; }
      bool GetIsexec() const { return _isexec; }

      ~HttpRequest()
      {
      }

      private:
      // 客户端发来的http请求解析后的数据
      std::string _method;// 请求方法
      std::string _uri;// URL
      std::string _path;// URL_路径(URL中可能不只有路径)
      std::string _args;// URL_参数(URL中可能带有参数)
      std::string _ver;// 版本
      std::unordered_map<std::string, std::string> _heardkv;// 请求报头的KV值,用unordered_map储存,方便查找
      std::string _empline;// 空行(为了完整性,也定义一个)
      std::string _body;// 请求正文

      bool _isexec; // 判断这个请求是否带参
      };

      • 根据HTTP协议的数据格式,在结构体中定义一系列的私有成员变量,并给出读取他们的方法
      • 为了方便从日志中寻找错误,我们用一个函数将反序列化之后的成员变量的值打印出来
    • 反序列化

      void Deserialize(std::string &req)
      {
      // 解析状态行
      int pos = req.find(sepline);
      if (pos == std::string::npos)
      {
      LOG(LogLevel::WARNING) << "find err!";
      return;
      }
      std::string firstline = req.substr(0, pos);
      std::stringstream ss(firstline);
      req.erase(0, pos + sepline.size());
      ss >> _method >> _uri >> _ver;
      LOG(LogLevel::DEBUG) << " # " << _method << " # " << _uri << " # " << _ver;

      // 解析报头
      while (true)
      {
      // 解析报头中的一行
      int p1 = req.find(sepline);
      if (p1 == std::string::npos)
      {
      LOG(LogLevel::WARNING) << "find err";
      return;
      }
      std::string line = req.substr(0, p1);
      req.erase(0, p1 + sepline.size());
      // 遇到空行跳出循环
      if (line.empty())
      break;

      // 一行中的kv值
      int p2 = line.find(sepheard);
      if (p2 == std::string::npos)
      {
      LOG(LogLevel::WARNING) << "find err";
      return;
      }
      std::string key = line.substr(0, p2);
      line.erase(0, p2 + sepheard.size());
      _heardkv.insert(std::pair<std::string, std::string>(key, line));
      }

      // 空行
      _empline = sepline;

      // 正文
      _body = req;

      // 判断是否带参
      if (_method == "GET")
      {
      pos = _uri.find("?");
      if (pos == std::string::npos)
      {
      _path = _uri;
      _args = "";
      _isexec = false;
      }
      else
      {
      _path = _uri.substr(0, pos);
      _args = _uri.substr(pos + 1);
      _isexec = true;
      }
      }
      else if (_method == "POST")
      {
      _path = _uri;
      if (_body.empty())
      {
      _isexec = false;
      }
      else
      {
      _args = _body;
      _isexec = true;
      }
      }
      }

      • 其实就是对客户端发来的一串字符串按照HTTP数据结构分析,把相应值储存在结构体的成员变量中——就是字符串操作,不做过多解释
      • 由于HTTP协议基于TCP协议。因此数据是以字节流的形式传输的,我们这里没有做差错处理,如有需要后面可以补上
    HTTP响应结构
    • 框架、构造析构函数

      class HttpResponse
      {
      private:
      std::string StatusCTOS(const int &code) const
      {
      if (code == 200)
      return "OK";
      else if (code == 404)
      return "Not Found";
      else
      return std::string();
      }

      public:
      HttpResponse() : _empline(sepline), _ver(defver)
      {
      }
      void LOGPrint()
      {
      LOG(LogLevel::DEBUG) << "ver: " << _ver;
      LOG(LogLevel::DEBUG) << "status_code: " << _status_code;
      LOG(LogLevel::DEBUG) << "status_str: " << _status_str;

      for (auto &h : _heardkv)
      {
      LOG(LogLevel::DEBUG) << h.first << "—–" << h.second;
      }

      LOG(LogLevel::DEBUG) << "empline: " << _empline;
      LOG(LogLevel::DEBUG) << "body: " << _body;
      }

      std::string GetVer() const { return _ver; }
      std::string GetStatusCode() const { return _status_code; }
      std::string GetStatusStr(const int &code) const { return StatusCTOS(code); }
      std::string GetHeardKV(const std::string &k) { return _heardkv[k]; }
      std::string GetBody() const { return _body; }

      void SetVer(const std::string &ver) { _ver = ver; }
      void SetStatusCode(const int &code)
      {
      _status_code = std::to_string(code);
      _status_str = StatusCTOS(code);
      }
      void SetHeardKV(const std::string &k, const std::string &v)
      {
      _heardkv.insert(std::pair<std::string, std::string>(k, v));
      }
      void SetBody(const std::string &body) { _body = body; }

      ~HttpResponse()
      {
      }

      private:
      std::string _ver;
      std::string _status_code;
      std::string _status_str;
      std::unordered_map<std::string, std::string> _heardkv;
      std::string _empline;
      std::string _body;
      };

      • 根据HTTP协议的数据格式,在结构体中定义一系列的私有成员变量,并给出读取和修改他们的方法
    • 序列化

      void Serialize(std::string &resp)
      {
      // 序列化状态行
      resp = _ver + sepblank + _status_code + sepblank + _status_str + sepline;
      // 序列化报头
      for (auto &h : _heardkv)
      {
      resp += h.first + sepheard + h.second + sepline;
      }
      // 序列化空行
      resp += _empline;
      // 序列化正文
      resp += _body;
      }

    TCP服务器封装

    #pragma once

    #include <iostream>
    #include <string>
    #include <memory>
    #include <functional>

    #include "Socket.hpp"
    #include "ThreadPool.hpp"
    #include "Log.hpp"

    using namespace My_Log;
    using namespace My_Socket;
    using namespace My_ThreadPool;

    using hander_t = std::function<void(SockPtr, InetAddr)>;
    using threadpool_t = std::function<void()>;

    class TcpServer
    {
    public:
    TcpServer(int port) : _port(port), _socket(std::make_unique<TcpSocket>())
    {
    }
    void InitServer(hander_t hander)
    {
    _hander = hander;
    _socket->BuildTcpSocket(_port);
    }
    void Loop()
    {
    LOG(LogLevel::DEBUG) << "running Loop";
    _isrunning = true;
    std::string recvstr;
    while (true)
    {
    InetAddr cliaddr;
    SockPtr sp = _socket->Accepter(&cliaddr);
    if (!sp)
    {
    break;
    }
    LOG(LogLevel::DEBUG) << "get a new client, info is: " << cliaddr.GetStrIpPort();
    ThreadPool<threadpool_t>::getinstance()->Equeue([this, cliaddr, sp]()
    { this->_hander(sp, cliaddr); });
    }
    }
    hander_t GetHander() { return _hander; }
    ~TcpServer()
    {
    _socket->Close();
    }

    private:
    std::unique_ptr<Socket> _socket;
    int _port;
    bool _isrunning;
    hander_t _hander;
    };

    • hander_t:我们使用包装器将TCP连接管理与业务逻辑解耦,允许不同的上层应用(如HTTP、FTP)复用相同的TCP服务器框架

    • 使用 unique_ptr 自动管理Socket生命周期,避免资源泄漏,确保异常安全,遵循RAII(Resource Acquisition Is Initialization)原则

    • 这里说明一点:我们实现tcpserver时我们会在对应结构体中创建一个TcpSocket套接字的私有成员变量,这个变量用于最开始的套接字的创建、绑定、监听工作,这种工作在一个服务器中只用执行一次,所以我们会把这个类内的成员变量用unique_ptr管理,不允许拷贝。之后在class TcpServer的Loop函数(循环逻辑)中,我们会将每个链接到的客户端的发送和接收任务注册给线程池,由线程池来执行每一个客户端的数据收发任务,这样主线程就可以只关注于服务器与客户端的连接工作(Accepter),数据收发可以由子线程多线程执行。但是要想子线程执行数据收发任务就需要拿到对应客户端的套接字以及Recv和Send方法。而我们在封装套接字的时候我们直接把套接字与Recv和Send方法封装在了一起,因此,我们可以让Accepter函数返回一个与客户端套接字有关的Socket对象,之后Loop函数把这个对象传递给子线程。就可以让子线程代替主线程执行数据收发的任务了。

    HTTP服务器核心

    全局字段

    #pragma once

    #include <iostream>
    #include <string>
    #include <memory>
    #include <unordered_map>

    #include "Socket.hpp"
    #include "TcpServer.hpp"
    #include "InetAddr.hpp"
    #include "HttpProtocol.hpp"

    using namespace My_Socket;

    using task_t = std::function<void()>;
    using route_hander_t = std::function<void(HttpRequest &, HttpResponse &)>;

    const std::string defhomepage = "wwwlsh";
    const std::string firstpage = "index.html";
    const std::string page404 = "/404.html";

    • 定义了一个类型别名 route_handler_t,用于表示处理HTTP请求和响应的函数
    class HttpServer
    • 基本框架

    class HttpServer
    {
    private:
    static std::string Suffix2Desc(const std::string &suffix);
    // 请求直接构建应答
    static bool ReqBuildResp(HttpRequest &req, HttpResponse &resp);
    void HanderRequest(SockPtr &sockptr, InetAddr &cliaddr);

    public:
    HttpServer(int port) : _tcpserver(std::make_unique<TcpServer>(port))
    {
    }
    void Start();
    static std::string GetContent(const std::string &path);
    void SetRouteMethod(const std::string &funcname, route_hander_t func);
    ~HttpServer()
    {
    }

    private:
    std::unique_ptr<TcpServer> _tcpserver;
    std::unordered_map<std::string, route_hander_t> _route; // 其他方法路由
    };

    • 这个结构体包含指向Tcp服务器的智能指针。通过TcpServer处理TCP连接,然后将HTTP协议的处理交给自己的成员函数。HTTP → TCP → Socket

    • HttpServer使用一个哈希表来存储路由处理函数,键为路径(如"/login"),值为处理该路径的函数。

    • Suffix2Desc函数

      static std::string Suffix2Desc(const std::string &suffix)
      {
      if (suffix == ".html")
      return "text/html";
      else if (suffix == ".jpg")
      return "application/x-jpg";
      else
      return "text/html";
      }

      • 内部私有成员函数,通过判断文件的后缀名返回对应的文件类型,用于构建HTTP响应结构中的Content-Type
    • ReqBuildResp函数

      static bool ReqBuildResp(HttpRequest &req, HttpResponse &resp)
      {
      std::string path = defhomepage + req.GetPath();
      if (path.back() == '/')
      {
      path += firstpage; // wwwlsh/index.html
      }

      LOG(LogLevel::DEBUG) << "——客户端在请求: " << req.GetUri();
      req.LOGPrint();
      LOG(LogLevel::DEBUG) << "———————————–" << req.GetUri();

      std::string content = GetContent(path);
      if (content.empty())
      {
      resp.SetStatusCode(404);
      content = GetContent(page404);
      }
      else
      {
      resp.SetStatusCode(200);
      }
      resp.SetHeardKV("Content-Length", std::to_string(content.size()));
      auto pos = req.GetUri().rfind(".");

      std::string mime_type;
      if (pos == std::string::npos)
      mime_type = std::string(".html");
      else
      mime_type = req.GetUri().substr(pos);
      resp.SetHeardKV("Content-Type", Suffix2Desc(mime_type));

      resp.SetBody(content);

      return true;
      }

      • 如果URL不带参数,直接读取指定路劲下的文件
    • HanderRequest函数

      void HanderRequest(SockPtr &sockptr, InetAddr &cliaddr)
      {
      std::string recvstr;
      while (true)
      {
      int n = sockptr->Recv(recvstr);
      if (n < 0)
      {
      LOG(LogLevel::WARNING) << "Recv fail!";
      continue;
      }
      else if (n == 0)
      {
      LOG(LogLevel::INFO) << "client close, client# " << cliaddr.GetStrIpPort();
      break;
      }

      LOG(LogLevel::INFO) << "recvbuf is : " << recvstr;

      HttpRequest req;
      HttpResponse resp;
      req.Deserialize(recvstr);

      if (req.GetIsexec())
      {
      // 带参,执行对应方法
      LOG(LogLevel::DEBUG) << "running there!";
      _route[req.GetPath()](req, resp);
      }
      else
      {
      // 不带参,请求直接构建应答
      ReqBuildResp(req, resp);
      }
      std::string resp_str;
      resp.Serialize(resp_str);

      LOG(LogLevel::INFO) << "result is : " << resp_str;
      sockptr->Send(resp_str);
      }
      }

      是实际处理HTTP请求的函数。它被设置为TcpServer的回调函数,当有新的客户端连接时,TcpServer会创建一个线程来执行这个函数。

    • Start函数

      void Start()
      {
      _tcpserver->InitServer([this](SockPtr sockptr, InetAddr addr)
      { this->HanderRequest(sockptr, addr); });
      _tcpserver->Loop();
      }

      启动HTTP服务器。它通过Lambda表达式向TcpServer注册处理函数,然后启动TcpServer的事件循环。

    • GetContent函数

      static std::string GetContent(const std::string &path)
      {
      std::string content;
      std::ifstream in(path, std::ios::binary);
      if (!in.is_open())
      return std::string();
      in.seekg(0, in.end);
      int filesize = in.tellg();
      in.seekg(0, in.beg);

      content.resize(filesize);
      in.read((char *)content.c_str(), filesize);
      in.close();
      LOG(LogLevel::DEBUG) << "content length: " << content.size();
      return content;
      }

      从指定路径的文件中获取文件内容

    • SetRouteMethod函数

      void SetRouteMethod(const std::string &funcname, route_hander_t func)
      {
      _route[funcname] = func;
      LOG(LogLevel::INFO) << "register route method success! name: " << funcname;
      }

      注册路由处理函数

    主程序入口—Httpserver.cc

    HttpServer.cc 作为整个HTTP服务器的启动入口和业务配置中心,通过依赖注入和路由注册机制将底层网络框架与上层业务逻辑解耦,实现了服务器配置、路由映射和业务处理的无缝衔接,使得开发者只需关注具体业务逻辑的实现而无需关心底层网络通信细节。

    #include <iostream>
    #include <memory>
    #include <string>

    #include "HttpServer.hpp"

    // 其他方法
    void Login(HttpRequest &req, HttpResponse &resp)
    {
    // req.Path(): /login
    // 根据req,动态构建username=lisi&password=abcdefg
    LOG(LogLevel::DEBUG) << "进入登录模块" << req.GetPath() << ", " << req.GetArgs();
    std::string req_args = req.GetArgs();
    // 1. 解析参数格式,得到要的参数

    // 2. 访问数据库,验证对应的用户是否是合法的用户,其他工作….
    // TODO
    // SessionManager m;
    // session_id = m.CreateSession(XXXXX);

    // 3. 登录成功
    std::string body = HttpServer::GetContent("wwwlsh/user.html");
    resp.SetStatusCode(200);
    resp.SetHeardKV("Content-Length", std::to_string(body.size()));
    resp.SetHeardKV("Content-Type", "text/html");
    resp.SetBody(body);

    // resp.SetStatusCode(302);
    // resp.SetHeardKV("Location", "/user.html");
    }
    void Register(HttpRequest &req, HttpResponse &resp)
    {
    // 根据req,动态构建resp
    LOG(LogLevel::DEBUG) << "进入注册模块" << req.GetPath() << ", " << req.GetArgs();
    }

    void Search(HttpRequest &req, HttpResponse &resp)
    {
    // 根据req,动态构建resp
    LOG(LogLevel::DEBUG) << "进入搜索模块" << req.GetPath() << ", " << req.GetArgs();
    }

    void Test(HttpRequest &req, HttpResponse &resp)
    {
    // 根据req,动态构建resp
    LOG(LogLevel::DEBUG) << "进入测试模块" << req.GetPath() << ", " << req.GetArgs();
    }

    int main(int argc, char *argv[])
    {
    if (argc != 2)
    {
    std::cout << "Usage: " << argv[0] << " <server_ip> <server_port>" << std::endl;
    return 1;
    }
    int port = std::stoi(argv[1]);

    std::unique_ptr<HttpServer> httpser_ptr = std::make_unique<HttpServer>(port);

    // 注册其他请求处理方法
    httpser_ptr->SetRouteMethod("/login", Login);
    httpser_ptr->SetRouteMethod("/register", Register);
    httpser_ptr->SetRouteMethod("/search", Search);
    httpser_ptr->SetRouteMethod("/test", Test);

    // 运行
    httpser_ptr->Start();

    return 0;
    }

    运行结果

    我们可以在目标文件夹下定义几个html文件用来测试,由于我们主要学习HTTP服务器的搭建,对于简单的网页可以由AI代劳:

    在这里插入图片描述

    服务器日志打印

    在这里插入图片描述

    浏览器客户端

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux笔记】网络部分——基于Socket套接字实现最简单的HTTP协议服务器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址