从零开始写一个简单的httpserver

个人博客:Sekyoro的博客小屋
个人网站:Proanimer的个人网站
使用c++基于socket库编写了一个简单的httpserver,主要作用是从请求头中解析字段并返回响应数据,并使用zlib库支持gzip压缩,
这里顺便推荐一下CodeCrafters平台,有许多练习,同时可以使用接近二十种语言.
在这里插入图片描述

#include "zlib.h"
#include <algorithm>
#include <arpa/inet.h>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <netdb.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <thread>
#include <unistd.h>
#include <vector>

std::string compressData(const char *input, unsigned long inputLEN,
                         char *&output, unsigned long &outputLEN) {
  z_stream defstream;
  defstream.zalloc = Z_NULL;
  defstream.zfree = Z_NULL;
  defstream.opaque = Z_NULL;

  int ret = deflateInit2(&defstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16,
                         8, Z_DEFAULT_STRATEGY);
  if (ret != Z_OK) {
    std::cerr << "Failed to initialize defstream\n";
    return "";
  }

  defstream.next_in = (Bytef *)input;
  defstream.avail_in = inputLEN;
  std::string outStr;
  // 压缩缓冲区的初始大小
  unsigned long compressedSize{32768};
  output = new char[compressedSize];
  do {
    defstream.avail_out = sizeof(output);
    defstream.next_out = reinterpret_cast<Bytef *>(output);

    ret = deflate(&defstream, Z_FINISH);
    if (outStr.length() < defstream.total_out) {
      outStr.append(output, defstream.total_out - outStr.length());
    }
  } while (ret == Z_OK);

  outputLEN = defstream.total_out;

  deflateEnd(&defstream);
  if (ret != Z_STREAM_END) {
    throw(std::runtime_error("Exception during zlib compression: " +
                             std::to_string(ret)));
  }
  return outStr;
}

char *toLower(const char *str) {
  char *lowerStr = new char[strlen(str) + 1];
  lowerStr[strlen(str)] = '\0';
  int n = 0;
  while (n < strlen(str)) {
    lowerStr[n] = std::tolower(str[n]);
    n++;
  }
  return lowerStr;
}

char *strstrInsentive(const char *haystack, const char *needle) {
  char *lowerHaystack = toLower(haystack);
  char *lowerNeedle = toLower(needle);
  return strstr(lowerHaystack, lowerNeedle);
}
std::string parseHeaderField(const char *header, const char *fieldName) {
  char *fieldStart = strstrInsentive(header, fieldName);
  std::string fieldValue;
  if (fieldStart != nullptr) {
    const char *fieldEnd = strstr(fieldStart, "\r\n");
    fieldStart += strlen(fieldName);
    std::string::size_type length = fieldEnd - fieldStart;
    fieldValue.assign(fieldStart, length);
  } else {
    std::clog << "Failed to read " << std::string(fieldName)
              << " from client\n";
  }
  return fieldValue;
}
std::vector<std::string> parseHeaderFieldList(const char *header,
                                              const char *fieldName) {
  std::vector<std::string> fieldValues;
  char *fieldStart = strstrInsentive(header, fieldName);
  std::string fieldValue;
  if (fieldStart != nullptr) {
    fieldStart += strlen(fieldName);
    while (strlen(fieldStart) > 0) {
      const char *fieldEnd = strstr(fieldStart, ", ");
      if (fieldEnd == nullptr) {
        fieldEnd = strstr(fieldStart, "\r\n");
        if (fieldEnd != nullptr) {
          std::string::size_type length = fieldEnd - fieldStart;
          fieldValue.assign(fieldStart, length);
          fieldValues.push_back(fieldValue);
          break;
        }
      } else {
        std::string::size_type length = fieldEnd - fieldStart;
        fieldValue.assign(fieldStart, length);
        fieldValues.push_back(fieldValue);
        fieldStart += length + strlen(", ");
      }
    }
  } else {
    std::clog << "Failed to read " << std::string(fieldName)
              << " from client\n";
  }
  return fieldValues;
}

void handle_client(int client_sock, const std::string dirPath) {
  const int bufsize = 1024;
  char buffer[bufsize] = {0};
  int valread;
  std::string pathName;
  std::string methodName;
  std::string userAgent;
  std::string accept;
  std::string contentType;
  std::string contentLenth;
  std::vector<std::string> acceptEncodings;
  char *requestBody = nullptr;
  if ((valread = recv(client_sock, (void *)&buffer, bufsize, 0)) > 0) {
    buffer[valread] = '\0'; // 截断请求
                            // 找到Request line
    std::clog << "Received buff length: " << valread << std::endl;
    // methodLine = strstr
    const char *methodLine = strchr(buffer, ' ');
    methodName.assign(buffer,
                      static_cast<std::string::size_type>(methodLine - buffer));
    if (methodLine != nullptr) {
      const char *pathStart = methodLine + 1;
      const char *pathEnd = strchr(pathStart, ' ');
      std::string::size_type length = pathEnd - pathStart;
      pathName.assign(pathStart, length);
    } else {
      std::clog << "Failed to read request line from client\n";
    }
    // headers
    char *header = strstr(buffer, "\r\n");
    if (header != nullptr) {
      userAgent = parseHeaderField(header, "User-Agent: ");
      accept = parseHeaderField(header, "Accept: ");
      contentType = parseHeaderField(header, "Content-Type: ");
      contentLenth = parseHeaderField(header, "Content-Length: ");
      acceptEncodings = parseHeaderFieldList(header, "Accept-Encoding: ");
    } else {
      std::clog << "Failed to read headers from client" << std::endl;
    }
    // request body
    char *headerEnd = strstr(header, "\r\n\r\n");
    if (headerEnd) {
      headerEnd = headerEnd + strlen("\r\n\r\n");
      if (strlen(headerEnd) > 0) {
        requestBody = headerEnd;
      }
    }
  }
  std::string echoRoute("/echo/");
  std::string userAgentRoute("/user-agent");
  std::string fileRoute("/files/");
  std::string message;
  std::cout << "path: " << pathName << std::endl;
  std::cout << "User-Agent: " << userAgent << std::endl;
  std::cout << "Accept-Encoding: ";
  for (const auto &encoding : acceptEncodings) {
    std::cout << encoding << ", ";
  }
  std::cout << std::endl;
  if (pathName == "/") {
    message = "HTTP/1.1 200 OK\r\n\r\n";
  } else if (pathName.starts_with(echoRoute)) {
    pathName = pathName.substr(echoRoute.length());
    // std::clog << "path: " << pathName << std::endl;
    // headers
    message = "HTTP/1.1 200 OK\r\n";
    // status line
    auto it = std::find(acceptEncodings.begin(), acceptEncodings.end(), "gzip");
    char *compredData = nullptr;
    std::string outputStr;
    bool compressed = false;
    unsigned long outputLEN{0};
    if (it != acceptEncodings.end()) {
      message += "Content-Encoding: gzip\r\n";
      outputStr = compressData(pathName.c_str(), pathName.length(), compredData,
                               outputLEN);
      compressed = true;
    }
    message += "Content-Type: text/plain\r\n";
    message += "Content-Length: " +
               std::to_string(compressed ? outputLEN : pathName.length()) +
               "\r\n";
    message += "\r\n";
    // response body
    message += compressed ? outputStr : pathName;
  } else if (pathName == userAgentRoute) {
    message = "HTTP/1.1 200 OK\r\n";
    auto it = std::find(acceptEncodings.begin(), acceptEncodings.end(), "gzip");
    if (it != acceptEncodings.end()) {
      message += "Content-Encoding: gzip\r\n";
    }

    message += "Content-Type: text/plain\r\n";
    message += "Content-Length: " + std::to_string(userAgent.length()) + "\r\n";
    message += "\r\n";
    message += userAgent;
  } else if (pathName.starts_with(fileRoute)) {
    std::string fileName = pathName.substr(fileRoute.length());
    if (dirPath.length() > 0) {
      fileName = dirPath + fileName;
    }
    // std::clog << "file: " << fileName << std::endl;
    if (methodName == "POST") {
      // 创建文件
      std::ofstream file(fileName);
      if (file.is_open()) {
        file.write(requestBody, strlen(requestBody));
        file.close();
        message = "HTTP/1.1 201 Created\r\n\r\n";
      } else {
        std::clog << "Failed to open file\n";
      }
    } else {
      std::filesystem::path filePath(fileName);
      if (!std::filesystem::exists(filePath)) {
        message = "HTTP/1.1 404 Not Found\r\n\r\n";
      } else {
        std::ifstream file(fileName);
        if (file.is_open()) {
          auto fileSize = std::filesystem::file_size(fileName);
          char fileBuf[static_cast<size_t>(fileSize)];
          file.read(fileBuf, fileSize);
          message += "HTTP/1.1 200 OK\r\n";
          auto it =
              std::find(acceptEncodings.begin(), acceptEncodings.end(), "gzip");
          if (it != acceptEncodings.end()) {
            message += "Content-Encoding: gzip\r\n";
          }
          message += "Content-Type: application/octet-stream\r\n";
          message += "Content-Length: " + std::to_string(fileSize) + "\r\n";
          message += "\r\n";
          message += fileBuf;
        } else {
          std::clog << "Failed to open file\n";
        }
      }
    }
  } else {
    message = "HTTP/1.1 404 Not Found\r\n\r\n";
  }
  size_t bsent = send(client_sock, message.c_str(), message.length(), 0);

  if (bsent != message.length()) {
    std::cerr << "Failed to send response\n";
  }
  // close(client_sock);
  std::cout << "Client connected\n";
}

int main(int argc, char **argv) {
  // Flush after every std::cout / std::cerr
  std::cout << std::unitbuf;
  std::cerr << std::unitbuf;
  std::string dirPath;
  if (argc > 1) {
    for (int i = 1; i < argc; i++) {
      std::cout << argv[i] << std::endl;
      if (strcmp(argv[i], "--directory") == 0) {
        dirPath.assign(argv[i + 1]);
      }
    }
  }
  // You can use print statements as follows for debugging, they'll be visible
  // when running tests.
  std::cout << "Logs from your program will appear here!\n";

  // Uncomment this block to pass the first stage
  //
  int server_fd = socket(AF_INET, SOCK_STREAM, 0);
  if (server_fd < 0) {
    std::cerr << "Failed to create server socket\n";
    return 1;
  }

  // Since the tester restarts your program quite often, setting SO_REUSEADDR
  // ensures that we don't run into 'Address already in use' errors
  int reuse = 1;
  if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) <
      0) {
    std::cerr << "setsockopt failed\n";
    return 1;
  }

  struct sockaddr_in server_addr;
  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = INADDR_ANY;
  server_addr.sin_port = htons(4221);

  if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) !=
      0) {
    std::cerr << "Failed to bind to port 4221\n";
    return 1;
  }

  int connection_backlog = 5;
  if (listen(server_fd, connection_backlog) != 0) {
    std::cerr << "listen failed\n";
    return 1;
  }

  struct sockaddr_in client_addr;
  int client_addr_len;
  std::cout << "Waiting for a client to connect...\n";

  // accept(server_fd, (struct sockaddr *)&client_addr,
  //        (socklen_t *)&client_addr_len);
  while (true) {
    client_addr_len = sizeof(client_addr);
    int client = accept(server_fd, (struct sockaddr *)&client_addr,
                        (socklen_t *)&client_addr_len);
    std::cout << "client port: " << client_addr.sin_port << std::endl;
    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
    std::cout << "client ip: " << client_ip << std::endl;
    if (client < 0) {
      std::cerr << "Failed to accept client connection\n";
      return 1;
    }
    std::thread clientThread(handle_client, client, dirPath);
    clientThread.detach();
  }
  // // Request line
  // GET                          // HTTP method
  // /index.html                  // Request target
  // HTTP/1.1                     // HTTP version
  // \r\n                         // CRLF that marks the end of the request
  // line
  //
  // // Headers
  // Host: localhost:4221\r\n     // Header that specifies the server's host
  // and port User-Agent: curl/7.64.1\r\n  // Header that describes the
  // client's user agent accept: */*\r\n              // Header that specifies
  // which media types the client can accept \r\n                         //
  // CRLF that marks the end of the headers
  //
  // Request body (empty)
  close(server_fd);
  return 0;
}

如有疑问,欢迎各位交流!

服务器配置
宝塔:宝塔服务器面板,一键全能部署及管理
云服务器:阿里云服务器
Vultr服务器
GPU服务器:Vast.ai
编码练习平台
CodeCrafters CodeCrafters

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

procoder338

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值