C++写一个http服务器/web服务器

2023-05-16

目录

    • 开篇废话
    • 正文
    • Http工作流程
    • Http.h
    • Http.cpp
    • 个人网站链接
    • 源码地址
    • 新的解析协议
      • 简单介绍一下

开篇废话

其实这篇文章一直想写,苦于没有时间,想一气呵成写完,在离回家前一天晚上,在上海的小宾馆里面异常兴奋,写一下如何用C++搭建一个简易的http服务器。

我相信大部分人都希望渴望一个自己的网站(哪怕在破在破也是自己做的),我当初是就是这么渴望的,记得那是2020年的8月,我查阅的各种资料终于学会了socket,于是迫不及待的写了一个聊天程序,一直在研究如何端口映射。那时候还很傻很天真,来了一个连接就开一根线程,离开就销毁这根线程,那会也知道线程创建和销毁的开销很大,但是一直没有想到其他的好办法呀。现在回头看这个问题会显得很蠢,咋就没想到用线程池技术呢?学习就是一步一步进化的过程,不去羡慕谁、不去跟任何人比,只跟自己比!

本篇文章不会涉及到很多复杂的概念,也没有写很难读懂的模板函数,代码简单可读,本篇文章送给每一个想自己用C++写一个http服务器的小伙伴!高手们、大佬们当然可以不用看的啦,因为我目前还是个菜鸟~我会把我再学习http遇到的问题、想法都写在本章中。

正文

怎么写一个简单的http服务器阿?很简单,只需要返回最基本的3个东西即可。

  1. 状态码
  2. 发送文件的长度
  3. 发送文件的类型

状态码如200(找到请求文件)、404(未找到请求文件),我实现的也比较简单,就实现了这两个状态码。

文件长度比如客户请求的是index.html页面,浏览器如何知道收到的这个文件什么时候结束呢?就靠是文件的长度

文件类型 html的类型是html格式,css的是css格式,图片有图片的格式,zip有zip格式文件格式对应的文件类型表

返回给客户端三个这种东西加上请求的文件即可(存在请求文件的情况下)可以了

还想再复杂的话,可以看这篇介绍http各个参数的文章文章链接

Http工作流程

在这个部分大概介绍一下大概的http的工作流程。客户端通过网址访问到你的网站(一定要记住客户端是主动请求连接的,服务端是被动连接的),实则就是通过ip+port访问的,只不过http的默认端口号是80。比如你还没有域名、云服务器这些东西,那如何在本地测试呢?就是在浏览框输入ip:port,例如127.0.0.1:9996,按下回车就可以在本地访问自己的http服务器了。当然了http要给客户(请求者)一个首页,当客户没有指定网页,单纯的打出域名或者127.0.0.1:9996,就给他一个默认的首页,这也是我们要实现的事情。客户写了请求文件,我们来判断是否存在,存在就返回状态码200和请求文件的内容。不存在就直接返回404。那我们如何判断啊,拿最简单的GET为例子吧,其他也大同小异,有兴趣的同学可以自行研究其他的。我们利用正则表达式来解析客户发来的请求是GET还是POST还是其他请求,在解析出来要请求文件。再利用状态机思想来文件是否存在,若存在在判断文件的类型。大概的流程就是这些。

Http.h

#pragma once
#include <string>
#include <unordered_map>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>


class TcpClient;

class Http{
        // 文件的根目录
        const std::string path_ = "./www/dxgzg_src";
        std::string filePath_;// 具体文件的绝对路径
        std::string fileType_;// 请求文件的类型
        std::string header_;  // http头response
        int fileSize_;
        int fileFd_;

        struct stat fileStat_;

        // POST请求的话将留言保存到本地文件中
        bool isPostMode_ = false;
public:
        Http() = default;
        void addHeader(const std::string& head);
        void Header(bool flag);
        // 把一些头文件的信息都加进来,只有成功的时候调用这个函数,
        // 并返回文件中的数据
        void processHead();

        // 把请求文件的路径加上
        void addFilePath(const std::string& requestFile);

        // 获取文件的类型
        void analyseFileType(const std::string& requestFile);


        bool analyseFile(const std::string& request);

        void SendFile(int clientFd,bool isRequestOk);

        bool fileIsExist();

        // 用户自定义的回调函数要正确的处理异常和自己负责关闭套接字
        void ReadCallback(TcpClient* t);
};

Http.cpp

#include "Http.h"
#include "TcpClient.h"
#include "Logger.h"


#include <regex>
#include <sys/socket.h>
#include <sys/sendfile.h>
#include <fcntl.h>
#include <unordered_map>
// 用于test
#include <iostream>

using namespace std;
void Http::addHeader(const string& head)
{
    if (!head.empty())
    {
        header_ += head;
        header_ += "\r\n";
    // Console.WriteLine("我在这里 head!= null" + header_);
    }
    // 自动加个结尾
    else
    {
        header_ += "\r\n";
    // Console.WriteLine("我在这里 head == null" + header_);
    }
}

void Http::Header(bool flag)
{
    // 判断要发送的头部 true 表示200 false 表示404
    if(flag == true)
    { 
        header_ = "HTTP/1.1 200 OK\r\n";
    }
    else
    {
        header_ = "HTTP/1.1 404 NOTFOUND\r\nContent-Length:0\r\n\r\n";
    }
}

void Http::processHead()
{
    string ContentType = "Content-Type:";
    if (fileType_ == "html")
    {
        ContentType += "text/html";
    }
    else if(fileType_ == "js")
    {
        ContentType += "application/x-javascript";
    }
    else if(fileType_ == "css")
    {
        ContentType += "text/css";
    }
    else if(fileType_=="jpg" || fileType_== "png")
    {
        ContentType += "image/" + fileType_;
    }
    else if (fileType_== "zip" || fileType_ == "tar")
    {
        ContentType += "application/" + fileType_;
    }
    addHeader(ContentType);

    // 代完善,要打开文件 filePath_是请求文件的路径
    fileSize_= fileStat_.st_size;
    string ContentLength = "Content-Length:" + to_string(fileSize_);
    addHeader(ContentLength);
    // 最后加了一个结尾
    addHeader("");
    // Console.WriteLine("process fileContent_:" + );
}

void Http::addFilePath(const string& requestFile)
{
    filePath_ += requestFile;
}

void Http::analyseFileType(const string& requestFile)
{
    for (int i = 0; i < requestFile.size(); ++i)
    {
        if (requestFile[i] == '.')
        {
            // 获取请求文件以什么结尾的
            fileType_ = requestFile.substr(i + 1);
        }
    }
}

bool Http::fileIsExist(){
    fileFd_ = ::open(filePath_.c_str(),O_CLOEXEC | O_RDWR);
    if (fileFd_ < 0)
    {   // 说明为找到请求的文件
        return false;
    }
    return true;
}

bool Http::analyseFile(const string& request)
{
    // 调用header的
    // 在[]的^是以什么什么开头,放在[]里面的是非的意思
    string pattern = "^([A-Z]+) ([A-Za-z./1-9-]*)";
    regex reg(pattern);
    smatch mas;
    regex_search(request,mas,reg);
    // 因为下标0是代表匹配的整体
    if(mas.size() < 3){
        LOG_INFO("不是正常请求");
        // 啥都不是直接返回false
        return false;
    }
    string requestMode = mas[1];
    if(requestMode == "POST"){
        isPostMode_ = true;
        cout << "POST请求!!!!!" << endl;
    }
    // 请求的具体文件
    string requestFile = mas[2];
    // 先获取请求的文件

    bool flag;
    if (requestFile == "/")
    { // 如果是/的话就给默认值
        filePath_.clear(); // 先清个零
        filePath_ = path_;
        filePath_ += "/run.html";
        // 文件的类型也要给人家加上
        fileType_ = "html"; 
    }
    else
    {
        filePath_.clear(); // 先清个零
        filePath_ = path_;
        addFilePath(requestFile);
        // 利用open函数
        
    }
    flag = fileIsExist();
    // 未找到文件的话
    if(!flag){
        LOG_INFO("未找到客户要的文件");
        cout << filePath_ << endl;
        return false;
    }
    ::fstat(fileFd_,&fileStat_);
    // 如果文件不存在的话也就不需要解析类型
    analyseFileType(requestFile);
    return true;
}


void Http::SendFile(int clientFd,bool isRequestOk)
{
    long len = 0;
    // 头部一定是有的。
    while(len < header_.size()){
        len += ::send(clientFd,header_.c_str(),header_.size(),0);
        cout << "len header" << header_ <<endl;
    }
    // 发完了头,在发请求文件的信息。如果是404这里是没有的
    if (isRequestOk == true)
    {
        len = 0;
	int num = 0;
       	int tmpLen = 0;// 连续好几次没变的话就加一个num
	 while (len < fileSize_)
        {
	    // 发送的文件个数已经写入在len当中了 
            ::sendfile(clientFd,fileFd_,(off_t*)&len,fileStat_.st_size- len);
            cout << "len sendfile" <<"len:" << len << "fileSize" << fileSize_ <<endl;
            if(len <= 0 ){
		break;
	    }
	    if(tmpLen == len){
		++num;
		if(num > 10){
			break;
		}
	    }
	    tmpLen = len;
	}

    }

}

void Http::ReadCallback(TcpClient* t){
    cout << "ReadCallback" << endl;
    int  sockFd = t->getFd();
    char buff[1024];
    int r = ::recv(sockFd,buff,sizeof(buff),0);
    if (r == 0)
    {
        t->CloseCallback();
        return;
    }
    buff[r] = '\0';
    string str = buff;
    cout << str << endl;
    // 未找到文件直接回应404.
    bool flag = analyseFile(str);
    Header(flag);
    if(!flag){
        SendFile(sockFd,false);
       // t->CloseCallback();
        return ;
    }
    // 这个修改头文件的,先调用这个
    processHead();
    //这是文件找到了发送的
    SendFile(sockFd,true);

    if(isPostMode_){
        int fd = ::open("./postLog/message.txt",O_RDWR);
        if(fd < 0){
            LOG_ERROR("未找到文件");
            
        }
        else{
            // 文件偏移到末尾
            ::lseek(fd,0,SEEK_END);
            ::write(fd,str.c_str(),str.size());
            close(fd);
        }
        isPostMode_ = true;
    }

    // 关闭文件套接字
    close(fileFd_);
    // 发完就关闭连接,主要是为了多去几个线程还能跑的快一些
    //t->CloseCallback();
}

不考虑高并发的情况,设计一个同步阻塞的epoll即可,看完http必备的三要素已经能够写出一个服务器了,我的底层socket采用的是自己封装的网络库,Reactor模型,one loop per thread的代码文件比较多,所以就没有放上来,但只要把状态码、文件类型(那一大段if)、文件的长度这三个实现了就可以搭建一个简易的http服务器了。可以利用sendfile零拷贝来发送文件

在补充一点就是,http协议是\r\n结尾。最后还有一个\r\n,就比如404,HTTP/1.1 404 NOTFOUND\r\nContent-Length:0\r\n\r\n,最后面再跟一个\r\n,结束一段跟一个\r\n

个人网站链接

个人网站

源码地址

2021.10.25更,最近终于有空把自己的代码重构了一下,感觉写的还可以,附上链接github链接有兴趣可以点个星哈哈哈哈,可以去github上看一看

新的解析协议

2022.5.18更新,根据\r\n解析每一行,这算一个能跑的伪码,在服务端的话还会设置一下状态码来标识当前解析的状态,所以想了解具体的话可以去github下载源码来看。

简单介绍一下

有一条请求是GET /add?name=hello world&age=19,之前的做法是不支持这样的解析,现在已经支持这样的解析。

#include <string>
#include <unordered_map>
#include <iostream>
#include <map>
#include <vector>
using namespace std;
map<string,string> queryKV;
// pos:偏移量注意是个引用,endIndex:参数结尾的下标.addPos:校正偏移量
pair<string,string> spilt(const string& s,string sep,size_t& pos,size_t endIndex,size_t addPos){
    size_t startIndex = s.find(sep,pos);
    if(startIndex == string::npos)return pair<string,string>();
    string key = s.substr(pos,startIndex - pos);

    string value ="";
    if(endIndex == string::npos){ // 比如 name=dxgzg,后面没有&就需要走第一个了。
        value =  s.substr(startIndex + 1);
        pos = s.size(); // endIndex + 1就等于0了,正数溢出
    } else{
        value = s.substr(startIndex + 1, endIndex - startIndex - 1);
        pos = endIndex + addPos;// ex: \r\n偏移量加2,&偏移量加1
    }

    pair<string,string> ans(key,value);
    return ans;
}
// query url
void example(){
    string s = "POST /addMsg HTTP/1.1\r\nHost: 192.168.0.106 : 9996\r\nConnection : keep - alive\r\n\r\n{\"content\":\"test\"}";

    // 第一行数据
    size_t oneIndex = s.find("\r\n");
    string oneData = s.substr(0, oneIndex);
    size_t index1 = oneData.find(" ");
    size_t index2 = oneData.find(" ",index1 + 1);
    string method = oneData.substr(0,index1);
    string path = oneData.substr(index1 + 1,index2 - index1 - 1);
    size_t flagSpilt = path.find("?"); // 找到"?"
    
    string query = "";    
    if(flagSpilt != string::npos){
        query = path.substr(flagSpilt + 1);
        size_t pos = 0;
        while(1){
            size_t endIndex = query.find("&",pos);
            cout << endIndex << endl;
            auto p = spilt(query,"=",pos,endIndex,1);
            if(p.first == "" && p.second == "")break;
            cout << p.first << " " << p.second << endl;
//        cout << pos << endl;

            queryKV[p.first] = p.second;
        }
    }


    string httpVersion = oneData.substr(index2 + 1,oneIndex - index2 - 1);

    cout << "method:" << method << endl;
    cout << "path:" << path <<endl;
    cout << "query:" << query << endl;
    cout << "httpVersion:" << httpVersion << endl;

    for(auto p: queryKV){
        cout << "key:" << p.first;
        cout << " value:" << p.second << endl;
    }
}
void example2(){
    string s = "POST /addMsg?name=dxgzg&age=19 HTTP/1.1\r\nHost: 192.168.0.106 : 9996\r\nConnection : keep - alive\r\n\r\n{\"content\":\"test\"}";

    // 第一行数据
    size_t oneIndex = s.find("\r\n");
    string oneData = s.substr(0, oneIndex);
    example();
    size_t lastlineIndex = 0;
    lastlineIndex = s.find("\r\n\r\n");

    size_t index = oneIndex + 2; // 偏移量,第一行的后面
    size_t endIndex = 0;
    map<string,string> m;
    while( index < lastlineIndex && ((endIndex = s.find("\r\n",index)) != string::npos)){
        auto p = spilt(s,":",index,endIndex,2);
        m[p.first] = p.second;
    }

    for(auto& p : m){
        cout << "key:" << p.first;
        cout << " value:" << p.second << endl;
    }

    size_t dataIndex = lastlineIndex + 4;
    string data = s.substr(dataIndex);

    cout << data.size() << endl;
    cout << data << endl;
}
int main() {
    // 解析url的
    example();
    
    // 解析http header
    example2();
	return 0;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++写一个http服务器/web服务器 的相关文章

随机推荐

  • mysql添加用户和权限

    用户管理 mysql gt use mysql 查看 mysql gt select host user password from user 创建 mysql gt create user 用户名 IDENTIFIED by 39 用户密
  • MacBook Pro(M1)安装mysql

    1 下载 网址 xff1a mysql com 2 选择社区版本 3 MySQL Community Server 版本 兼容性说明 xff1a 适配 macOS 10 15 版本 xff0c 但上面有说明 xff0c 可运用于 Big S
  • MySQL进阶-监控、高可用

    MySQL监控 1 常见的监控方式 一般来说 xff0c 常见的监控方式主要有如下三种 xff1a 监控方式特点优点缺点工具 脚本自己编写工具或脚本 xff0c 适合初期机器很少的生产环境在企业初期可以快速满足监控需求后期部署和维护成本大商
  • Druid+Commons DBUtils基本使用

    Druid 1 jar包下载 xff1a https github com alibaba druid releases 2 导入jar包 2 1创建lib文件夹 xff0c 复制粘贴进去 2 2 2 3 3 创建配置文件 文件名称 xff
  • MAC安装maven及每次启动需要刷新bash_profile问题

    1 下载 网址 xff1a https maven apache org download cgi 2 解压安装 选择一个目录 示例 xff1a Users i18 apache maven 3 8 1 3 配置变量 vim span cl
  • win10解压安装mysql方法及遇见的问题(缺少MSVCR120.dll文件、服务无法启动)

    WIN10系统MYSQL的下载与安装详细教程 第一步 xff1a 下载 MySQL 下载地址 xff1a https dev mysql com downloads mysql 5 1 html downloads 具体过程如下 xff1a
  • 基于select函数实现的tcp简单服务器

    select 实现 tcp demo 回忆TCP的连接过程selectselect 的封装tcp类的封装程序流程 程序cli cpp 客户端建立连接SelectSvr hpp 服务器的头文件Tcpsvr hppmain cpp 主函数mak
  • 【odroid-xu3】 ODROID-XU3软件环境搭建记录

    原文链接 xff1a http blog csdn net aganlengzi article details 50036951 1 操作系统环境准备 我用的是Ubuntu12 04 xff0c 但是建议用更高的版本 按照android官
  • D435i相机首次开发与踩坑记录

    D435i相机首次开发与踩坑记录 介绍 配置完D435i相机的开发环境后开始尝试研究官方例程 xff0c 试着运行一些demo初入intel相机 开始的时候也是很头疼 xff0c 不知道如何下手 xff0c 看了众多博客后稍微有了一些眉目
  • 单片机串口收发字符数据的类型

    今天在用51单片机进行串口收发数据的时候遇到了这样一个问题 xff0c 上位机给单片机的字符数据是什么类型的 xff0c 单片机又是怎样存储的 xff1f 串口中断如下 UART中断服务函数 void InterruptUART inter
  • cmake之链接外部动态库

    cmake不再使你在构建项目时郁闷地想自杀了 xff0d xff0d 佚名KDE开发者 xff11 xff0e 写在开头 有两种方式 xff0c 一种是cmake自己内置的find package 另一种是使用pkg config 2 fi
  • 一帧CAN数据需要多长时间发送

    1 CAN通讯速率 默认 500kbit s xff1b 2 xff1a 从下图CAN数据包的完整结构可知 xff0c 一包完整的扩展帧CAN数据总共包含 128bit xff1b 3 xff1a 发送一帧扩展帧CAN数据耗时 128 50
  • 在单片机中什么是堆栈?它的作用是什么?

    在片内RAM中 xff0c 常常要指定一个专门的区域来存放某些特别的数据 它遵循顺序存取和后进先出 LIFO FILO 的原则 这个RAM区叫堆栈 子程序调用和中断服务时CPU自动将当前PC值压栈保存 xff0c 返回时自动将PC值弹栈 保
  • 初识Java内部类

    初识Java内部类原创 xff1a morgan83 提起Java内部类 xff08 Inner Class xff09 可能很多人不太熟悉 xff0c 实际上类似的概念在C 43 43 里也有 xff0c 那就是嵌套类 xff08 Nes
  • 微型四轴设计之通过arduino读取MPU6050原始数据

    概述 打算自己选型配件 画PCB以及焊元件 xff0c 制作一个微型四轴飞行器 主控板打算使用stm32 xff0c 此处使用arduino来读取mpu6050只是为了便于开发和调试 xff08 arduino的串口监视器用起来很方便 xf
  • PS304远距离串口服务器模块应用于电子设备开发芯片测试工业数字接口转换数字接口学习验证

    PS304 Ports Server channel 4 是多种数字接口物理层协议转发器 xff0c 可实现 UART 转换 I2C SPI 1Wire 远距离通讯 xff0c 内嵌磁隔离双电源及辅助增强电源电路 自适应线缆算法 强大灵活的
  • 使用 eBPF 技术跟踪 Netfilter 数据流

    1 网络层数据流向与 Netfilter 体系 图 1 1 为网络层内核收发核心流程图 xff0c 在函数流程图中我们可以看到 Netfliter 在其中的位置 xff08 图中深色底纹圆角矩形 xff09 图中对应的 hook 点有 5
  • 【Modern OpenGL】颜色 Colors

    说明 xff1a 跟着learnopengl的内容学习 xff0c 不是纯翻译 xff0c 只是自己整理记录 强烈推荐原文 xff0c 无论是内容还是排版 原文链接 本文地址 xff1a http blog csdn net aganlen
  • C++怎样打开网页?

    C 43 43 如何打开网页 xff1f 一 调用cmd函数打开C 43 43 网页 xff1a 二 使用const命令打开网页 xff1a 最近 xff0c 有朋友问我 xff0c 怎么用C 43 43 打开网页呢 xff1f 今天我给大
  • C++写一个http服务器/web服务器

    目录 开篇废话正文Http工作流程Http hHttp cpp个人网站链接源码地址新的解析协议简单介绍一下 开篇废话 其实这篇文章一直想写 xff0c 苦于没有时间 xff0c 想一气呵成写完 xff0c 在离回家前一天晚上 xff0c 在