使用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日

相关文章

  • iOS16如何自定义Home应用程序 iOS16自定义Home应用程序方法

    iOS16如何自定义Home应用程序 在iOS 14及之前的版本中,我们只能通过在App库中搜索要添加的应用程序并将其放置在主屏幕上,但在iOS 15及之后的版本中,我们可以使用自定义应用库和自定义主屏幕来实现自定义排序和分类应用程序。本文将介绍如何使用iOS 16来自定义Home应用程序。 步骤1. 创建自定义应用程序 您可以在iOS 16的应用程序库中创…

    other 2023年6月25日
    00
  • Flash cs6怎么创建数组元素?

    下面是详细的攻略,包含Flash CS6创建数组元素的过程以及示例说明。 创建Flash CS6数组元素的攻略 第一步:创建一个数组变量 在Flash CS6中,创建一个数组变量需要使用Array类。可以通过var关键字和数组字面量语法,来直接声明和创建一个空数组变量。例如: var myArray: Array = []; 以上代码创建了一个空的名为myA…

    other 2023年6月26日
    00
  • Java教程package和import访问控制的步骤详解

    Java教程:package和import访问控制的步骤详解 在Java编程中,package和import是用于管理代码组织和访问控制的重要概念。本教程将详细介绍package和import的使用方法,并提供示例说明。 1. package(包) 在Java中,package用于将相关的类组织在一起。它提供了一种逻辑上的分组机制,使得代码更加模块化和可维护…

    other 2023年9月7日
    00
  • 百度开发者工具怎么使用?百度开发者工具使用教程与常见问题

    百度开发者工具怎么使用? 百度开发者工具是一款专门为开发者设计的浏览器插件,可以帮助开发者更方便地调试和优化代码,提高开发效率。在使用百度开发者工具之前,我们先来了解一下它的使用方法和常见问题。 百度开发者工具使用教程 以下是百度开发者工具使用教程的具体步骤: 步骤1:下载和安装百度开发者工具 首先打开 Chrome 浏览器,在 Chrome 商店中搜索百度…

    other 2023年6月26日
    00
  • oracle中读写blob字段的问题解析

    Oracle中读写BLOB字段的问题解析 1. BLOB是什么? BLOB是Binary Large Object的缩写,它是Oracle数据库中一种数据类型,通常用于存储图像、音频、视频等二进制格式的数据。 2. 读取BLOB字段 2.1 使用PL/SQL 在PL/SQL中,读取BLOB字段通常需要通过创建BFILE来实现。BFILE是BLOB的一个子类型…

    other 2023年6月25日
    00
  • hbuilderx对比Android有什么区别? 两款软件特点介绍

    HBuilderX vs. Android: A Detailed Comparison Introduction HBuilderX and Android are two different software tools used in the development of mobile applications. While HBuilderX is …

    other 2023年7月27日
    00
  • iDempiere 使用指南 绿色版一键启动测试环境

    iDempiere 使用指南 绿色版一键启动测试环境 开发测试环境的设置是 iDempiere 实现数字化转型必不可少的一步。在使用 iDempiere 时,搭建一个安全可靠的测试环境是非常重要的。为了帮助 iDempiere 用户更加方便地搭建测试环境,我们发布了 iDempiere 使用指南 绿色版一键启动测试环境。 iDempiere 简介 iDemp…

    其他 2023年3月28日
    00
  • Flutter之 ListView组件使用示例详解

    下面我就详细讲解一下“Flutter之 ListView组件使用示例详解”的完整攻略。 简介 ListView是Flutter中非常重要的控件之一,可以实现列表的展示,并且支持添加滚动等操作。在本篇文章中,我们将深入讲解ListView的使用方法和注意事项。 ListView的基本使用 下面是一个最简单的ListView控件的示例代码: ListView( …

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