I tried to rewrite the WEB server of the normal Linux programming 1st edition with C ++ 14

Introduction

In the latter half of the first edition of normal Linux programming, there is a chapter to create a web server (littiehttpd) in C language. I rewrote this WEB server with C ++ 14. Although it is free software, since the book is copyrighted, the part that has not been rewritten in C ++ is not described. The source comment only describes the rewritten part. For more details and unclear points that this article alone is not a working source, please refer to the first edition of normal Linux programming.

environment

Makefile In the book, it is explained that Makefile is automatically generated by autoconf, but I did not adopt it because it is difficult to create it unless I am accustomed to it. Instead, I decided to automatically generate a Makefile with CMake, which was pushed hard by Qiita.

CMakeLists.txt


add_executable(littlehttpd++
  main.cpp)
set(CMAKE_VERBOSE_MAKEFILE FALSE)
set(CMAKE_CXX_COMPILER /usr/bin/clang++)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I ./")
set(CMAKE_CXX_STANDARD 14)

struct HTTPRequest Added smart pointer typedef. I changed the string to std :: string and the linked list to std :: list. Since it is a smart pointer, free_request (HTTPRequest *) is not necessary.

main.cpp


// Declaration
struct HTTPHeaderField {
  typedef std::shared_ptr<HTTPHeaderField> sptr;
  std::string name;
  std::string value;
  //next is std::list<sptr>It is unnecessary because it is a list of
};

struct HTTPRequest {
  typedef std::shared_ptr<HTTPRequest> sptr;
  int32_t protocol_minor_version;
  std::string method;
  std::string path;
  std::list<HTTPHeaderField::sptr> headers;
  std::string body;
  int32_t length;
  std::string lookup_header_field_value(std::string name) const;
};

std::string HTTPRequest::lookup_header_field_value(std::string name) const {
  for (auto h : this->headers) {
    if (name == h->name) {
      return h->value;
    }
  }
  return "";
}

main() There are two changes: I changed the docroot and port variables to std :: string, and ended with std :: quick_exit ().

main.cpp


int main(int argc, char* argv[])
……abridgement……
  std::string port = DEFAULT_PORT;
  std::string docroot;
……abridgement……
    ……
  server_main(server, docroot);
  std::quick_exit(EXIT_SUCCESS);
}

Logging

In C ++, it does not expand variadic at run time, but at compile time. Format specification (% s etc.) reboots due to a specification error, so let the compiler determine the type.

main.cpp


static void log_exit() {
  std::quick_exit(EXIT_FAILURE);
}

static void log_exit(std::string str) {
  if (debug_mode) {
    std::cerr << str << std::endl;
  } else {
    syslog(LOG_ERR, str.c_str());
  }
  log_exit();
}

template <class Tail>
static void log_exit(std::stringstream &ss, Tail tail) {
  ss << tail;
  if (debug_mode) {
    std::cerr << ss.str() << std::endl;
  } else {
    syslog(LOG_ERR, ss.str().c_str());
  }
  log_exit();
}
template <class Head, class... Tail>
static void log_exit(std::stringstream &ss, Head head, Tail... tail) {
  ss << head;
  log_exit(ss, tail...);
}

template <class Head, class... Tail>
static void log_exit(Head head, Tail... tail) {
   std::stringstream ss;
   log_exit(ss, head, tail...);
}

signal

Uses std :: thread instead of fork (), so child process related signals are unnecessary

listen_socket Only changed the argument port to std :: string.

main.cpp


static int listen_socket(std::string &port){
……abridgement……
}

server_main Accept () and fork () are the same as books. Parallel processing with std :: thread instead of fork ().

main.cpp


static void server_main(int server, std::string &docroot) {
……abridgement……

    //sock std::Pass to thread(be bound), Thread processing
    std::thread th([=](intsock,std::stringdocroot) {
      fdBuf sbuf(sock);
      try {
        std::istream ins(&sbuf);
        std::ostream outs(&sbuf);

        service(ins, outs, docroot);
      } catch (interrupt_error e) {
        if (debug_mode) {
          std::cout << e.what() << std::endl;
        }
      }
      //Close with fdBuf destructor
      //close(sock);
      return;
    }, sock, docroot);
    th.detach();
  } // end-for
}

The fdBuf class is a streamBuffer class for processing sockets with iostream. The simplest implementation is used to check the operation, and the processing is slow. If you rewrite here, the processing speed will increase.

main.cpp


class fdBuf : public std::streambuf {
public:
  fdBuf(int sock);
  virtual ~fdBuf(void);
  int state(void) const;
static const int ERROR = -1;
static const int CLOSE_EOF = -2;
protected:
  int underflow(void) override;
  int uflow(void) override;
  int overflow(int c = std::char_traits<char>::eof()) override;
  int sync(void) override;
  int m_socket;
  bool m_gpend;
  int m_gpend_char;
  bool m_isEof;
  auto ceof(void) { return std::char_traits<char>::eof(); }
};

fdBuf::fdBuf(int sock) {
  this->m_gpend = false;
  m_gpend_char = ceof();
  //dup()Is not thread safe and is not needed
  //m_socket = ::dup(sock);
  m_socket = sock;
  m_isEof = false;
}

fdBuf::~fdBuf() {
  if (m_socket != -1) {
    ::close(m_socket);
  }
}

int fdBuf::state(void) const {
  if (m_socket == -1) {
    return fdBuf::ERROR;
  }
  if (m_isEof) {
    return fdBuf::CLOSE_EOF;
  }
  return 0;
}

int fdBuf::underflow(void) /*override*/ {
  //Simple implementation for checking operation and slow
  unsigned char buf[2] = {0};
  int ret = 0;
  if (m_gpend) { return m_gpend_char; }
  if (m_socket == -1) {
    return ceof();
  }
  ret = ::recv(m_socket, buf, 1, 0);
  if (ret == -1) {
    return ceof();
  } else if (ret == 0) {
    m_isEof = true;
    return ceof();
  } else if (ret != 1) {
    return ceof();
  }
  int c = (int)buf[0];
  m_gpend = true;
  m_gpend_char = (c & 255);
  return m_gpend_char;
}

int fdBuf::uflow(void) /*override*/ {
  int c = this->underflow();
  m_gpend = false;
  return c;
}

int fdBuf::overflow(int c) /*override*/ {
  if (c == ceof()) { return 0; }
  if (m_socket == -1) { return ceof(); }
  //Simple implementation and slow to check operation
  unsigned char buf[2] = { 0 };
  buf[0] = (unsigned char)(c & 255);
  if (::send(m_socket, buf, 1, 0) != 1) {
    return ceof();
  }
  return c;
}

int fdBuf::sync(void) /*override*/ {
  return 0;
}

service() Since HTTPRequest is a smart pointer, there is no need to call free ().

main.cpp


static void service(std::istream &in, std::ostream &out, std::string docroot) {
  HTTPRequest::sptr req = read_request(in);
  response_to(req, out, docroot);
}

read_request() Headers and body are not used. Implemented based on the policy of rewriting book processing to C ++.

main.cpp


static HTTPRequest::sptr read_request(std::istream &in) {
  try {
    HTTPRequest::sptr req = std::make_shared<HTTPRequest>();
    read_request_line(req, in);
    while (HTTPHeaderField::sptr h = read_header_field(in)) {
      req->headers.push_front(h);
    }
    req->length = content_length(req);
    if (req->length != 0) {
      if (req->length > MAX_REQUEST_BODY_LENGTH) {
        log_exit("request body too long");
        return nullptr;
      }
      req->body.reserve(req->length);
      in.read(const_cast<char *>(req->body.data()), req->length);
      if (!(in.rdstate() & std::ios::eofbit) ||
          (req->body.length() != req->length)) {
        log_exit("failed to read request body");
        return nullptr;
      }
    } else {
      req->body.clear();
    }
    return req;
  } catch(std::bad_alloc e) {
    log_exit(e.what());
  } catch (interrupt_error e) {
    throw std::move(e);
  } catch (...) {
    log_exit("Unkonw Exception occured");
  }
  return nullptr;
}

read_request_line() You can use std :: getline () to get input without size restrictions. Istream :: getline () is used from the description of the book that it is dangerous to read unlimited input character strings from the outside.

main.cpp


static void read_request_line(HTTPRequest::sptr &req, std::istream &in) {
  std::string strbuf;
  std::string::size_type path = 0, p = 0;
  try {
    std::string::value_type buf[LINE_BUF_SIZE] = {0};
    in.getline(buf, LINE_BUF_SIZE);
    if (in.fail()) {
      if (dynamic_cast<fdBuf*>(in.rdbuf())->state() == fdBuf::CLOSE_EOF) {
        if (debug_mode) {
          std::cout << "fdBuf::CLOSE_EOF" << std::endl << std::flush;
        }
        throw interrupt_error("Client disconnected");
      }
      log_exit("istream.getline(buf,", LINE_BUF_SIZE, ") failed(2)");
      return ;
    }
    // GET /<path>/Search for blanks immediately after the method of
    strbuf = buf;
    p = strbuf.find_first_of(' ');
    if (p == std::string::npos) {
      log_exit("parse error on request line(1): ", strbuf);
      return;
    }

    req->method = strbuf.substr(0, p - 0);
    std::transform(req->method.begin(), req->method.end(),
                   req->method.begin(), toupper);
    p++;
    path = p;
    p = strbuf.find_first_of(' ', path);
    if (p == std::string::npos) {
      log_exit("parse error on request line(2): ", strbuf);
      return;
    }

    req->path = strbuf.substr(path, p - path);
    p++;
    //uppercase letter/HTTP1 without case sensitivity.X or compare
    std::string strHttp;
    strHttp = strbuf.substr(p);
    std::transform(strHttp.begin(), strHttp.end(),
                   strHttp.begin(), toupper);
    p = strHttp.find_first_of("HTTP/1.");
    if (p == std::string::npos) {
      log_exit("parse error on request line(3): ", strbuf);
      return;
    }
    std::string strVersion = strHttp.substr(strlen("HTTP/1."));
    req->protocol_minor_version = std::stoi(strVersion);
  } catch(interrupt_error e) {
    throw std::move(e);
  } catch(...) {
    log_exit(__func__, ": Exception occured");
  }
  return;
}

Defines an error class to terminate the thread when disconnecting from the client

main.cpp


class interrupt_error : public std::runtime_error {
public:
  interrupt_error(const std::string& message) : std::runtime_error(message) { }
  interrupt_error(const char *message) : std::runtime_error(message) { }
};

read_header_field()

main.cpp


static HTTPHeaderField::sptr read_header_field(std::istream &in) {
  std::string strbuf;
  std::string::size_type p = 0, tpos = 0;
  std::string::value_type buf[LINE_BUF_SIZE] = {0};
  in.getline(buf, LINE_BUF_SIZE);
  if (in.eof()) {
    return nullptr;
  }
  if (in.fail()) {
    log_exit("istream.getline(buf,", LINE_BUF_SIZE, ") failed(1)");
    return nullptr;
  }
  //istream::getline()Does not include line breaks
  strbuf = buf;
  if (strbuf[0] == 0 || strbuf[0] == '\r') {
    return nullptr;
  }
  p = strbuf.find_first_of(':');
  if (p == std::string::npos) {
    log_exit("parse error on request header field: ", strbuf);
    return nullptr;
  }
  HTTPHeaderField::sptr h = std::make_shared<HTTPHeaderField>();
  h->name = strbuf.substr(0, p - 0);
  ++p;
  while (1) {
    tpos = strbuf.find(" \t", p);
    if (tpos == std::string::npos) {
      break;
    }
    p = tpos;
    p += 2;
  }
  h->value = strbuf.substr(p);
  return h;
}

content_length()

main.cpp


static long content_length(HTTPRequest::sptr &req) {
  std::string val = req->lookup_header_field_value(FIELD_NAME_CONTENT_LENGTH);
  if (val.length() == 0) {
    return 0;
  }
  long len = std::stol(val);
  if (len < 0) {
    log_exit("nagative Content-Length value");
    return 0;
  }
  return len;
}

response_to()

main.cpp


static const std::string  METHOD_HEAD = "HEAD";
static const std::string  METHOD_GET = "GET";
static const std::string  METHOD_POST = "POST";

static void response_to(HTTPRequest::sptr &req, std::ostream &out,
                        std::string &docroot) {
  if (req->method == METHOD_GET) {
    do_file_response(req, out, docroot);
  } else if (req->method == METHOD_HEAD) {
    do_file_response(req, out, docroot);
  } else if (req->method == METHOD_POST) {
    method_not_allowed(req, out);
  } else {
    not_implemented(req, out);
  }
}

do_file_response() FileInfo is a smart pointer, so free () is not done.

main.cpp


static const std::string  FIELD_NAME_CONTENT_LENGTH = "Content-Length";
static const std::string  FIELD_NAME_CONTENT_TYPE = "Content-Type";
static const std::string HTTP_OK = "200 OK";
  ……
static void do_file_response(HTTPRequest::sptr &req, std::ostream &out,
                             std::string &docroot) {
    FileInfo::sptr info = get_fileinfo(docroot, req->path);
    if (!(info->ok)) {
      not_found(req, out);
      return;
    }

    out_put_common_header_fields(req, out, HTTP_OK);
    out_one_line(out, FIELD_NAME_CONTENT_LENGTH, ": ", info->size);
    out_one_line(out, FIELD_NAME_CONTENT_TYPE, ": ", guess_content_type(info));
    out_one_line(out);
    if (req->method != METHOD_HEAD) {
      std::ifstream ifs(info->path);
      if (!ifs) {
        log_exit("failed to open ", info->path, ": ", strerror(errno));
        return;
      }
      // istream_iterator skips whitespace and line breaks
      std::copy(std::istreambuf_iterator<std::ifstream::char_type>(ifs), 
                std::istreambuf_iterator<std::ifstream::char_type>(),
                std::ostreambuf_iterator<std::ostream::char_type>(out));
      out << std::flush;
    }
    return;
}

In the book, \ r \ n was written at the end of the response string every time and output by fprintf. I created an output function for the response.

main.cpp


template <typename Stream>
static void out_one_line(Stream &stream) {
  stream << "\r\n";
}

template <typename Stream, class Tail>
static void out_one_line(Stream &stream, Tail tail) {
  stream << tail;
  out_one_line(stream);
}

template <typename Stream, class Head, class... Tail>
static void out_one_line(Stream &stream, Head head, Tail... tail) {
  stream << head;
  out_one_line(stream, tail...);
}

get_fileinfo() No change except that FileInfo is a smart pointer.

main.cpp


static FileInfo::sptr get_fileinfo(std::string &docroot, std::string &urlpath) {
   try {
     struct stat st;
     FileInfo::sptr info = std::make_shared<FileInfo>();
     info->path = build_fspath(docroot, urlpath);
……abridgement……
     return info;
   } catch(std::bad_alloc e) {
     log_exit(e.what());
   }
   return nullptr;
}

build_fspath() Although it is not in the book, a check for specifying the path outside the DOC Root and a process to change the file name to "index.html" when the path ends with'/' are added.

main.cpp


static std::string build_fspath(std::string &docroot, std::string &urlpath) {
  std::string::size_type pos = 0;
  std::string::size_type pos1 = 0;
  int32_t cnt = 0;
  char fs = '/';
  std::string upDir = "../";
  //Check outside document root
  pos = urlpath.find_first_of(fs);
  // '/'Start from
  if (pos == 0) {
    pos1= 1;
  }
  while (1) {
    pos = urlpath.find_first_of(fs, pos1);
    if (pos == std::string::npos) { break; }
    std::string dir =  urlpath.substr(pos1, pos - pos1 + 1);
    if (dir == upDir) {
      --cnt;
    } else {
      ++cnt;
    }
    if (cnt < 0) {
      log_exit("Invalid url path: ", urlpath);
    }
    ++pos;
    pos1 = pos;
  }
  std::string path = docroot;
  if (path.back() == '/') {
    path.pop_back();
  }
  if (urlpath.front() != '/') {
    path += "/";
  }
  path += urlpath;
  //path is"/"Index when ending with.Let it be html
  if (path.back() == '/') {
    path += "index.html";
  }
  return path;
}

out_put_common_header_fields() Since C ++ 20 corresponds to the day of the week with std :: chrono, the character string creation of the date and time is the same implementation as the book.

main.cpp


static const std::string  SERVER_NAME = ……
static const std::string  SERVER_VERSION = ……

static void 
out_put_common_header_fields(HTTPRequest::sptr &req, std::ostream &out,
                             const std::string &status) {
……abridgement……
  out_one_line(out, "HTTP/1.", HTTP_MINOR_VERSION, " ", status);
  out_one_line(out, "Date: ", buf);
  out_one_line(out, "Server: ", SERVER_NAME, "/", SERVER_VERSION);
  out_one_line(out, "Connection: close");
}

guess_content_type() In the book, text / plain was returned fixedly, but I implemented the minimum so that it can be displayed correctly in the browser.

main.cpp


static std::string guess_content_type(FileInfo::sptr &info) {
  static std::map<std::string, std::string> mimeMap 
    = { { "html", "text/html" }, { "htm", "text/html" }, {"js", "text/javascript"},
        {"jpg", "image/jpeg"}, {"jpeg", "image/jpeg"}, {"png", "image/png"},
        {"bmp", "image/bmp"}, {"gif", "image/gif"}, {"svg", "image/svg+xml"},
        {"css", "text/css"}, {"au", "audio/basic"}, {"wav", "audio/wav"},
        {"mpeg", "video/mpeg"}, {"mp3", "audio/mp3"}, {"ogg", "application/ogg"},
        {"pac","application/x-ns-proxy-autoconfig"} };
        
  int extPos = info->path.find_last_of(".");
  std::string extention = info->path.substr(extPos + 1, info->path.size() - extPos);
  if (mimeMap.count(extention) != 0) {
    return mimeMap.at(extention);
  }
  return "text/plain";
}

not_found()

main.cpp


static const std::string HTTP_NOT_FOUND = "404 Not Found";

static void not_found(HTTPRequest::sptr &req, std::ostream &out) {
  out_put_common_header_fields(req, out, HTTP_NOT_FOUND);
  out_one_line(out, FIELD_NAME_CONTENT_TYPE, ": text/html");
  out_one_line(out);
  if (req->method != METHOD_HEAD) {
    out_one_line(out, "<html>");
    out_one_line(out, "<header><title>Not Found</title></header>");
    out_one_line(out, "<body><p>File not found</p></body>");
    out_one_line(out, "</html>");
  }
  out << std::flush;
}

References

Recommended Posts

I tried to rewrite the WEB server of the normal Linux programming 1st edition with C ++ 14
I tried to find the entropy of the image with python
I tried to find the average of the sequence with TensorFlow
I tried to solve the ant book beginner's edition with python
[Linux] I tried to summarize the command of resource confirmation system
I tried to automate the watering of the planter with Raspberry Pi
I tried to expand the size of the logical volume with LVM
I tried to improve the efficiency of daily work with Python
I tried the asynchronous server of Django 3.0
I tried to get the authentication code of Qiita API with Python.
I tried to automatically extract the movements of PES players with software
I tried to analyze the negativeness of Nono Morikubo. [Compare with Posipa]
I tried to streamline the standard role of new employees with Python
I tried to visualize the text of the novel "Weathering with You" with WordCloud
[Linux] I tried to verify the secure confirmation method of FQDN (CentOS7)
I tried to get the movie information of TMDb API with Python
I tried to predict the behavior of the new coronavirus with the SEIR model.
I tried to make Othello AI with tensorflow without understanding the theory of machine learning ~ Battle Edition ~
I tried web scraping to analyze the lyrics.
I tried to save the data with discord
I tried to touch the API of ebay
I tried to correct the keystone of the image
I tried to operate Linux with Discord Bot
I tried to predict the price of ETF
I tried to vectorize the lyrics of Hinatazaka46!
I tried to easily visualize the tweets of JAWS DAYS 2017 with Python + ELK
The story of making soracom_exporter (I tried to monitor SORACOM Air with Prometheus)
I tried to create a model with the sample of Amazon SageMaker Autopilot
I tried to automatically send the literature of the new coronavirus to LINE with Python
I tried to learn the sin function with chainer
I tried to extract features with SIFT of OpenCV
I tried to touch the CSV file with Python
I tried to solve the soma cube with python
I tried to visualize the spacha information of VTuber
I tried to erase the negative part of Meros
I tried to solve the problem with Python Vol.1
I tried to classify the voices of voice actors
I tried to summarize the string operations of Python
I tried to make something like a chatbot with the Seq2Seq model of TensorFlow
[Linux] I learned LPIC lv1 in 10 days and tried to understand the mechanism of Linux.
I tried to put out the frequent word ranking of LINE talk with Python
I tried to automate the article update of Livedoor blog with Python and selenium.
I tried to visualize the characteristics of new coronavirus infected person information with wordcloud
I tried to visualize the running data of the racing game (Assetto Corsa) with Plotly
I tried to compare the processing speed with dplyr of R and pandas of Python
The 15th offline real-time I tried to solve the problem of how to write with python
[Horse Racing] I tried to quantify the strength of racehorses
I tried "gamma correction" of the image with Python + OpenCV
I tried to simulate how the infection spreads with Python
I tried to analyze the whole novel "Weathering with You" ☔️
I tried to display the point cloud data DB of Shizuoka prefecture with Vue + Leaflet
I tried to automatically post to ChatWork at the time of deployment with fabric and ChatWork Api
I wanted to solve the Panasonic Programming Contest 2020 with Python
I tried starting Django's server with VScode instead of Pycharm
How to write offline real time I tried to solve the problem of F02 with Python
I tried to notify the train delay information with LINE Notify
I tried to visualize the power consumption of my house with Nature Remo E lite
Check the memory status of the server with the Linux free command
I tried to move ROS (Melodic) with the first Raspberry Pi (Stretch) at the beginning of 2021
I tried to get the number of days of the month holidays (Saturdays, Sundays, and holidays) with python
Check the operating status of the server with the Linux top command