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 件のコメント:
コメントを投稿