cpp项目 ---ranxin_tinyhttpd(一)

C语言版解析

Tinyhttpd 是J. David Blackstone在1999年写的一个不到 500 行的超轻量型 Http Server

本项目主要是为了通过cpp复现一遍该项目

原项目主体逻辑

设置端口号--》服务器初始化--》 接收客户端连接--》运行CGI

服务器初始化

mingw64中不提供<sys/socket.h>因为这是属于类unix系统的

可以在第三方库文档中进行查看

创建套接字

通过<sys/socket.h>库中的socket函数创建socket

httpd = socket(PF_INET, SOCK_STREAM, 0);

其中的PF_INET和文档中的AF_INET几乎相同,不做赘述,详看博客

SOCK_STREAM是socket的类型,表示面向连接,文档数据传输(TCP)

image-20240225145134319

初始化地址

通过结构体sockaddr_in实现地址的存储,具体内部结构如下:

struct sockaddr_in {
    short            sin_family;   // 地址族(Address Family),对于IPv4协议,总是设置为AF_INET。
    unsigned short   sin_port;     // 16位的端口号,使用网络字节顺序(大端序)。
    struct in_addr   sin_addr;     // 32位IP地址,存储网络中的主机地址。
    char             sin_zero[8];  // 未使用,为了与struct sockaddr保持大小一致而保留的空字节。
};

原项目如下:

struct sockaddr_in name;
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);

其中的htons()是为了将主机字节顺序转换为网络字节顺序(网络协议规定)

htonl(INADDR_ANY);是表示绑定到任意可用接口

设置套接字选项

具体结构如下:

int setsockopt(int socket, int level, int option_name, const void
                *option_value, socklen_t option_len);

setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)

httpd socket描述符

level:SOL_SOCKET 设置协议级别选项

SO_REUSEADDR 允许重用本地地址,

&on指向选项值的缓冲区的指针,用于启用(提供了一个指向实际选项值的指针

sizeof(on)指定了缓冲区大小

绑定套接字和地址

bind(httpd, (struct sockaddr *)&name, sizeof(name)

将httpd socket描述符与name地址绑定

动态分配端口

如果传入的port为0,则重新动态分配一个可用端口,通过getsockname函数来自动填充name地址(其中就包括了动态端口的分配

getsockname(httpd, (struct *sockaddr* *)&name, &namelen)

然后再把端口从网络字节顺序转换成主机字节顺序

*port = ntohs(name.sin_port);

监听套接字

listen(httpd, 5),5是监听队列的最大长度

开始监听后,返回socket描述符

处理客户端请求

主程序循环接收请求

client_sock = accept(server_sock,
                (struct sockaddr *)&client_name,
                &client_name_len);
        if (client_sock == -1)
            error_die("accept");
        /* accept_request(&client_sock); */
        if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
            perror("pthread_create");

具体结构如下

int accept (int socket, struct sockaddr *address,
                                 socklen_t *address_len);

这里的socket就是服务端的套接字描述符,目的是为了和服务端的类型协议、地址族一致

后面的sockaddr和sockelen_t和之前的服务端类似,主要是设置客户端的地址

返回的依旧是socket文件描述符

读取请求行

原函数int get_line(int sock, char *buf, int size)主要是通过socket.h提供的recv函数实现,结构如下

ssize_t recv(int socket, void *buffer, size_t length, int flags);

需要注意的是读取过程中读到了'\r'回车符,需要预览下个字符,如果是换行符就把'\r'移除,如果不是,就把'\r'改为'\n'存入缓冲区

最后根据缓冲区size或者读到'\n'(或'\r\n')为结束

解析请求行

方法比较简单,就是根据空格分隔符分割字符串,然后分别作为

  • 请求方法
  • URL

如果方法是POST则需要建立CGI,如果方法是GET则检查是否有查询参数

分别进行缓存

构造资源路径

sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");

先进行路径构造,如果无资源就返回NOT FOUND

如果有,通过位掩码来判断是否为目录,如果是目录就加默认页面(index.html)

如果不是则判断是否可执行,如果可执行则进行CGI调用

CGI调用

子进程

需要向父进程输出,所以要留子进程输出管道的写端

  1. 重定向输入输出:

    • 通过STDOUT重定向,CGI可以通过管道cgi_output,将输出发送给父进程
    • 通过STDIN重定向,CGI可以通过管道cgi_input,从父进程读取数据
  2. 关闭不需要的管道端口

  3. 设置环境变量 REAUEST_METHOD、QUERY_STRING、CONTENT_LENGTH

  4. 执行CGI脚本

父进程

需要从子进程读CGI执行后的输出,所以要留子进程输出管道的读端

同时要写入子进程的输入管道

  1. 重定向标准输入输出
    • dup2(cgi_output[1], STDOUT);:将标准输出(STDOUT)重定向到cgi_output管道的写端,这样CGI脚本的输出可以通过管道发送给父进程。
    • dup2(cgi_input[0], STDIN);:将标准输入(STDIN)重定向到cgi_input管道的读端,允许CGI脚本从父进程读取数据(例如POST请求的正文)。
  2. 关闭不需要的管道端
    • 子进程关闭cgi_output[0](管道的读端)和cgi_input[1](管道的写端),因为子进程只需要写入输出并读取输入。
  3. 设置环境变量
    • 设置REQUEST_METHOD环境变量为HTTP请求方法(GET或POST)。
    • 如果是GET请求,设置QUERY_STRING环境变量为URL中的查询字符串。
    • 如果是POST请求,设置CONTENT_LENGTH环境变量为请求体的长度。
  4. 执行CGI脚本
    • 使用execl函数执行指定路径的CGI脚本。execl函数会替换当前进程的映像为指定的程序,这里就是CGI脚本。
    • 执行完毕后,子进程通过exit(0)正常退出。

补充

虽然socket描述在该项目中只是简单的整数,但这只是标识符,真正携带网络会话信息的是OS底层数据结构,描述符使程序能通过标准API与底层结构进行交互

详细介绍:

描述符实际上是操作系统提供的一个抽象,它代表了一个网络通信会话的端点。操作系统通过这个描述符来管理所有与该会话相关的信息,包括协议类型(如TCP或UDP)、本地和远程的IP地址和端口号、连接状态、缓冲区信息等。


CPP重构版

正片开始!

重构策略

  • 面向对象设计:每个类有明确的职责,通过类和对象的组合来完成服务器的功能。
  • 线程池:为了提高效率,可以使用线程池来管理ConnectionHandler的实例,避免频繁创建和销毁线程的开销。
  • 异常处理:使用C++异常处理机制来处理错误,提高代码的健壮性。
  • 智能指针:使用智能指针管理动态分配的内存,减少内存泄漏的风险。
  • STL:使用标准模板库(STL)提供的数据结构和算法,如std::vector, std::map等。

主体设计结构

main 函数

#include <iostream>
#include <memory>
#include <thread>
#include "Server.h"
#include "ConnectionHandler.h"

int main() {
    try {
        Server server(4000); // 创建Server实例,监听端口4000
        server.start(); // 启动服务器,开始监听端口

        std::cout << "Server started on port 4000" << std::endl;

        while (true) {
            // 等待并接受客户端连接
            int clientSocket = server.acceptConnection();
            if (clientSocket < 0) {
                std::cerr << "Failed to accept client connection" << std::endl;
                continue;
            }

            // 使用智能指针管理ConnectionHandler,确保资源正确释放
            std::shared_ptr<ConnectionHandler> handler(new ConnectionHandler(clientSocket));

            // 创建一个线程来处理连接,实现并发处理
            std::thread([handler]() {
                handler->handleRequest();
            }).detach(); // 将线程分离,让它独立执行
        }
    } catch (const std::exception& e) {
        std::cerr << "Exception caught in main: " << e.what() << std::endl;
    } catch (...) {
        std::cerr << "Unknown exception caught in main" << std::endl;
    }

    return 0;
}

注意,这里的Lambda表达式捕获时使用的是值捕获,如果是引用捕获,中间shared_ptr如果执行或修改了参数,那么表达式内部也会发生变化(socket描述符发生变化,通道无法建立)

同时,不能直接使往thread传入函数指针,因为这里调用的是对象的成员函数,因此传入指针前必须先捕获对象,这就要求使用lambda表达式了

1. Server类

  • 职责:负责服务器的启动、监听和关闭。

  • 方法

    • start(): 配置服务器,绑定端口,监听连接。
    • acceptConnection(): 等待并接受客户端连接。
    • shutdown(): 关闭服务器。

2. ConnectionHandler类

  • 职责:处理单个客户端连接。

  • 方法

    • handleRequest(): 处理客户端的HTTP请求。
    • sendResponse(): 发送HTTP响应给客户端。

3. Request类

  • 职责:解析和存储HTTP请求信息。

  • 属性

    • HTTP方法(GET、POST等)
    • URL
    • 查询字符串
    • 报文头
    • 报文体
  • 方法

    • parseRequest(): 从客户端连接中读取并解析HTTP请求。

问题:有必要去新建一个Request类吗

还是说我可以把所有的成员变量和函数都设置为static

不需要实例化即可调用

4. Response类

  • 职责:构建和存储HTTP响应信息。

  • 方法

    • setHeader(): 设置响应头。
    • setBody(): 设置响应体。
    • send(): 发送响应给客户端。

5. CGIHandler类

  • 职责:执行CGI脚本并处理其输出。

  • 方法

    • execute(): 执行CGI脚本,传递必要的环境变量和输入数据,并捕获输出。

6. Router类

  • 职责:根据请求的URL决定如何处理请求(静态资源服务或CGI执行)。

  • 方法

    • routeRequest(): 确定请求是请求静态资源还是需要执行CGI。

7. Logger类

  • 职责:提供日志记录功能。

  • 方法

    • log(): 记录日志信息。

这只是第一版草案,后续已经更新了

参考资料

Tinyhttpd仓库(fork的)

socket库文档

地址库文档

附上本项目仓库

文章作者: P4ul
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 打工人驿站
后端 操作系统 github c++
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝