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() は,実際に発生したイベントのリストを返却するため,バッファ内の全てのイベントを線形探査する必要がなく,定数時間で処理される.
non-blocking socket の実装には,いくつかの方法があるが,中でも Linux の epoll() は,実際に発生したイベントのリストを返却するため,バッファ内の全てのイベントを線形探査する必要がなく,定数時間で処理される.
Previous research
select() を用いた httpd の実装には [3] が挙げられる.select() を用いた実装では,イベントを線形探査する必要が生じるため,
この問題を避けるため,Linux は epoll() という独自実装を持つ.epoll() を用いた non-blocking socket の実装には [4][5] がある.ここでは,
...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
./sstd_tcp/server.cpp
./main.cpp
ここで,通信処理は ./sstd_tcp/server.hpp および ./sstd_tcp/server.cpp に実装されている.また,./main.cpp には httpd が実装されている.
上記の httpd で配信する Web ページのサンプルとして,下記の html ファイル,jpg 画像,raw テキスト を用意した.
./contents/root/index.html
./contents/root/headImg.jpg
./contents/root/sample.txt
./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
・AMD Ryzen7 1700
・DDR4-2666 32GB
Experimental method and Execution result
ここでは,実験方法と実験結果を提示する.
1. git からソースコードをダウンロードする.
5. ここで,ブラウザから http://localhost/index.html へアクセスすると下記の出力を得る.
このときブラウザは下記のように描画し,httpd が画像付き html ファイルを正しく配信したことがわかる.
また,http://localhost/sample.txt へのアクセスは,
のようになる.
現状では,Content-Type: が text/html に固定されているため,ブラウザがファイルを html として描画するためである.
たとえば,上記の ./main.cpp を
./main.cpp
のように修正すると,ブラウザは拡張子が html 以外のファイルを plain text として描画するため,改行コードは,
のように正しく表示される.
1. git からソースコードをダウンロードする.
$ git pull https://github.com/admiswalker/forBlog_sstd_tcp_Ver00.11_002. ディレクトリの移動
$ cd forBlog_sstd_tcp_Ver00.11_003. コンパイル
$ make4. 実行
$ 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 へのアクセスは,
たとえば,上記の ./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 からロードするような実装を行う. また,通信時間の長い接続は,別にスレッドを立てて処理する必要がある.
単純には,最終アクセス時刻を 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日閲覧
[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 件のコメント:
コメントを投稿