ふつうのLinuxプログラミング 第1版のWEBサーバーをC++14で書き直してみた


はじめに

ふつうのLinuxプログラミング 第1版の後半に、C言語でWEBサーバー(littiehttpd)を作成する章がある。
このWEBサーバーをC++14で書き直してみた。
フリーソフトだが書籍の著作権があるのでC++で書き直してない部分は記述しない。
ソースのコメントは書き直した部分に関する説明しか記述しない。
この記事だけでは動作するソースにはならない、詳細や不明点は、ふつうのLinuxプログラミング 第1版を参考にしてほしい。

環境

  • OS Window8.1 32bitメモリ1G
  • Cygwin 2.893
  • clang 8.0.1 (c++14指定)
  • CMake 3.14.5

Makefile

書籍ではautoconfでMakefileを自動生成するように説明しているが、慣れてないと作成が難しいので採用しなかった。
代わりにQiita激押しのCMakeでMakefileを自動生成することにした。

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

スマートポインタのtypedefを追加。文字列はstd::string、リンクリストはstd::listに変更した。
スマートポインタなのでfree_request( HTTPRequest* )は必要ない。

main.cpp
// Declaration
struct HTTPHeaderField {
  typedef std::shared_ptr<HTTPHeaderField> sptr;
  std::string name;
  std::string value;
  // nextはstd::list<sptr>のリストにするので不要
};

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()

変更点は、docrootとport変数をstd::stringに変更したのと、std::quick_exit()で終了の二点。

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

ロギング

C++では、実行時に可変引数を展開せずに、コンパイル時に展開する。
書式指定(%s等)は、指定ミスでリブートするので、コンパイラに型を判定させる

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...);
}

シグナル

fork()ではなく、std::threadを使うので、子プロセス関連のシグナルは不用 

listen_socket

引数portをstd::stringに変更したのみ。

main.cpp
static int listen_socket(std::string &port){
  ……省略……
}

server_main

accept()して、fork()までは書籍と同じ。fork()ではなく、std::threadで並列処理する。

main.cpp
static void server_main(int server, std::string &docroot) {
  ……省略……

    // sockをstd::threadに渡して(bindされる)、スレッド処理
    std::thread th([=](int sock, std::string docroot) {
      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;
        }
      }
      // fdBufのデストラクタでcloseする
      //close(sock);
      return;
    }, sock, docroot);
    th.detach();
  } // end-for
}

fdBufクラスはソケットをiostreamで処理するためのstreamBufferクラス。
動作確認の為、最も簡単な実装にしており処理が遅い。ここを書き直すと処理速度が早くなる。

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()はスレッドセーフではないし必要ない
  //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*/ {
  // 動作確認の為の単純な実装であり遅い
  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(); }
  // 動作確認の為に単純な実装であり遅い
  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()

HTTPRequestはスマートポインタなので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やbodyは使用していない。書籍の処理を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;
      }
      int read_bytes = 0;
      while (read_bytes < req->length) {
        std::vector<char> buf;
        buf.resize(req->length + 1, 0);
        in.read(&buf[0], req->length);
        int rc = in.gcount();
        if (rc == 0 && read_bytes != 0) { break; }
        if (!in.good()) {
          log_exit("failed to read request body");
          return nullptr;
        }
        if (rc == 0) { in.clear(); }
        else {
          read_bytes += rc;
          req->body += &buf[0];
        }
        if (in.eof()) { break; }
      }
      if (req->body.length() < req->length) {
        std::string str_err = "mismatch contnet length=" + std::to_string(req->length)
                                + ",body.length()=" + std::to_string(req->body.length());
        throw interrupt_error(str_err);
      }
    } 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()

std::getline()を使えば、サイズ制限なしに入力を取得できる。外部からの入力文字列を無制限に読み込むのは危険との書籍の記述からistream:: getline()を使っている。

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>/のメゾッド直後の空白の検索
    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++;
    // 大文字/小文字区別なしでHTTP1.Xか比較
    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;
}

クライアントからの切断でスレッドを終了する為のエラークラスを定義している

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()では改行は含まれない
  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はスマートポインタなのでfree()はしていない。

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は空白、改行を読み飛ばす
      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;
}

書籍では、レスポンス文字列の最後に¥r¥nを毎回記述してfprintfで出力していた。
レスポンス用の出力関数を作成した。

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()

FileInfoをスマートポインタにした以外は変更なし。

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);
     ……省略……
     return info;
   } catch(std::bad_alloc e) {
     log_exit(e.what());
   }
   return nullptr;
}

build_fspath()

書籍にはないが、パスのDOC Root外指定のチェックと、パスが'/'で終わってる時にファイル名を"index.html"とする処理を追加している

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 = "../";
  // document root 外のチェック
  pos = urlpath.find_first_of(fs);
  // '/'から開始
  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が"/"で終わっている時は index.htmlとする
  if (path.back() == '/') {
    path += "index.html";
  }
  return path;
}

out_put_common_header_fields()

std::chronoで曜日に対応したのがC++20なので、日時の文字列作成は書籍と同じ実装。

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) {
  ……省略……
  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()

書籍ではtext/plainを固定で返してたが、ブラウザで正しく表示されるように最低限の実装をした。

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;
}

 参考文献

  • 青木 峰郎 ふつうのLinuxプログラミング第一版 ソフトバンクパブリッシング
  • C++ iostream 実装例 その 1
  • επιστημη 高橋晶 C++テンプレートテクニック 第2版 ソフトバンククリエイティブ
  • Rip Tutorial
  • 小松正樹 C++11/14/17プログラミング kindle版