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

yizhihongxing

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

相关文章

  • python爬虫万能代码-最精简的爬虫

    以下是“python爬虫万能代码-最精简的爬虫”的完整攻略: 1. 导入必要的库 首先,我们需要导入必要的库。这个例子中,我们需要使用requests库和BeautifulSoup库。可以使用以下代码导入这些库: import requests from bs4 import BeautifulSoup 2. 发送请求并解析HTML 接下来,我们需要发送请求…

    other 2023年5月7日
    00
  • Linux系统下根目录扩容介绍

    下面是“Linux系统下根目录扩容介绍”的完整攻略。 前置条件 在进行根目录扩容之前,请确保你已经: 了解你的磁盘分区和文件系统类型; 关闭正在写入磁盘的进程; 备份重要的数据,以防止数据丢失。 查看磁盘使用情况 首先,我们需要查看当前磁盘的使用情况,以确定是否需要对根目录进行扩容。 df -h 这里 -h 参数表示以可读性较好的方式显示磁盘使用情况。你将会…

    other 2023年6月27日
    00
  • 详解Python中while无限迭代循环方法

    详解Python中while无限迭代循环方法 在Python中,while循环是一种常用的迭代结构,它可以用于创建无限循环。在本攻略中,我们将详细讲解如何使用while循环来实现无限迭代,并提供两个示例说明。 1. 基本语法 while循环的基本语法如下: while condition: # 循环体 其中,condition是一个布尔表达式,当其值为Tru…

    other 2023年7月28日
    00
  • Ubuntu下如何创建XFS文件系统的LVM详解

    下面是创建XFS文件系统的LVM攻略: 1. 准备工作 在进行创建XFS文件系统的LVM之前,需要做一些准备工作: 安装LVM工具:sudo apt-get install lvm2 确保所有磁盘分区大小相同 确保磁盘分区所在磁盘组的空间足够支持创建LV 确认分区信息:sudo fdisk -l 2. 创建物理卷(PV) 首先需要将磁盘分区转化为物理卷(PV…

    other 2023年6月27日
    00
  • mysql无法远程连接的解决方法

    当然,我很乐意为您提供有关“MySQL无法远程连接的解决方法”的完整攻略。以下是详细的步骤和两个示例: 1 MySQL无法远程连接的解决方法 如果您无法从远程计算机连接到MySQL服务器,则可能需要进行以下设置: 1.1 修改MySQL配置文件 在MySQL服务器上,您需要修改MySQL配置文件以允许远程连接。以下是修改MySQL配置文件的示例: sudo …

    other 2023年5月6日
    00
  • 怎么配置局域网中的各机器的TCP/IP协议

    配置局域网中的各机器的TCP/IP协议攻略 1. 确定网络拓扑结构 在配置局域网中的机器的TCP/IP协议之前,首先需要确定网络的拓扑结构。拓扑结构决定了各机器之间的连接方式,常见的拓扑结构包括星型、环形、总线等。确定拓扑结构后,可以开始配置各机器的TCP/IP协议。 2. 配置IP地址 每台机器在局域网中都需要有一个唯一的IP地址,用于标识和寻址。IP地址…

    other 2023年7月30日
    00
  • iPhone手机无法上网怎么办 连不上频繁断开的解决方法

    iPhone手机无法上网怎么办 连不上频繁断开的解决方法 问题表现 有些用户在使用 iPhone 手机上网时,遇到无法上网或连接频繁断开等问题,这些问题会严重影响用户的使用体验。 问题原因 网络信号问题:当手机网络信号不稳定或者信号弱时,会出现上网困难的情况。 网络设置问题:网络设置错误也可能导致上网失败或者频繁断开。 软件问题:当 iOS 系统或者浏览器等…

    other 2023年6月27日
    00
  • python函数的两种嵌套方法使用

    Python函数的两种嵌套方法使用攻略 在Python中,函数可以嵌套在其他函数中,这种嵌套可以帮助我们组织和管理代码。本攻略将详细讲解Python函数的两种嵌套方法的使用。 1. 内部函数(Inner Functions) 内部函数是指在一个函数内部定义的函数。内部函数可以访问外部函数的变量和参数,并且可以在外部函数的作用域之外被调用。下面是内部函数的使用…

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