使用C++制作简单的web服务器(续)

使用C++制作简单的web服务器(续)攻略

实现目标

本篇攻略主要讲解如何使用C++进行制作简单的Web服务器,其主要实现目标为:

  • 实现静态文件的服务器
  • 实现HTTP请求的解析和响应
  • 支持并发处理请求
  • 支持多线程和多进程的方式进行并发处理请求

环境准备

在开始制作Web服务器之前,我们需要先安装一些必要的库和工具:

  • C++编译器(可以使用gcc或clang)
  • Libevent库:用于事件驱动编程
  • OpenSSL库(可选):用于加密和解密

服务器架构

Web服务器的整体架构如下:

  • 创建监听套接字
  • 绑定监听套接字
  • 开始监听
  • 接收客户端连接请求
  • 接收客户端请求报文
  • 解析客户端请求报文
  • 处理客户端请求
  • 生成服务器响应数据
  • 发送响应给客户端
  • 关闭连接

代码实现

参数解析

在开始编写Web服务器之前,我们需要先解析全部的传入参数。下面是一个示例命令:

./web_server -h 192.168.31.168 -p 8080 -t 10 -s on -l debug

其中包含的参数解释如下:

  • -h:指定服务器IP地址
  • -p:指定服务器端口号
  • -t:指定服务器线程池的线程数量
  • -s:是否启用SSL
  • -l:指定日志的级别

例如,我们通过如下方式进行参数的解析:

bool Server::parseArgs(int argc, char* argv[])
{
    int opt;

    while ((opt = getopt(argc, argv, "h:p:t:s:l:")) != -1) {
        switch (opt) {
        case 'h':
            m_host = optarg;
            break;
        case 'p':
            m_port = std::stoi(std::string(optarg));
            break;
        case 't':
            m_threadNum = std::stoi(std::string(optarg));
            break;
        case 's':
            m_ssl = (std::string(optarg) == "on");
            break;
        case 'l':
            m_logLevel = optarg;
            break;
        default:
            return false;
        }
    }

    return true;
}

上述代码中我们使用了getopt函数对参数进行解析,并使用switch语句进行请求的处理。

监听套接字

在开始监听客户端连接请求之前,我们需要先创建并设置监听套接字。我们可以使用socket函数创建和设置套接字。在本次实现中我们采用了AF_INET协议簇,并使用setsockopt方法设置套接字选项。

bool Server::initSocket()
{
    m_listenFd = socket(AF_INET, SOCK_STREAM, 0);
    assert(m_listenFd != -1);

    int optval = 1;
    if (setsockopt(m_listenFd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, 
            sizeof(int)) < 0) {
        close(m_listenFd);
        return false;
    }

    if (setsockopt(m_listenFd, SOL_SOCKET, SO_REUSEPORT, (const void *)&optval, 
            sizeof(int)) < 0) {
        close(m_listenFd);
        return false;
    }

    return true;
}

绑定监听套接字

在设置好监听套接字之后,我们需要将其绑定在指定的IP地址和端口号上:

bool Server::bindSocket()
{
    struct sockaddr_in serverAddr;
    memset(&serverAddr, 0, sizeof(serverAddr));

    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(m_host.c_str());
    serverAddr.sin_port = htons(m_port);

    if (bind(m_listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {
        close(m_listenFd);
        return false;
    }

    return true;
}

开始监听

之后我们需要将监听套接字转为监听状态,并使用listen函数开始接受用户的连接请求。在本次实现中我们采用1024作为连接队列的长度。

bool Server::startListen()
{
    if (listen(m_listenFd, 1024) == -1) {
        close(m_listenFd);
        return false;
    }

    return true;
}

接收客户端连接请求

在服务器开始监听之后,我们需要不断地接受并处理用户的连接请求,并使用accept函数生成系统分配的套接字描述符,返回可供操作的套接字。在本次实现中我们使用epoll异步IO来进行监听。

bool Server::acceptConnection()
{
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen = sizeof(clientAddr);
    int connFd = accept(m_listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen);
    if (connFd == -1) {
        return false;
    }

    if (m_ssl) {
        // SSL accept
    }

    else {
        m_eventLoop->add_handler(connFd, poll_event::READ_EVENT, 
            std::bind(&Server::handleRequest, this, std::placeholders::_1, std::placeholders::_2));
    }

    return true;
}

客户端请求报文处理

在接受到客户端请求之后,我们需要对其进行解析处理。Web服务器会根据URL路径找到对应的处理函数,然后将处理结果构造为HTTP响应(包括响应头和响应体)依次发送给客户端。在本次实现中我们使用了正则表达式解析请求报文,并使用回调函数进行处理。

void Server::handleRequest(int fd, uint32_t events)
{
    uint32_t len = 0;
    int ret = ioctl(fd, FIONREAD, &len);
    if (ret == -1 || len <= 0) {
        removeConn(fd);
        return;
    }

    char buf[MAX_BUFFER];
    memset(buf, 0, sizeof(buf));
    int nRead = read(fd, buf, len);
    if (nRead < 0 && errno != EAGAIN) {
        removeConn(fd);
        return;
    }

    std::string buffer = std::string(buf);
    std::regex pattern("([^\\s]+)", std::regex_constants::icase);
    std::sregex_iterator it(buffer.begin(), buffer.end(), pattern);
    std::sregex_iterator end;
    std::vector<std::string> strs;

    while (it != end) {
        strs.push_back(it->str());
        ++it;
    }

    if (!strs.size()) {
        removeConn(fd);
        return;
    }

    auto method = strs[0];
    auto url = strs[1];
    auto version = strs[2];

    if (url == "/") {
        url = "/index.html";
    }

    auto it1 = UrlHandlers.find(std::string(url));
    if (it1 != UrlHandlers.end()) {
        it1->second(fd);
    } else {
        removeConn(fd);
        return;
    }
}

客户端请求的响应

在处理完成客户端的请求之后,我们需要将处理结果构造为响应结果,并发送给客户端。在实现过程中,我们采用多线程池的方式进行TCP数据的异步读写处理。

bool Server::sendResponse(Response& res)
{
    std::stringstream ss;
    std::unique_lock<std::mutex> lock(m_mtx);

    // 1. 发送响应头
    ss << "HTTP/1.1 " << res.getCode() << " OK\r\n";
    for (auto it = res.getHeader().begin(); it != res.getHeader().end(); ++it) {
        ss << it->first << ": " << it->second << "\r\n";
    }

    ss << "\r\n";
    len_ += ss.str().length();

    if (ssl) {
        // SSL write
    } else {
        write(fd_, ss.str().c_str(), ss.str().length());
    }

    lock.unlock();

    // 2. 发送响应体
    if (res.body.length()) {
        if (ssl) {
            // SSL write
        } else {
            m_threadPool->AddTask(ResponseTask(fd_, res.body.c_str(), res.body.length()));
        }

        len_ += res.body.length();
    }

    return true;
}

处理函数的示例

下面是一个示例处理函数,它会返回一个HTML页面。

void Server::handleIndex(int fd)
{
    Response res;
    res.setCode(ResponseCode::OK);
    res.setBody("<html>\r\n<head>Web Server</head>\r\n<body>\r\nThis is Web Server!\r\n</body></html>");

    res.addHeader("Content-Length", std::to_string(res.getBody().length()));
    res.addHeader("Content-Type", "text/html");

    sendResponse(res);
}

支持并发处理请求的方式

在本次实现中,我们采用了多线程编程和多进程编程的方式对客户端请求进行异步处理。如果使用多线程,我们可以通过线程池的方式进行高效处理;如果使用多进程,则可以创建多个子进程进行超高并发处理。下面是一个多进程的实例示例:

bool Server::run()
{
    pid_t pid;
    for (int i = 0; i < processNum_; ++i) {
        if ((pid = fork()) < 0) {
            std::cout << "fork failed." << std::endl;
            return false;
        } else if (pid == 0) {
            break;
        } else {
            m_childs.push_back(pid);
        }
    }

    if (pid == 0) {
        loop();

        return true;
    } else {
        while(1) {
            wait(nullptr);
            if (errno == ECHILD && status == 0) {
                std::cout << "All servers has exited." << std::endl;
                return true;
            }
        }

        return true;
    }
}

完整代码

完整代码可以在我的GitHub仓库中获取:

总结

本次攻略主要讲解了如何使用C++实现一个简单的Web服务器,包括参数解析、套接字设置和监听、客户端请求的处理、响应的发送和多线程处理等方面的实现方法,同时也讲解了一些工具和库的使用方法。这个服务器并没有很长的代码,但是需要理解的知识点还是很多的。通过学习本节知识,你将掌握如何使用C++制作Web服务器的技巧,希望本篇攻略对你有所帮助。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:使用C++制作简单的web服务器(续) - Python技术站

(0)
上一篇 2023年6月27日
下一篇 2023年6月27日

相关文章

  • echart中的itemstyle如何设置

    以下是ECharts中的itemStyle如何设置的完整攻略: 什么是itemStyle? itemStyle是ECharts中的一个配置项,用于设置表中各种图形元素的样式,包括颜色边框、阴影、透明度等。 步骤1:设置全局样式 可以使用ECharts的setOption方法设置全局样式,例如: option = { // 设置全局样式 textStyle: …

    other 2023年5月6日
    00
  • 详细对比php中类继承和接口继承

    当我们编写面向对象的代码时,经常需要使用到类继承和接口继承。本文将详细对比PHP中类继承和接口继承,从继承的概念、语法、应用场景等多个方面进行讲解。 一、类继承 1. 概念 类继承是指子类继承父类的属性和方法,并且可以在子类中新增属性和方法,或者覆盖父类的方法。子类也可以继承父类的常量。 2. 语法 class 父类名 { // 父类的属性和方法 } cla…

    other 2023年6月27日
    00
  • 深入了解C语言中的字符串和内存函数

    欢迎来到本网站,我们将为您详细介绍“深入了解C语言中的字符串和内存函数”的攻略。 字符串的概念 在 C 语言中,字符串是一个字符数组,以 null 字符(’\0’)结尾。这意味着 C 语言中的字符串实际上是一个字符数组,该数组以 null 字符作为其最后一个元素来终止字符串的标记。 例如, “hello” 字符串实际上是一个包含 6 个字符的字符数组,这些字…

    other 2023年6月20日
    00
  • vue自定义指令之面板拖拽的实现

    接下来我将详细讲解Vue自定义指令之面板拖拽的实现攻略,并且提供两个示例说明。 什么是Vue自定义指令? Vue自定义指令是指在Vue中可以编写自己的指令,来扩展Vue的功能。比如实现拖拽、复制等功能。 面板拖拽实现的思路 面板拖拽的实现思路是:当鼠标按下时,获取当前面板的位置,当鼠标移动时,计算鼠标移动的距离,更新面板的位置,当鼠标松开时,停止移动。 具体…

    other 2023年6月27日
    00
  • linux循环读取文件的每一行

    以下是“Linux循环读取文件的每一行”的完整攻略: Linux循环读取文件的每一行 在Linux中,我们可以使用各种命令和脚本来处理文本文件。本攻略将详细讲解如何使用Linux命令和脚本循环读取文件的每一行,以及示例说明。 使用while循环 我们可以使用while循环来逐行读取文件。以下是一个示例: #!/bin/bash while read line…

    other 2023年5月8日
    00
  • bilibili怎么激活账号?B站激活账号两种方法

    B站激活账号有两种方法,分别是邮件验证和手机号验证,下面我会一一讲解并附带示例说明。 一、邮件验证 邮件验证是一种常见的激活账号的方式,打开B站官网,进入登录页面,在用户名和密码输入框下方会有“注册账号”的链接,点击这个链接进入注册页面。 在注册页面中填写完必填项后,点击“注册”按钮,系统会自动向你所填写的邮箱发送一封验证邮件,邮件中会包含一个链接,点击链接…

    other 2023年6月27日
    00
  • jenkins部署分支报finished:unstable的问题解决

    当然,我可以为您提供有关“Jenkins部署分支报finished:unstable的问题解决”的完整攻略,以下是详细说明: 问题描述 在使用Jenkins分支部署时,可能会遇到“finished:unstable”状态的问题。这种情况通常表示构建过程中出现了一些问题,但构建仍然完成了。这可能会导致部署失败或出现其他问题。 问题解决 以下是解决Jenkins…

    other 2023年5月7日
    00
  • 手机信号不好怎么办(多种解决方法)

    手机信号不好怎么办(多种解决方法) 手机信号不好可能会影响我们正常的通话、短信发送和网络使用,因此让我们不得不思考如何解决。下面是一些常见的方法,可以帮助我们提高手机信号的质量。 方法一:更换运营商 更换运营商是解决手机信号问题的最直接和有效的方法之一。因为不同的运营商在地区覆盖和信号强弱上存在很大的差异。可以通过以下几种方式来了解不同运营商在所在地区的信号…

    other 2023年6月27日
    00
合作推广
合作推广
分享本页
返回顶部