2018年9月29日

C++ による non-blocking socket を用いた http サーバの実装

Abstract
  http サーバは http daemon (httpd) とも呼ばれ,socket 通信を用いてブラウザと通信するサーバ側のプログラムである.以前は,接続数と同じ数のプロセスを立ち上げ,blocking を行う実装が一般的であったが,nginx (エンジンエックス) [1] の登場以来,特に静的な web ページでは,non-blocking な実装により,1 つのプロセスで複数の接続を処理することで,コンテキストスイッチと呼ばれるプロセス切り替えのオーバーヘッドを削減する手法が広がっている.しかしながら,non-blocking の実装サンプルは少なく,実装しても些細なミスで致命的に動作せず,また,存在するサンプルも,httpd の実装と,socket 回りの実装が密結合となっており,分離が難しいことが分かる.加えて,nginx のソースコードも,動作を理解するには,非常に大規模で複雑であった.したがって,本投稿では,定数時間でイベントを監視可能な epoll を用いて,httpd の socket 通信部分と httpd 部分とが疎結合となる見通しのよいサンプルを実装する.結果として,簡単な HTML と CSS を用いた画像付きのサンプルページを配信可能な httpd を実装した.また,request URL の確認において,hash table を用いた照合によりパスを確認することで,ディレクトリトラバーサル [2] のような,古典的で基本的なセキュリティ問題に対処した.
Introduction
  nginx 登場以前の Appach など,プロセス駆動型のサーバでは,1 リクエストごとにプロセスを生成し,各々 blocking しすることで通信を待機していた.このようなプロセス駆動型の戦略は,サーバのシンプルな実装を可能とし,接続時間の長い動的な web ページでは,他の接続を考慮することなくイベントを処理できる利点がある.しかし,同時接続数が増え,物理 CPU の数を上回るイベントを処理しようとすると,コンテキストスイッチによるオーバーヘッドは非常に大きくなった.そこで,nginx は,CPU の数以上のプロセスを立ち上げず,代わりに socket を non-blocking とすることで,1 つのプロセスで複数のイベントを処理した.このとき,プロセス数は CPU コア数以下となるため,プロセスを切り替える必要がなくなり,オーバヘッドはなくなる.このようなイベント駆動型の戦略は,接続時間の長い動的なページでは,他のクエリをブロックしてしまうため,利用できない一方,接続時間の短い静的な web ページにおいて,より多くの同時接続の処理を可能とする利点がある.
  non-blocking socket の実装には,いくつかの方法があるが,中でも Linux の epoll() は,実際に発生したイベントのリストを返却するため,バッファ内の全てのイベントを線形探査する必要がなく,定数時間で処理される.
Previous research
  select() を用いた httpd の実装には [3] が挙げられる.select() を用いた実装では,イベントを線形探査する必要が生じるため,
...ellipsis...

/* Wait until one or more fds are ready to read */
select(maxfd+1, &readset, NULL, NULL, NULL);

/* Process all of the fds that are still set in readset */
for (i=0; i < n_sockets; ++i) {
    if (FD_ISSET(fd[i], &readset)) {
        n = recv(fd[i], buf, sizeof(buf), 0);
        if (n == 0) {
            handle_close(fd[i]);
        } else if (n < 0) {
            if (errno == EAGAIN)
                ; /* The kernel didn't have any data for us to read. */
            else
                handle_error(fd[i], errno);
        } else {
            handle_input(fd[i], buf, n);
        }
    }
}                                                                                                    [3]
...ellipsis...
のように,n_sockets (イベントの上限) まで探索を行っている.当然,これまで出現した feed 値の最大値までの探査に制限すれば,ある程度の探査を削減はできる.しかしながら,接続中の feed 値が飛び値と取る限り,不要な探索は避けられない.また,最大イベント数が FD_SETSIZE (通常 OS のコンパイル時に 1024 とされる[6]) に制限される.ただし,select() を用いた実装には,Linux 以外に FreeBSD でも利用できる利点がある.
  この問題を避けるため,Linux は epoll() という独自実装を持つ.epoll() を用いた non-blocking socket の実装には [4][5] がある.ここでは,
...ellipsis...

int nready = epoll_wait(epollfd, events, MAXFDS, -1);
for (int i = 0; i < nready; i++) {
    if (events[i].events & EPOLLERR) {
        perror_die("epoll_wait returned EPOLLERR");
    }

    if (events[i].data.fd == listener_sockfd) {
        // The listening socket is ready; this means a new peer is connecting.

        struct sockaddr_in peer_addr;
        socklen_t peer_addr_len = sizeof(peer_addr);
        int newsockfd = accept(listener_sockfd, (struct sockaddr*)&peer_addr,
                               &peer_addr_len);
        if (newsockfd < 0) {
            ...ellipsis....
        } else {
            make_socket_non_blocking(newsockfd);
            ...ellipsis....
        }
    } else {
        // A peer socket is ready.
        if (events[i].events & EPOLLIN) {
            ...ellipsis....
        } else if (events[i].events & EPOLLOUT) {
            ...ellipsis....
        }
    }
}                                                                                                    [5]
...ellipsis...
となる.ここでは,epoll_wait() の指示する nready 回だけ for 文を回しており,配列 events には先頭から読み込みが必要な nready 個だけ値が更新される.このように epoll() では定数時間で効率的にイベントを処理することができる.また,select() のように最大イベント数に上限がない.ただし,epoll() は Linux 以外で利用できない.
Purpose
  簡易 httpd のサンプルは数多く存在するが,完動するサンプルは少なく,全容を理解し難い.また,non-blocking 型のサンプルは,通信部分とサーバ部分が密結合な実装が多く,見通しが悪い.したがって,この投稿では疎結合かつ httpd として完動するサンプルを実装したい.そこで,明確な目標として,画像 1 枚を含む,簡単な web ページを配信可能なサーバの実装を目指す.
Method/Algorism
  実装としては,イベント駆動型の httpd を epoll() を用いた non-blocking socket として実装する.また,予め hash table に配信を許可するディレクトリの一覧を格納し,URL が hash table に含まれるか照合することで,ディレクトリトラバーサルへ対処するように実装した.
Implementation
  下記に実際の実装を示す.

  ./sstd_tcp/server.hpp
#pragma once
#include <sstd/sstd.hpp>

#include <netinet/in.h> // for sockaddr_in
#include <sys/epoll.h>  // for epoll

//-----------------------------------------------------------------------------------------------------------------------------------------------

struct fdState{
private:
public:
    fdState();
    ~fdState();
    
    std::string writeMsg;
};

//-----------------------------------------------------------------------------------------------------------------------------------------------

namespace sstd{ class tcpSrv_nonblocking; }

class sstd::tcpSrv_nonblocking{
private:
    // settings
    int numOfListenLimits;
    uint maxEvents;

    // for init
    struct sockaddr_in sIn;
    
    // for all
    int  sock;              // listening socket
    int  cFd;               // current feed
    int  eNum;              // event number
    uint sNum;              // state number
    struct fdState* pState; // status set
    
    // for epoll
    int epFd;                    // epoll feed
    int nFd;                     // number of feeds
    int fd_offset;               // offset of feed number
    struct epoll_event* pEvents; // events set
    struct epoll_event cEv;      // current epoll event

    // for event loop
    bool isProc;
    
    bool new_fd();
    bool del_fd(int delFd);
    bool workerRecv(std::vector<uchar>& retOut, const uint& limitSize);
    bool printInfo_fd(const struct sockaddr_storage& ss, socklen_t& ssLen);
    
    bool isEvent();
    bool ctlState_setR (const int& setFd); // control state: set read
    bool ctlState_setW (const int& setFd); // control state: set write
    bool ctlState_setRW(const int& setFd); // control state: set read and write
    bool ctlState_rmR  (const int&  rmFd); // control state: remove read
    bool ctlState_rmW  (const int&  rmFd); // control state: remove write
    bool ctlState_rmRW (const int&  rmFd); // control state: remove read and write
    bool ctlState_addR (const int& addFd); // control state: add read
    bool ctlState_addW (const int& addFd); // control state: add write
    bool ctlState_addRW(const int& addFd); // control state: add read and write
    
public:
    tcpSrv_nonblocking(const uint maxEvents_in);
    ~tcpSrv_nonblocking();
    
    bool open(const uint portNum);
    
    bool wait();
    bool isRedy4recv();
    bool isRedy4send();
    
    bool recv(std::vector<uchar>& retOut, const uint& limitSize);
    bool setSend(std::string& msg);
    bool setSend(const char*& pMsg);
    bool send();
};

  ./sstd_tcp/server.cpp
#include "server.hpp"
#include <sys/socket.h>
#include <fcntl.h>
#include <netdb.h>
#include <sstd/sstd.hpp>

//-----------------------------------------------------------------------------------------------------------------------------------------------

fdState::fdState(){
    this->writeMsg = "";
}
fdState::~fdState(){}

//-----------------------------------------------------------------------------------------------------------------------------------------------

sstd::tcpSrv_nonblocking::tcpSrv_nonblocking(const uint maxEvents_in){
    this->eNum = 0;
    this->nFd  = 0;
    this->numOfListenLimits = 16;
    this->isProc = false;

    this->maxEvents = maxEvents_in;
    this->pState  = new struct fdState    [maxEvents];
    this->pEvents = new struct epoll_event[maxEvents];
}
sstd::tcpSrv_nonblocking::~tcpSrv_nonblocking(){
    delete[] this->pState;
    delete[] this->pEvents;
}

//---

bool sstd::tcpSrv_nonblocking::printInfo_fd(const struct sockaddr_storage& ss, socklen_t& ssLen){
    
    // get host (host name or its IP address) and port number
    char host[NI_MAXHOST];
    char port[NI_MAXSERV];
    int ret = getnameinfo((struct sockaddr*)&ss, ssLen, host, NI_MAXHOST, port, NI_MAXSERV, 0);
    if(ret!=0){
        if(ret==EAI_SYSTEM){ sstd::pdbg("ERROR: getnameinfo() failed: %s\n", strerror(errno)  );
        }       else       { sstd::pdbg("ERROR: getnameinfo() failed: %s\n", gai_strerror(ret)); }
        return false;
    }
    sstd::pdbg("info of feed: host: %s, port: %s\n", host, port);
        
    return true;
}

//---

bool sstd::tcpSrv_nonblocking::isEvent(){ return ((cEv.events & EPOLLIN) || (cEv.events & EPOLLOUT)); }
bool sstd::tcpSrv_nonblocking::ctlState_setR(const int& setFd){
    cEv={0};
    cEv.events = EPOLLIN;
    cEv.data.fd = setFd;
    if(epoll_ctl(epFd, EPOLL_CTL_ADD, setFd, &cEv)==-1){ sstd::pdbg("ERROR: epoll_ctl(): %s.\n", strerror(errno)); return false; }
    return true;
}
bool sstd::tcpSrv_nonblocking::ctlState_setW(const int& setFd){ return true; } // not implimented yet
bool sstd::tcpSrv_nonblocking::ctlState_setRW(const int& setFd){ return true; } // not implimented yet
bool sstd::tcpSrv_nonblocking::ctlState_rmR(const int& rmFd){
    cEv={0};
    cEv.data.fd = rmFd;
    if(pEvents[eNum].events & EPOLLOUT){ cEv.events |= EPOLLOUT; }
    if(epoll_ctl(epFd, EPOLL_CTL_MOD, rmFd, &cEv)==-1){ sstd::pdbg("ERROR: epoll_ctl(): %s.\n", strerror(errno)); return false; }
    return true;
}
bool sstd::tcpSrv_nonblocking::ctlState_rmW(const int& rmFd){
    cEv={0};
    cEv.data.fd = cFd;
    if(pEvents[eNum].events & EPOLLIN){ cEv.events |= EPOLLIN; }
    if(epoll_ctl(epFd, EPOLL_CTL_MOD, rmFd, &cEv)==-1){ sstd::pdbg("ERROR: epoll_ctl(): %s.\n", strerror(errno)); return false; }
    return true;
}
bool sstd::tcpSrv_nonblocking::ctlState_rmRW(const int& rmFd){ return true; } // not implimented yet
bool sstd::tcpSrv_nonblocking::ctlState_addR(const int& addFd){ return true; } // not implimented yet
bool sstd::tcpSrv_nonblocking::ctlState_addW(const int& addFd){
    cEv={0};
    cEv.data.fd = addFd;
    if(pEvents[eNum].events & EPOLLIN){ cEv.events |= EPOLLIN; }
    cEv.events |= EPOLLOUT;
    if(epoll_ctl(epFd, EPOLL_CTL_MOD, addFd, &cEv)==-1){ sstd::pdbg("ERROR: epoll_ctl(): %s.\n", strerror(errno)); return false; }
    return true;
}
bool sstd::tcpSrv_nonblocking::ctlState_addRW(const int& addFd){ return true; } // not implimented yet

//---

void set_nonblocking(int fd){ fcntl(fd, F_SETFL, O_NONBLOCK); }
bool sstd::tcpSrv_nonblocking::open(const uint portNum){
    sIn.sin_family      = AF_INET;
    sIn.sin_addr.s_addr = 0;
    sIn.sin_port        = htons(portNum);
    
    sock = socket(AF_INET, SOCK_STREAM, 0); // SOCK_STREAM : TCP/IP
    if(sock<0){ sstd::pdbg("ERROR: socket(): %s.\n", strerror(errno)); return false; }
    set_nonblocking(sock);
    
    // SO_REUSEADDR : in order to solve "ERROR: bind(): Address already in use.".
    // SO_REUSEPORT : 同一ポートに複数リスナー接続可能にする.
    int on = 1;
    if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int))!=0){ sstd::pdbg("ERROR: setsockopt(): %s.\n", strerror(errno)); ::close(sock); return false; }
    if(setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(int))!=0){ sstd::pdbg("ERROR: setsockopt(): %s.\n", strerror(errno)); ::close(sock); return false; }
    
    if(  bind(sock, (struct sockaddr*)&sIn, sizeof(sIn))<0){ sstd::pdbg("ERROR: bind(): %s. (You might need to run your program by \"$ sudo ./exe\".)\n", strerror(errno)); ::close(sock); return false; }
    if(listen(sock, numOfListenLimits)                  <0){ sstd::pdbg("ERROR: listen(): %s.\n",                                                         strerror(errno)); ::close(sock); return false; }
    
    // init epoll()
    if((epFd=epoll_create(maxEvents))<0){ sstd::pdbg("ERROR: epoll_create(): %s.\n", strerror(errno)); ::close(sock); return false; }
    if(!ctlState_setR(sock)){ sstd::pdbg("ERROR: ctlState_setR(): was failed.\n"); return false; }
    fd_offset = epFd+1; // newFd number will begin "epFd+1".
    
    return true;
}
bool sstd::tcpSrv_nonblocking::new_fd(){
    struct sockaddr_storage ss;
    socklen_t ssLen = sizeof(struct sockaddr_storage);
//    int newFd = accept(sock, (struct sockaddr*)&ss, &ssLen);
    int newFd = accept4(sock, (struct sockaddr*)&ss, &ssLen, SOCK_NONBLOCK); // accept4() can set SOCK_NONBLOCK flag
    if(newFd<0){ sstd::pdbg("ERROR: accept(): %s.\n", strerror(errno)); return false; }
//    sfet_nonblocking(newFd);
    
    // init state buffer
    sNum = newFd - fd_offset; if(sNum>=maxEvents){ sstd::pdbg("ERROR: accept(): Over the maxEvents.\n"); ::close(newFd); return false; }
    pState[sNum].writeMsg.clear();
    
    if(!printInfo_fd(ss, ssLen)){ sstd::pdbg("ERROR: printInfo_fd() faild.\n"); }
    
    // set epoll
    if(!ctlState_setR(newFd)){ sstd::pdbg("ERROR: ctlState_setR(): was failed.\n"); return false; } // If you want to receive the data, the program needs to wait until the "epoll_wait()" function will return "EPOLLIN" singal.
    
    return true;
}
bool sstd::tcpSrv_nonblocking::del_fd(int delFd){
    if(epoll_ctl(epFd, EPOLL_CTL_DEL, delFd, NULL)==-1){ sstd::pdbg("ERROR: epoll_ctl(): %s.\n", strerror(errno)); ::close(delFd); return false; }
    ::close(delFd);
    return true;
}

bool sstd::tcpSrv_nonblocking::wait(){
 continue_SSTD_TCP_WAIT:
    
    if(isProc && !isEvent()){ sstd::pdbg("close socket: %d\n\n\n", cFd); del_fd(cFd); } // if there is no events, close socket
    isProc=false;
    
    // blocking here until reads() or write() functions enable to process data without waite time, or EPOLLERR.
    if(eNum==0){ if((nFd=epoll_wait(epFd, pEvents, maxEvents, -1))==-1){ sstd::pdbg("ERROR: epoll_wait(): %s.\n", strerror(errno)); ::close(sock); return false; }
    }   else   { eNum++; } // count up one state
    
    for(; eNum<nFd; eNum++){
        if(pEvents[eNum].data.fd==sock){
            // accept new socket
            if(!sstd::tcpSrv_nonblocking::new_fd()){ sstd::pdbg("ERROR: sstd::tcpSrv_nonblocking::new_fd() was failed.\n"); return false; }
        }else{
            // in order to process recv() or send()
            isProc=true;
            return true;
        }
    }
    eNum=0;
    goto continue_SSTD_TCP_WAIT; // same as for(;;){}, but i dont like deep indents.
}
bool sstd::tcpSrv_nonblocking::isRedy4recv(){ return (pEvents[eNum].events & EPOLLIN); }
bool sstd::tcpSrv_nonblocking::isRedy4send(){ return (pEvents[eNum].events & EPOLLOUT); }

bool sstd::tcpSrv_nonblocking::workerRecv(std::vector<uchar>& retOut, const uint& limitSize){
    
    retOut.resize(limitSize);
    ssize_t len = ::recv(cFd, &retOut[0], retOut.size(), 0);
    if(len<=0){
        retOut.resize( 0 );
        if(errno==EAGAIN || errno==EWOULDBLOCK){ ctlState_rmR(cFd); return false; }
        sstd::pdbg("ERROR: error number: %d, message: %s\n", errno, strerror(errno));
        del_fd(cFd);
        return false;
    }else{
        retOut.resize(len);
    }
    
    std::vector<uchar> buf(limitSize);
    for(;;){
        len = ::recv(cFd, &buf[0], buf.size(), 0);
        if(len<=0){ break; // len == '0' or '-1'
        }  else   { retOut.insert(retOut.end(), &buf[0], &buf[0]+len); }
        
        if(retOut.size()>=limitSize){ sstd::pdbg("ERROR: ret over limitSize\n"); return true; }
    }
    
    return true;
}
bool sstd::tcpSrv_nonblocking::recv(std::vector<uchar>& retOut, const uint& limitSize){
    sstd::pdbg("--- in recv(), fd: %d ---\n", eNum); // for debug
    
    // update to current states
    cFd  = pEvents[eNum].data.fd;
    sNum = cFd - fd_offset;
    
    sstd::pdbg("--- in recv(), eNum: %d, cFd: %d ---\n", eNum, cFd); // for debug
    
    if(workerRecv(retOut, limitSize)==false || retOut.size()==0){ return false; }
    
    // update epoll event (remove EPOLLIN)
    ctlState_rmR(cFd);
    
    return true;
}
bool sstd::tcpSrv_nonblocking::setSend(std::string& msg){
    
    // update epoll event (add EPOLLOUT)
    ctlState_addW(cFd);
    
    pState[sNum].writeMsg.swap(msg);
    msg.clear();
    
    return true;
}
bool sstd::tcpSrv_nonblocking::setSend(const char*& pMsg){
    std::string msg = pMsg;
    return sstd::tcpSrv_nonblocking::setSend(msg);
}
bool sstd::tcpSrv_nonblocking::send(){
    sstd::pdbg("--- in send(), fd: %d ---\n", eNum); // for debug
    
    // update to current states
    cFd  = pEvents[eNum].data.fd;
    sNum = cFd - fd_offset;
    
    if(pState[sNum].writeMsg.size()<=0){ del_fd(cFd); return false; }
    
    ssize_t result = ::send(cFd, pState[sNum].writeMsg.c_str(), pState[sNum].writeMsg.size(), MSG_NOSIGNAL); // MSG_NOSIGNAL: SIGPIPE を送信しない (送信されると,プログラムが終了する)
    if(result==-1){ sstd::pdbg("ERROR: ::send(): %s.\n", strerror(errno)); del_fd(cFd); return false; }
    
    // update epoll event (add EPOLLOUT)
    ctlState_rmW(cFd);
    
    return true;
}

  ./main.cpp
#include "./sstd_tcp/server.hpp"
#include "./sstd_tcp/client.hpp"

#include <unordered_map>
#include <unistd.h>   // for fork
#include <sys/wait.h> // for fork

//-----------------------------------------------------------------------------------------------------------------------------------------------
// server sample

std::unordered_map<std::string, bool> getURL_table(bool& result, const char* URL_rootPath){ // const char* rootPath = "./contents/root";
    result = true;
    
    std::vector<std::string> ret;
    if(!sstd::getAllFile(ret, URL_rootPath)){ sstd::pdbg("ERROR: getAllFile() failed.\n"); result=false; return std::unordered_map<std::string, bool>(); }
    
    std::unordered_map<std::string, bool> URL_table(ret.size()); // In order to avoid directory traversal
    for(uint i=0; i<ret.size(); i++){ URL_table[ret[i]]=true; }  // add on a hash table
    return URL_table;
}
std::string getURL(std::unordered_map<std::string, bool>& URL_table, const char* str_in){
    std::vector<std::string> vecLine = sstd::splitByLine(str_in);
    sstd::printn_all(vecLine); // fro debug

    std::string ret;
    for(uint i=0; i<vecLine.size(); i++){
        std::vector<std::string> vecRow = sstd::split(vecLine[i], ' ');
        if(vecRow.size()<2){ continue; }
        
        if      (sstd::strcmp(vecRow[0], "GET"                       )){
            auto itr = URL_table.find("./contents/root"+vecRow[1]);
            if(itr!=URL_table.end()){ ret=itr->first; } // iterator により提供される key は,iterator の開放と同時に開放される一時変数.従って,ポインタのみのコピーしても開放されてしまう.
        }else if(sstd::strcmp(vecRow[0], "Host:"                     )){
        }else if(sstd::strcmp(vecRow[0], "Connection:"               )){ // keep-alive
        }else if(sstd::strcmp(vecRow[0], "Cache-Control:"            )){ // max-age=0
        }else if(sstd::strcmp(vecRow[0], "Upgrade-Insecure-Requests:")){ // Upgrade-Insecure-Requests: 1
        }else if(sstd::strcmp(vecRow[0], "User-Agent:"               )){ // User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
        }else if(sstd::strcmp(vecRow[0], "Accept:"                   )){ // text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
        }else if(sstd::strcmp(vecRow[0], "Accept-Encoding:"          )){ // Accept-Encoding: gzip, deflate, br
        }else if(sstd::strcmp(vecRow[0], "Accept-Language:"          )){ // ja,en-US;q=0.9,en;q=0.8
        }
    }
    
    return ret;
}
std::string genMsg(std::string& requestURL){
    std::string msg;
    
    sstd::printn_all(requestURL); // ./contents/root/index.html
    if(requestURL.size()!=0){
        // status 200
        std::string rawFile = sstd::readAll(requestURL); // There is no problem with BOM
        msg += "HTTP/1.1 200 OK\r\n";
        msg += sstd::ssprintf("Content-Length: %d\r\n", rawFile.size());
        msg += "Content-Type: text/html\r\n";
        msg += "\r\n";
        msg += rawFile;
    }else{
        // 404
        std::string rawFile = sstd::readAll("./contents/statusCodes/404.html"); // There is no problem with BOM
        msg += "HTTP/1.1 404 Not Found\r\n";
        msg += "Connection: close\r\n";
        msg += "Content-Type: text/html\r\n";
        msg += sstd::ssprintf("Content-Length: %d\r\n", rawFile.size());
        msg += "\r\n";
        msg += rawFile;
        
    //    msg += "HTTP/1.1 404 Not Found\r\n";
    //    msg += "Connection: close\r\n";
    //    msg += "Content-Length: 0\r\n";
    //    msg += "\r\n";
    }
    
    return msg;
}
void httpd(const uint portNum, const char* URL_rootPath){

    // - init URL table -----------------------------------------------------------------
    bool result;
    std::unordered_map<std::string, bool> URL_table = getURL_table(result, URL_rootPath);
    if(!result){ sstd::pdbg("ERROR: getURL_table() is failed\n"); return; }
    // ---------------------------------------------------------- end of init URL table -
    
    uint limitSize = 1024; // limit size of recv buf.
    uint maxEvents = 1024; // limit size of events
    sstd::tcpSrv_nonblocking srv(maxEvents); if(!srv.open(portNum)){ sstd::pdbg("ERROR: sstd::tcpSrv_nonblocking::open() was faild.\n"); return; }
    
    for(;;){
        srv.wait(); // blocking here
        
        if(srv.isRedy4recv()){
            std::vector<uint8> rhs;
            if(!srv.recv(rhs, limitSize)){ sstd::pdbg("failed\n"); continue; } rhs.push_back('\n');
            std::string requestURL = getURL(URL_table, (const char*)&rhs[0]);
            std::string msg = genMsg(requestURL);
            srv.setSend(msg);
        }else if(srv.isRedy4send()){
            //bool result = srv.send();
            srv.send();
        }
    }
}

//-----------------------------------------------------------------------------------------------------------------------------------------------

// usage
//   1. $ sudo ./exe
//   2. accessing to http://localhost/index.html

// how to check ip address and access from the other device.
//   1. $ ip a
//   2. an address begin 192.168.XXX.XXX is your local address.
//   3. A device like a smart phone on the same local net work is able to access httpd by "http://192.168.XXX.XXX/index.html".

int main(){
    setvbuf(stdout, NULL, _IONBF, 0); // output immediately without buffering (not waiting line feed code)
    
    // - settings --------------------------------------------------------
    const uint portNum          =  80;               // Port number of http is 80. Web browser requires to specify port number without 80 using ':' option like "http://192.168.XXX.XXX/index.html:8080".
    const char* URL_rootPath    = "./contents/root"; // Root directory of httpd
    // ------------------------------------------------- end of settings -
    
    httpd(portNum, URL_rootPath); // http Daemon
    
    return 0;
}

  ここで,通信処理は ./sstd_tcp/server.hpp および ./sstd_tcp/server.cpp に実装されている.また,./main.cpp には httpd が実装されている.
  上記の httpd で配信する Web ページのサンプルとして,下記の html ファイル,jpg 画像,raw テキスト を用意した.

  ./contents/root/index.html
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>sstd::sock()</title>
  </head>
  
  <style>
    .mainBox {
    margin: 0;
    padding: 0;
    color: #272727;
    background: #FFFFFF;
    height:380px;
    width:1185px;
    }
    .subBox {
    margin: 50;
    }
    
    .overWrite_rel{
    position: relative;
    }
    .overWrite_abs{
    position: absolute;
    top: -10px;
    left: 20px;
    }
  </style>
  
  <body style="background-color: #EFEFF0; margin: 0;">
    <br/>
    <br/>
    <br/>
    <div align="center">
      <div class="mainBox"><!-- beginning of mainBox ----------------------------------- -->
        
        <br/>
        <div class="overWrite_rel">
          <img src="./headImg.jpg" width="1145" height="119">
          <p class="overWrite_abs"><font size='10' color="white">sstd::sock()</font></p>
        </div>
        
        <div class="subBox">
          <div align="left">
            <font size='10' color="#272727">
              Welcome to the tiny http daemon<br/>
              using sstd::sock() a non-blocking TCP socket.
            </font>
          </div>
        </div>
        
      </div><!-- -------------------------------------------------------  end of mainBox -->
    </div>
  </body>
</html>

  ./contents/root/headImg.jpg

  ./contents/root/sample.txt
This is a sample of plain text file.
As you know, line feed code is not work.
This means that plain text file needs to treat as a raw file.
(Right clike and trying to save this page will telling the expansion of this page is ".html".)
Execution environment
・Ubuntu 16.04 LTS
・AMD Ryzen7 1700
・DDR4-2666 32GB
Experimental method and Execution result
  ここでは,実験方法と実験結果を提示する.

1. git からソースコードをダウンロードする.
$ git pull https://github.com/admiswalker/forBlog_sstd_tcp_Ver00.11_00
2. ディレクトリの移動
$ cd forBlog_sstd_tcp_Ver00.11_00
3. コンパイル
$ make
4. 実行
$ sudo ./exe 
[sudo] usr のパスワード: 

5. ここで,ブラウザから http://localhost/index.html へアクセスすると下記の出力を得る.
sstd_tcp/server.cpp: printInfo_fd(44): info of feed: host: localhost, port: 55060
sstd_tcp/server.cpp: printInfo_fd(44): info of feed: host: localhost, port: 55064
sstd_tcp/server.cpp: recv(192): --- in recv(), fd: 0 ---
sstd_tcp/server.cpp: recv(198): --- in recv(), eNum: 0, cFd: 5 ---
getURL(52): vecLine[11] = [ [ GET /index.html HTTP/1.1 ] [ Host: localhost ] [ Connection: keep-alive ] [ Purpose: prefetch ] [ Upgrade-Insecure-Requests: 1 ] [ User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36 ] [ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 ] [ Accept-Encoding: gzip, deflate, br ] [ Accept-Language: ja,en-US;q=0.9,en;q=0.8 ] [  ] [  ] ]
genMsg(78): requestURL = ./contents/root/index.html
sstd_tcp/server.cpp: send(222): --- in send(), fd: 0 ---
sstd_tcp/server.cpp: wait(143): close socket: 5


sstd_tcp/server.cpp: recv(192): --- in recv(), fd: 0 ---
sstd_tcp/server.cpp: recv(198): --- in recv(), eNum: 0, cFd: 6 ---
getURL(52): vecLine[11] = [ [ GET /headImg.jpg HTTP/1.1 ] [ Host: localhost ] [ Connection: keep-alive ] [ User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36 ] [ Accept: image/webp,image/apng,image/*,*/*;q=0.8 ] [ Purpose: prefetch ] [ Referer: http://localhost/index.html ] [ Accept-Encoding: gzip, deflate, br ] [ Accept-Language: ja,en-US;q=0.9,en;q=0.8 ] [  ] [  ] ]
genMsg(78): requestURL = ./contents/root/headImg.jpg
sstd_tcp/server.cpp: send(222): --- in send(), fd: 0 ---
sstd_tcp/server.cpp: wait(143): close socket: 6


sstd_tcp/server.cpp: printInfo_fd(44): info of feed: host: localhost, port: 55068
sstd_tcp/server.cpp: printInfo_fd(44): info of feed: host: localhost, port: 55072

このときブラウザは下記のように描画し,httpd が画像付き html ファイルを正しく配信したことがわかる.

また,http://localhost/sample.txt へのアクセスは,
のようになる. 現状では,Content-Type: が text/html に固定されているため,ブラウザがファイルを html として描画するためである.
  たとえば,上記の ./main.cpp
  ./main.cpp
...ellipsis....

std::string genMsg(std::string& requestURL){
    std::string msg;
    
    sstd::printn_all(requestURL);
    if(requestURL.size()!=0){
        
        std::string ct;
        char* fe = sstd::getExtension(requestURL.c_str());
        if(sstd::strcmp(fe, "html")){ ct="text/html";
        }            else           { ct="text/plain"; }
        
        // status 200
        std::string rawFile = sstd::readAll(requestURL); // There is no problem with BOM
        msg += "HTTP/1.1 200 OK\r\n";
        msg += sstd::ssprintf("Content-Length: %d\r\n", rawFile.size());
        msg += sstd::ssprintf("Content-Type: %s\r\n", ct.c_str());
        msg += "\r\n";
        msg += rawFile;
    }else{
        // 404
        std::string rawFile = sstd::readAll("./contents/statusCodes/404.html"); // There is no problem with BOM
        msg += "HTTP/1.1 404 Not Found\r\n";
        msg += "Connection: close\r\n";
        msg += "Content-Type: text/html\r\n";
        msg += sstd::ssprintf("Content-Length: %d\r\n", rawFile.size());
        msg += "\r\n";
        msg += rawFile;
        
    //  msg += "HTTP/1.1 404 Not Found\r\n";
    //  msg += "Connection: close\r\n";
    //  msg += "Content-Length: 0\r\n";
    //  msg += "\r\n";
    }
    
    return msg;
}

...ellipsis....

のように修正すると,ブラウザは拡張子が html 以外のファイルを plain text として描画するため,改行コードは,
のように正しく表示される.
Summary
  通信部分とサーバの処理部分とを完全に分離した httpd サンプルを実装した.また,hash table を利用してディレクトリトラバーサルへの耐性を持たせた.
Future work
・keep-alive の実装.
  単純には,最終アクセス時刻を struct fdState に記録しておき,監視用のスレッドを 1 つ生成し,ループを回すことが考えられる.このとき,タイムアウト時刻からのずれをどれだけ許容するかで,ループ中にどれだけ sleep を挟めるかが決定する.端的に 1 項目あたりの sleep 時間を $sleepLen$ [sec],許容時間誤差 (本来接続を切断する時刻より切断が遅れる時間の長さ) を $timeMargin$ [sec],処理時間を $procLen$ [sec],同時接続数を $fdNum$ とすると, \begin{align*} sleepLen=\frac{timeMargin}{fdNum}-procLen \end{align*} となる.したがって,$fdNum=10^4$,$timeMargin=10$ [sec],簡単のため $procLen=0$ [sec] とすると,ループ中に $1$ ms 程度の sleep を挟めることがわかる.また,$procLen$ は実測を行いながらフィードバックするのが簡単だと考えられる.これらから,許容する誤差を大きく許せば,簡単な実装でも大きな負荷なく keep-alive を行うことができる.ただし,接続切断までの時間が長くなることも許容しなくてはならない.
  また,配列を共有する場合には,ロックする必要が生じるため,ロックの粒度についても検討が必要である.まず,ロックフリーで実装する方法がないか調査・検討する.
・https (Let's Encrypt [7]) への対応.
・apache のアクセス log 等を参考に,web サーバがどこまでの動作 log を保存しているかを確認する.
・相対パスでのアクセスが不可能となっている問題.-> 一度絶対パスへ変換した後,hash table 上のデータと照合する.
・ファイルの追加・削除および名前を変更した場合,サーバを再起動しないと反映されない問題の解決.-> 404 エラー発生時に,ディレクトリ構成を再生成する.この際,一度絶対パスに変換した後,指定ディレクトリ以下の場合は,hash table へ追加する.
・巨大なデータの扱い.-> ダウンロードのレジューム機能に対応する.
・レスポンスの改善.-> 10 MB 以下など,サイズの小さなファイルは全て hash table に紐づけて,RAM 上にロードしておく. 1 GB やそれ以上の,サイズの大きなデータについては,RAM 上へのバッファを 100 MB 程度に抑え,逐次 HDD/SSD からロードするような実装を行う. また,通信時間の長い接続は,別にスレッドを立てて処理する必要がある.
References
  [1] nginx - Wikipedia - 2018年09月23日閲覧
  [2] ディレクトリトラバーサル - Wikipedia - 2018年09月23日閲覧
  [3] A tiny introduction to asynchronous IO - 2018年09月23日閲覧
  [4] Concurrent Servers: Part 3 - Event-driven - 2018年09月23日閲覧
  [5] code-for-blog/2017/async-socket-server/epoll-server.c - 2018年09月23日閲覧
  [6] Linuxの備忘録とか・・・(目次へ): select - 2018年09月23日閲覧
  [7] Let's Encrypt - Wikipedia - 2018年09月23日閲覧

0 件のコメント:

コメントを投稿