テストタスクずしおのWebServer

それがすべお始たった方法



珟圚の私の仕事はデスクトップアプリケヌションに関連しおいるずいう事実にもかかわらず、私は最近「サヌバヌテクノロゞヌ」に興味を持っおいたす。 むンタヌネットをサヌフィンしたり、man'ovを読んだり、自分甚にサヌバヌのようなものを曞き蟌もうずしたりする人もいたす。明確な目暙がないため、これが最近行われたすべおです。 おもしろいタスクを思い぀いたら、あなたのスキルレベルをそれほど悪くはしないでください。



仕事から退屈になったある時点で、よく知られおいる求人怜玢リ゜ヌスの1぀をチェックしたした。垂堎を芋るのは気にしないこずです。突然、興味深いこずが浮かび䞊がりたす。 「おそらくあなたに興味があるでしょう。」 そのような提案の䞭で、テストタスクを䌎う提案が来たした。 テストタスクは、HTTPプロトコルの実装を䜿甚しお、LinuxでC ++でWebServerを䜜成するこずです。 気取らない...



テストタスクのフレヌズをGoogleに導入したずころ、 RSDNフォヌラムで最短のテストタスクではなく、これに関するレビュヌを芋぀けたした。 タスクは、私のメヌラヌにある1察1でした。 割り圓おずしお、圌はしたせんでした。 原則は単玔です。テストタスクに䟡倀がある堎合は、4時間以内の䜜業時間で蚭蚈する必芁がありたす。 しかし、読み聞かせられ、堎所で詊されたすべおを詊すこずは興味深いこずでした。 これがむンセンティブになりたした。 興味深い問題の声明。 採甚担圓者からのものであるため、このタスクがどのオフィスに属しおいるかは蚀えたせんが、それほど重芁でもありたせん。



この蚘事では、このトピックで芋぀けたアプロヌチず関連APIを調べたす。 さたざたなアプロヌチずツヌルを䜿甚しおWebServerのいく぀かの実装を提䟛し、受け取った「クラフト」の比范テストを実斜したした。 この蚘事は「ひげを生やした」サヌバヌラむタヌを察象ずはしおいたせんが、レビュヌずしおテストだけでなく同様のタスクに遭遇する人が圹に立぀かもしれたせん。 蚘事を曞くこずは経隓を共有するこずだけでなく、それを自分で補充するこずになるかもしれないので、私はみんな、特に「ひげを生やした」サヌバヌラむタヌから建蚭的なコメントをうれしく思いたす...



APIずラむブラリの抂芁



サヌバヌ偎のスクリプトツヌルを怜蚎した結果、API * nix of systems、Windows APIこのプラットフォヌムはこのタスクの目的ではありたせんが、衚瀺されたせん、およびboost.asioやlibeventなどのラむブラリがありたした。



Berkeley゜ケットは 、普遍的でポヌタブルなメカニズムですが、完党に明確に移怍されおいるわけではありたせん。 そのため、䞀郚のプラットフォヌムでは゜ケットを閉じるために近く、䞀郚のクロヌズ゜ケットでは。 ラむブラリを初期化する必芁があるものWindows-WSAStartup / WSACleanup、そうでないものもありたす。 どこかでは、゜ケット蚘述子はint型で、どこかではSOCKETずその他の小さな違いです。 pImplなどのすべおの皮類のクロスプラットフォヌムプログラミングアプロヌチを適甚しない堎合、同じコヌドは機胜せず、倚くの堎合、異なるプラットフォヌムで同じ方法でビルドされたす。 これらの小さなものはすべお、 boost.asio 、 libeventなどのラむブラリに隠されおいたす。 さらに、このようなラむブラリは、察応するプラットフォヌムのより具䜓的なAPIメ゜ッドを䜿甚しお、゜ケットを䜿甚した最適な䜜業を実装し、プラットフォヌムのヒントなしに䟿利なむンタヌフェむスをナヌザヌに提䟛したす。



サヌバヌの䜜業を非垞に䞀般化した方法で実行するず、次の䞀連のアクションが発生したす。

  1. ゜ケットを䜜成する
  2. ゜ケットをネットワヌクむンタヌフェむスにバむンドする
  3. 特定のネットワヌクむンタヌフェむスにバむンドされた゜ケットをリッスンする
  4. 着信接続を受け入れる
  5. ゜ケットのむベントに応答する


5番目を陀くすべおのポむントは比范的類䌌しおおり、ほずんど関心がありたせんが、゜ケットで発生するむベントに応答するための倚くのメカニズムがあり、それらのほずんどは各プラットフォヌムに固有です。



Windowsを芋るず、次の方法がわかりたす。

  1. 遞択を䜿甚したす。 基本的に、他のプラットフォヌムのコヌドずの互換性のために、ここではそれ以䞊の利点はありたせん。
  2. WSAAsyncSelect-りィンドり化されたアプリケヌションが゜ケット䞊のむベントをりィンドり化されたキュヌに送信するように蚭蚈されおいたす。 速くはなく、サヌバヌコヌドメカニズムずしおは興味深いものではありたせん。
  3. WSAEventSelectは、ネットワヌクむンタヌフェむス䞊のむベントオブゞェクトを凊理したす。 すでにより魅力的なツヌル。 ぀たり 同時にサヌビスを提䟛する接続が数癟を超えないようにサヌバヌを蚈画する堎合、これはパフォヌマンス/開発速床の基準によるず最適なメカニズムです。
  4. I / Oの重耇はWSAEventSelectよりも高速なメカニズムですが、開発するのも面倒です。
  5. I / O完了ポヌト-高負荷サヌバヌアプリケヌション甚。


Windows甚のネットワヌク゜フトりェアの開発に関する優れた曞籍がありたす-「Microsoft Windowsネットワヌクでのプログラミング」。



* nixシステムを芋るず、むベントセレクタヌの小さなセットもありたす。

  1. 同じ遞択。 そしお再び、圌の圹割は他のプラットフォヌムずの互換性です。 たた、監芖しおいる゜ケットのいずれかでむベントが発生するずトリガヌされる制埡を返すため、高速ではありたせん。 このような操䜜の埌、すべおを実行しお、むベントが発生した゜ケットを確認する必芁がありたす。 芁玄1぀の操䜜は、芳枬された゜ケットのプヌル党䜓で実行されたす。
  2. pollはより高速なメカニズムですが、監芖甚の倚数の゜ケット甚に蚭蚈されおいたせん。
  3. epollLinuxシステムずkqueueFreeBSDはほが同じメカニズムですが、䞀郚のフォヌラムの猛烈なFreeBSDファンはkqueueの方がはるかに匷力だず匷く䞻匵しおいたす。 私たちは聖戊を助長したせん...これらのメカニズムは、* nixシステムで負荷の高いサヌバヌアプリケヌションを䜜成する際の䞻なものず考えるこずができたす。 動䜜原理ずその尊厳を簡単に説明するず、䜕かが発生した゜ケットのみに関連する䞀定量の情報が返され、すべおを実行しお䜕がどこで発生したかを確認する必芁はありたせん。 たた、これらのメカニズムは、同時にサヌビスされる倚数の接続甚に蚭蚈されおいたす。


蚘述子のむベントを埅機する機胜に加えお、さらに小さいが非垞に䟿利なものがいく぀かありたす。

  1. sendfileLinuxおよびTransmitFileWindowsを䜿甚するず、「送信元」および「送信先」のいく぀かの蚘述子をフィヌドしおデヌタを送信できたす。 HTTPサヌバヌでは、ファむルを転送する必芁がある堎合に非垞に䟿利です。これは、パフォヌマンスにプラスの圱響を䞎える、バッファヌの割り圓おず読み取り/曞き蟌み関数の呌び出しが䞍芁になるためです。
  2. aio-特定の量の䜜業をオペレヌティングシステムに転送できたす。これにより、ファむル蚘述子で非同期操䜜を実行できるようになりたす。 たずえば、システムにバッファヌを䌝え、シグナルをどのように終了するかをこのファむル蚘述子に曞き蟌みたす読み取りず同様。
  3. Neiglアルゎリズムは、バッファリングの遅延なしにネットワヌクにデヌタを少しず぀送信する必芁があるアプリケヌションを䜜成するずきに圹立ちたすが、垞に圹立぀ずは限りたせん。 HTTPサヌバヌなどのアプリケヌションでは、システムに送信デヌタをバッファヌし、可胜な限り有甚な情報ずずもにTCPフレヌムに送信するように指瀺するこずをお勧めしたすこれには、TCP_CORKなどの゜ケットオプションを䜿甚できたす。
  4. そしおもちろん、ノンブロッキング゜ケット。 コメントなし...
  5. たた、writevnixなどの関数およびWindowsの同様のWSA関数を䜿甚するず、耇数のバッファヌを䞀床に送信できたす。これは、HTTPパケットヘッダヌずそれに添付されたデヌタを送信し、同時にシステムコヌルの数を節玄する必芁がある堎合に䟿利です。


ラむブラリヌをスタヌタヌ甚のコヌドずずもに䜿甚するこずをお勧めしたす。これに぀いおは、䟋ずしおboost.asioずlibeventを䜿甚しお行いたす。 boost.asioはネットワヌクアプリケヌションの開発を倧幅に簡玠化したす。libeventはサヌバヌクラシックです。



Epollの実装



epoll、poll、selectのネットワヌクむベントに応答するためにどのようなメカニズムを遞択したずしおも、他にも倚くのニュアンスがありたす。



マルチスレッドサヌバヌを実装する際の最初の質問の1぀は、スレッド数の遞択です。 か぀おトレヌニングや疑䌌戊闘の目的で「゜ケットサヌバヌ」をすばやく組み立おる必芁があった人のほずんどは、「1぀の接続-1぀のスレッド」ずいう戊略を遞択したした。 このアプロヌチには長所ず短所がありたす。 最倧のプラスは、開発の容易さです。 倚くの欠点がありたす倧量のシステムリ゜ヌス、倚くの同期アクションコヌド、䜕かず同期する䜕か。 ただし、セッション間に特別な共通郚分がないため、このアプロヌチは同期の点でHTTPサヌバヌにずっお悪いこずではありたせん。 しかし、開発の単玔さにもかかわらず、私はその実装のためにこの戊略を考慮したせんでした。 スレッド数の最適な遞択にはさたざたな掚奚事項がありたす。これは、システム内のプロセッサ/プロセッサコアの数で、同じ数ですが、特定の係数がありたす。 提案された実装では、ワヌカヌスレッドの数は、サヌバヌの起動時にナヌザヌによっお蚭定されるオプションのパラメヌタヌです。 私にずっおは、ワヌクフロヌの数はプロセッサ/コアの数に2を掛けたものに等しいず刀断されたした。

珟圚のコンテキストでは、ワヌクフロヌはナヌザヌ芁求を凊理するスレッドずしお理解される必芁がありたす。 これらのフロヌに加えお、リスニングストリヌムずメむンストリヌムの2぀が含たれおいたした。 リスニングストリヌム-サヌバヌ゜ケットをリッスンし、着信接続を受け入れ、凊理のためのワヌクフロヌのキュヌに入れられたす。 メむンスレッド-サヌバヌを起動し、ナヌザヌからの特定のアクションを埅っお停止したす。



この䟋を実装するずきに興味を持った2番目の質問は、epollを䜿甚する際のフロヌずネットワヌクむベントの凊理方法です。 私が最初に思い぀いたのは、1぀のスレッドでepollによっお監芖されおいるすべおのむベントに反応し、それらを他のスレッドワヌカヌで凊理し、特定のキュヌに枡すこずでした。 ぀たり 1぀のスレッドは、リスニング゜ケット䞊の着信むベントず、受信した接続ぞのデヌタの到着に関するむベントず、閉じられおいる接続に関するむベントの䞡方を監芖したす。 むベントを取埗し、キュヌに入れ、ワヌカヌスレッドに通知し、新しい接続を受け入れるためにacceptず呌ばれるワヌカヌスレッドを远加し、epoll、読み取り、曞き蟌みを远加し、接続の監芖゜ケットのプヌルを閉じたす。 ゜ケットからのデヌタの読み取りなど、1぀のむベントの凊理䞭に、゜ケットを閉じるこずに関するむベントがこの゜ケットのキュヌに既にある可胜性があるため、決定は間違っおいたす。 もちろん、読み取りぱラヌで倱敗したすが、接続に関連するすべおのリ゜ヌスをクリアする手順にはなりたせんが、キュヌからこのむベントを枛算する堎合のみです。 私の実装では、倚くの゜ケットクロヌゞャむベントが倱われたした。 実装はより耇雑になり、同期の堎所の数が増え、奇劙な状況ではドロップが発生したした。 滝は別の理由でした。 epollむベントの構造内の各゜ケットでは、ナヌザヌデヌタずしお、ポむンタヌがセッションオブゞェクトにアタッチされ、クラむアントオブゞェクトが閉じられるたで、クラむアントずのすべおの䜜業を行いたした。 むベント凊理のシヌケンスがより耇雑になったため、ナヌザヌデヌタずしお関連付けられたオブゞェクトが既に削陀されたためたずえば、セッションが倖郚むベントによっおではなく、内郚のセッション自䜓のロゞックによっお閉じられたずき、これにより、既に壊れたポむンタヌでの凊理。 最初のアむデアから「熊手で」そのような経隓を受け取ったので、別の戊略が採甚されたしたepollによるメむンリスニングストリヌムは、リスニング゜ケットのむベントにのみ応答し、着信接続を受け入れ、その数が埅機キュヌに蚱可されおいる堎合、それらを閉じたすそれ以倖の堎合、受信した接続を凊理のためにキュヌに入れたす。 ワヌクフロヌはこのキュヌを差し匕いお、この゜ケットをepollセットに入れたす。 ワヌクフロヌはepoll蚘述子で動䜜し、すべおが1぀のストリヌムのフレヌムワヌク内で行われたすepollぞの配眮、デヌタ到着のむベントぞの反応、読み取り/曞き蟌み、終了ハンドルが閉じられるず、epollからの削陀はシステムレベルで自動的に行われたす 。 このような組織の結果ずしお、着信接続のキュヌを保護する同期プリミティブは1぀だけです。 䞀方では、リスニングストリヌムのみがこのキュヌに曞き蟌み、他方では、受信された接続のために受信されたフロヌが遞択されたす。 1぀少ない問題。 epoll構造のナヌザヌデヌタを䜿甚したセッションオブゞェクトぞのポむンタヌのバむンドを攟棄したす。 解決策連想配列を䜿甚したす。 キヌは゜ケット蚘述子であり、デヌタはセッションオブゞェクトです。 これにより、むベントが発生したずき、epollむベントからナヌザヌデヌタを取埗する機䌚があるずきだけでなく、䜕らかのロゞックに埓っお、たずえばタむムアりトによっお接続を閉じる必芁があるずき接続プヌルが利甚可胜にセッションを操䜜できたす。



最初のオプションは、完党に1぀のファむルに蚘述され、C/ Java開発者のスタむルで宣蚀ず定矩に分割するこずなく、1800行以䞊のコヌドがありたした。 テストタスクには倚すぎたす。HTTPプロトコルの実装が最小限であるずいう事実にもかかわらず、他の䜕もせずにHTTPヘッダヌのパラメヌタヌを最小限で凊理しおGET / HEADを凊理するための最小限です。 それはポむントではありたせん。 繰り返しになりたすが、テストタスクは単に䜕かを詊す「キック」でした。 この゜リュヌションでの私の䞻な関心は、HTTPプロトコルの実装ではなく、マルチスレッドサヌバヌの実装、接続ずセッションの管理でしたセッションは、接続に関連する凊理アルゎリズムを持぀論理デヌタ構造ずしお理解できたす。

この巚倧なファむルを砎壊し、いく぀かの堎所で実装をずかすず、次のようになりたす。

class TCPServer : private Common::NonCopyable { public: TCPServer(InetAddress const &locAddr, int backlog, int maxThreadsCount, int maxConnectionsCount, UserSessionCreator sessionCreator); private: typedef std::tr1::shared_ptr<Common::IDisposable> IDisposablePtr; typedef std::vector<IDisposablePtr> IDisposablePool; Private::ClientItemQueuePtr AcceptedItems; IDisposablePool Threads; };
      
      





これはおそらく、私が蚘述しなければならなかった最短のサヌバヌクラス実装です。 このクラスは、リスナヌず耇数のワヌカヌの耇数のスレッドのみを䜜成し、それらのホルダヌです。
実装
 TCPServer::TCPServer(InetAddress const &locAddr, int backlog, int maxThreadsCount, int maxConnectionsCount, UserSessionCreator sessionCreator) : AcceptedItems(new Private::ClientItemQueue(backlog)) { int EventsCount = maxConnectionsCount / maxThreadsCount; for (int i = 0 ; i < maxThreadsCount ; ++i) { Threads.push_back(IDisposablePtr(new Private::WorkerThread( EventsCount + (i <= maxThreadsCount - 1 ? 0 : maxConnectionsCount % maxThreadsCount), AcceptedItems ))); } Threads.push_back(IDisposablePtr(new Private::ListenThread(locAddr, backlog, AcceptedItems, sessionCreator))); }
      
      



たた玠晎らしいではありたせん。 䞡方のクラスは
リスニングストリヌム
 class ListenThread : private TCPServerSocket , public Common::IDisposable { public: ListenThread(InetAddress const &locAddr, int backlog, ClientItemQueuePtr acceptedClients, UserSessionCreator sessionCreator) : TCPServerSocket(locAddr, backlog) , AcceptedClients(acceptedClients) , SessionCreator(sessionCreator) , Selector(1, WaitTimeout, std::tr1::bind(&ListenThread::OnSelect, this, std::tr1::placeholders::_1, std::tr1::placeholders::_2)) { Selector.AddSocket(GetHandle(), Network::ISelector::stRead); } private: enum { WaitTimeout = 100 }; ClientItemQueuePtr AcceptedClients; UserSessionCreator SessionCreator; SelectorThread Selector; void OnSelect(SocketHandle handle, Network::ISelector::SelectType selectType) { //  ,  -      } };
      
      



だから
ワヌクフロヌ
 class WorkerThread : private Common::NonCopyable , public Common::IDisposable { public: WorkerThread(int maxEventsCount, ClientItemQueuePtr acceptedClients) : MaxConnections(maxEventsCount) , AcceptedClients(acceptedClients) , Selector(maxEventsCount, WaitTimeout, std::tr1::bind(&WorkerThread::OnSelect, this, std::tr1::placeholders::_1, std::tr1::placeholders::_2), SelectorThread::ThreadFunctionPtr(new SelectorThread::ThreadFunction(std::tr1::bind( &WorkerThread::OnIdle, this)))) { } private: enum { WaitTimeout = 100 }; typedef std::map<SocketHandle, ClientItemPtr> ClientPool; unsigned MaxConnections; ClientItemQueuePtr AcceptedClients; ClientPool Clients; SelectorThread Selector; void OnSelect(SocketHandle handle, Network::ISelector::SelectType selectType) { //  ,      ( ,  ,  ) } void OnIdle() { //   .         -  epoll. } };
      
      



むベントフロヌクラスを䜿甚する
セレクタヌスレッド
 class SelectorThread : private EPollSelector , private System::ThreadLoop { public: using EPollSelector::AddSocket; typedef System::Thread::ThreadFunction ThreadFunction; typedef std::tr1::shared_ptr<ThreadFunction> ThreadFunctionPtr; SelectorThread(int maxEventsCount, unsigned waitTimeout, ISelector::SelectFunction onSelectFunc, ThreadFunctionPtr idleFunc = ThreadFunctionPtr()); virtual ~SelectorThread(); private: void SelectItems(ISelector::SelectFunction &func, unsigned waitTimeout, ThreadFunctionPtr idleFunc); };
      
      



。 このスレッドは
EPollSelector
 class EPollSelector : private Common::NonCopyable , public ISelector { public: EPollSelector(int maxSocketCount); ~EPollSelector(); virtual void AddSocket(SocketHandle handle, int selectType); virtual void Select(SelectFunction *function, unsigned timeout); private: typedef std::vector<epoll_event> EventPool; EventPool Events; int EPoll; static int GetSelectFlags(int selectType); };
      
      



承認された化合物の蚘述子で発生するむベントに察する反応を敎理するため。

サヌバヌの゜ヌスクラスを芋るず、ナヌザヌセッションクラスを䜜成するためのファンクタヌが最埌のパラメヌタヌずしお枡されおいるこずがわかりたす。 ナヌザヌセッションはむンタヌフェヌスの実装です
 struct IUserSession { virtual ~IUserSession() {} virtual void Init(IConnectionCtrl *ctrl) = 0; virtual void Done() = 0; virtual unsigned GetMaxBufSizeForRead() const = 0; virtual bool IsExpiredSession(std::time_t lastActionTime) const = 0; virtual void OnRecvData(void const *buf, unsigned bytes) = 0; virtual void OnIdle() = 0; };
      
      



このむンタヌフェむスの実装に応じお、異なるプロトコルを実装できたす。 Initメ゜ッドずDoneメ゜ッドは、それぞれセッションの開始時ず終了時に呌び出されたす。 GetMaxBufSizeForReadは、デヌタ読み取り操䜜䞭に割り圓おられる最倧バッファヌサむズを返す必芁がありたす。 読み取られたデヌタはOnRecvDataに含たれたす。 セッションが期限切れになったこずを䌝えるには、IsExpiredSessionを適切な方法で実装する必芁がありたす。 OnIdleはいく぀かのアクションの間に呌び出されたす。ここで、セッション実装はいく぀かのバックグラりンドアクションを実行し、むンタヌフェむスを介しお閉じるこずを目的ずしお自身をマヌクできたす。
 struct IConnectionCtrl { virtual ~IConnectionCtrl() { } virtual void MarkMeForClose() = 0; virtual void UpdateSessionTime() = 0; virtual bool SendData(void const *buf, unsigned *bytes) = 0; virtual bool SendFile(int fileHandle, unsigned offset, unsigned *bytes) = 0; virtual InetAddress const& GetAddress() const = 0; virtual SocketTuner GetSocketTuner() const = 0; };
      
      





IConnectionCtrlむンタヌフェむスは、ナヌザヌセッションがネットワヌクにデヌタを送信できるようにSendDataおよびSendFileメ゜ッド、閉じられるように自身をマヌクできるようにMarkMeForCloseメ゜ッド、「生きおいる」UpdateSessionTimeメ゜ッド。 IsExpiredSession、同じセッションは着信接続GetAddressメ゜ッドのアドレスず゜ケット蚭定のSocketTunerオブゞェクト珟圚の接続GetSocketTunerメ゜ッドを取埗できたす。

HTTPプロトコルの実装は、HttpUserSessionクラスにありたす。 䞊で蚀ったように、HTTPの実装は私にずっお最も興味深くも優先床も䜎いので、あたり考えたせんでした。 私は䜕が起こったのかを曞くのに十分であるず正確に思った:)



Libeventの実装



libeventでの実装は今でも私にずっおお気に入りです。 このラむブラリにより、非同期入出力を敎理し、ネットワヌクプログラミングの埮劙な点の倚くを開発者から隠すこずができたす。 これにより、デヌタやその他のむベントの受信、送信、デヌタの非同期送信のためのコヌルバック関数をハングアップするこずにより、生デヌタの凊理を実装できたす。 デヌタの䜎レベルの䜜業に加えお、高レベルのプロトコルもありたす。 libeventにはHTTPサヌバヌが組み蟌たれおいるため、芁求ヘッダヌの解析ず同じ応答ヘッダヌの生成を抜象化できたす。 ラむブラリおよびその他の機胜を䜿甚しおRPCを実装するこずができたす。

組み蟌みを䜿甚しおHTTPサヌバヌを実装する堎合、シヌケンスは次のようになりたす。

  1. event_base_newを呌び出しお、いく぀かの基本的なオブゞェクトを䜜成したすより簡単な堎合のために単玔化されたevent_initもありたす。 オブゞェクトを削陀するペアの関数はevent_base_freeです。
  2. evhttp_newを呌び出しお、HTTP゚ンゞンオブゞェクトを䜜成したす。 evhttp_freeオブゞェクトを削陀するためのペア関数。
  3. フラグの組み合わせでevhttp_set_allowed_methods関数を䜿甚しお、サヌバヌがサポヌトするメ゜ッドを指定できたす。 したがっお、たずえば、GETメ゜ッドのみをサポヌトするには、evhttp_set_allowed_methodsHttp、EVHTTP_REQ_GETのようになりたす。ここで、Httpはステップ2で䜜成された蚘述子です。
  4. evhttp_set_gencbを呌び出しお、着信芁求を凊理するコヌルバック関数を蚭定したす。
  5. evhttp_accept_socket関数を呌び出しお、リスニング゜ケットをHTTPサヌバヌオブゞェクトのむンスタンスに関連付けたす。 リスニング゜ケットは、同じ゜ケット/バむンド/リスンを介しお䜜成および構成できたす。
  6. event_base_loop関数を呌び出しお、むベントルヌプを開始したす。 event_base_dispatchずいう簡単なオプションがありたす。 event_base_loopはルヌプで呌び出す必芁がありたす。 この関数は、むンストヌルされたコヌルバック関数ぞの呌び出しが行われるラむブラリの腞内で䜕か有甚なこずを行うか、䜕もする必芁がない堎合に制埡を返し、この時点で䜕か䟿利なこずができたす。 たた、メッセヌゞ凊理サむクルの寿呜をより簡単に制埡するこずも可胜になりたす。
  7. リク゚ストハンドラでは、evbuffer_add_printfを呌び出しおテキストデヌタを送信するか、ラむブラリにファむル蚘述子を指定しお、evbuffer_add_fileを呌び出しお送信するこずができたす。 これらの関数は、自分で䜜成する時間内に削陀するこずを忘れないでくださいか、芁求フィヌルドevhttp_request :: output_bufferを䜿甚できるバッファヌオブゞェクトで動䜜したす。 すべおの魅力は、これらの関数が非同期であるずいうこずです。 ファむルを送信する䟋では、ファむル蚘述子を同じevbuffer_add_fileに枡すず、すぐに制埡が返され、ファむルが送信された埌、ファむル自䜓が閉じられたす。


すべおが1぀のスレッドで非垞に矎しくなりたすが、刀明したように、マルチスレッドサヌバヌを䜜成するこずも難しくありたせん。 boost ::スレッド、たたはストリヌムの䜜業をカプセル化するクロスプラットフォヌムクラスなどを䜿甚するず、libeventはクロスプラットフォヌムラむブラリであるため、完党にクロスプラットフォヌムの゜リュヌションを取埗できたす。 私自身の実装では、Linuxのストリヌムに察しおのみラッパヌを䜿甚したす。 しかし、これはそれほど重芁ではありたせん。

各ワヌクフロヌのメむンスレッドは、独自の蚘述子を䜜成する必芁がありたす。 手順1〜5を完了したす。 ワヌクフロヌは、メッセヌゞ凊理サむクルのみをねじる必芁がありたす-ステップ6。各ワヌクフロヌでステップ7が実行されたす。 芁玄するず、1぀のリスニング゜ケットを䜜成し、その凊理を耇数のワヌカヌスレッドに課したす。

そのため、実装では、ストリヌム、ファむル、およびコマンドラむン解析甚のプリミティブが既にいく぀かあるため、C/ Javaスタむルで玄200行のGETメ゜ッドのみをサポヌトするHTTPサヌバヌを取埗したした。 起こっおいるこずを完党に制埡しおコヌドを蚘述する䜜業のこの削枛は、喜ばしいこずです。 さらに、䞻芳的に、結果のサヌバヌは少し速く動䜜したすが、最埌にテストを芋おみたしょう...

libeventでのHTTPサヌバヌの実装
 #include <event.h> #include <evhttp.h> #include <unistd.h> #include <string.h> #include <signal.h> #include <vector> #include <iostream> #include <tr1/functional> #include <tr1/memory> #include "tcp_server_socket.h" #include "inet_address_v4.h" #include "thread.h" #include "command_line.h" #include "logger.h" #include "file_holder.h" namespace Network { namespace Private { DECLARE_RUNTIME_EXCEPTION(EventBaseHolder) class EventBaseHolder : private Common::NonCopyable { public: EventBaseHolder() : EventBase(event_base_new()) { if (!EventBase) throw EventBaseHolderException("Failed to create new event_base"); } ~EventBaseHolder() { event_base_free(EventBase); } event_base* GetBase() const { return EventBase; } private: event_base *EventBase; }; DECLARE_RUNTIME_EXCEPTION(HttpEventHolder) class HttpEventHolder : public EventBaseHolder { public: typedef std::tr1::function<void (char const *, evbuffer *)> RequestHandler; HttpEventHolder(SocketHandle sock, RequestHandler const &handler) : Handler(handler) , Http(evhttp_new(GetBase())) { evhttp_set_allowed_methods(Http, EVHTTP_REQ_GET); evhttp_set_gencb(Http, &HttpEventHolder::RawHttpRequestHandler, this); if (evhttp_accept_socket(Http, sock) == -1) throw HttpEventHolderException("Failed to accept socket for http"); } ~HttpEventHolder() { evhttp_free(Http); } private: RequestHandler Handler; evhttp *Http; static void RawHttpRequestHandler(evhttp_request *request, void *prm) { reinterpret_cast<HttpEventHolder *>(prm)->ProcessRequest(request); } void ProcessRequest(evhttp_request *request) { try { Handler(request->uri, request->output_buffer); evhttp_send_reply(request, HTTP_OK, "OK", request->output_buffer); } catch (std::exception const &e) { evhttp_send_reply(request, HTTP_INTERNAL, e.what() ? e.what() : "Internal server error.", request->output_buffer); } } }; class ServerThread : private HttpEventHolder , private System::Thread { public: ServerThread(SocketHandle sock, std::string const &rootDir, std::string const &defaultPage) : HttpEventHolder(sock, std::tr1::bind(&ServerThread::OnRequest, this, std::tr1::placeholders::_1, std::tr1::placeholders::_2)) , Thread(std::tr1::bind(&ServerThread::DispatchProc, this)) , RootDir(rootDir) , DefaultPage(defaultPage) { } ~ServerThread() { IsRun = false; } private: enum { WaitTimeout = 10000 }; bool volatile IsRun; std::string RootDir; std::string DefaultPage; void DispatchProc() { IsRun = true; while(IsRun) { if (event_base_loop(GetBase(), EVLOOP_NONBLOCK)) { Common::Log::GetLogInst() << "Failed to run dispatch events"; break; } usleep(WaitTimeout); } } void OnRequest(char const *resource, evbuffer *outBuffer) { std::string FileName; GetFullFileName(resource, &FileName); try { System::FileHolder File(FileName); if (!File.GetSize()) { evbuffer_add_printf(outBuffer, "Empty file"); return; } evbuffer_add_file(outBuffer, File.GetHandle(), 0, File.GetSize()); File.Detach(); } catch (System::FileHolderException const &) { evbuffer_add_printf(outBuffer, "File not found"); } } void GetFullFileName(char const *resource, std::string *fileName) const { fileName->append(RootDir); if (!resource || !strcmp(resource, "/")) { fileName->append("/"); fileName->append(DefaultPage); } else { fileName->append(resource); } } }; } class HTTPServer : private TCPServerSocket { public: HTTPServer(InetAddress const &locAddr, int backlog, int maxThreadsCount, std::string const &rootDir, std::string const &defaultPage) : TCPServerSocket(locAddr, backlog) { for (int i = 0 ; i < maxThreadsCount ; ++i) { ServerThreads.push_back(ServerThreadPtr(new Private::ServerThread(GetHandle(), rootDir, defaultPage))); } } private: typedef std::tr1::shared_ptr<Private::ServerThread> ServerThreadPtr; typedef std::vector<ServerThreadPtr> ServerThreadPool; ServerThreadPool ServerThreads; }; } int main(int argc, char const **argv) { if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) { std::cerr << "Failed to call signal(SIGPIPE, SIG_IGN)" << std::endl; return 0; } try { char const ServerAddr[] = "Server"; char const ServerPort[] = "Port"; char const MaxBacklog[] = "Backlog"; char const ThreadsCount[] = "Threads"; char const RootDir[] = "Root"; char const DefaultPage[] = "DefaultPage"; // Server:127.0.0.1 Port:5555 Backlog:10 Threads:4 Root:./ DefaultPage:index.html Common::CommandLine CmdLine(argc, argv); Network::HTTPServer Srv( Network::InetAddressV4::CreateFromString( CmdLine.GetStrParameter(ServerAddr), CmdLine.GetParameter<unsigned short>(ServerPort)), CmdLine.GetParameter<unsigned>(MaxBacklog), CmdLine.GetParameter<unsigned>(ThreadsCount), CmdLine.GetStrParameter(RootDir), CmdLine.GetStrParameter(DefaultPage) ); std::cin.get(); } catch (std::exception const &e) { Common::Log::GetLogInst() << e.what(); } return 0; }
      
      





boost.asioでの実装



boost.asioはboostの䞀郚であり、ネットワヌクアプリケヌション、およびクロスプラットフォヌムアプリケヌションの開発を倧幅に削枛するのに圹立ちたす。 ラむブラリは、開発者から倚くのルヌチンを隠したす。

ブヌストでHTTPサヌバヌの実装を䜜成したせんでした。 完成した䟋をboost.asioに取りたした。 マルチスレッドHTTPサヌバヌの䟋。 HTTPサヌバヌ3この䟋の実装は、䞊蚘の䟋ず組み合わせたテストに非垞に適しおいたす。

テスト甚のHTTPサヌバヌの実装がありたすが、䞀般的な原則に぀いお話すのは悪くありたせん...残念ながら、libeventずは異なり、boost.asioはHTTPなどに類䌌した高レベルのプロトコルをサポヌトしおいたせん。 この堎合、ラむブラリはTCPを介したネットワヌクでの䜜業を隠したすが、プロトコルヘッダヌを収集および解析するには、HTTPの実装を開発者自身が行う必芁がありたす。

以䞋は、このトピックに照らしおHTTPヘッダヌを解析/収集するのはあたり面癜くないので、説明付きのマルチスレッド゚コヌサヌバヌの小さな䟋です。 boost.asioを䜿甚しおマルチスレッドサヌバヌを䜜成する手順は次のずおりです。

  1. boost :: asio :: io_serviceおよびboost :: asio :: ip :: tcp ::アクセプタヌクラスのオブゞェクトを䜜成したす。
  2. boost :: asio :: ip :: tcp :: resolverおよびboost :: asio :: ip :: tcp ::゚ンドポむントを䜿甚するず、リスニング゜ケットがバむンドされるロヌカルアドレスがラむブラリで䜿甚される構造に倉換されたす。
  3. bindを呌び出しお、クラスboost :: asio :: ip :: tcp :: acceptorのオブゞェクトでリッスンしたす。
  4. クラス「接続」を䜜成したす。 別名「セッション」。着信ナヌザヌ接続を受信するずきにそのむンスタンスが䜿甚されたす。
  5. 適切なコヌルバック関数を構成しお、着信接続を受信し、デヌタを受信したす。
  6. boost :: asio :: io_service :: runを呌び出しお、メッセヌゞ凊理ルヌプを開始したす。


たた、libeventの䟋ず同様に、マルチスレッドサヌバヌは、䞊蚘の䞀連の手順を䜿甚しおシングルスレッドサヌバヌから非垞に簡単に䜜成できたす。 この堎合、シングルスレッドサヌバヌずマルチスレッドサヌバヌの違いは、マルチスレッド実装の各スレッドでboost :: asio :: io_service :: runメ゜ッドを呌び出す必芁があるこずだけです。
boost.asioでの゚コヌサヌバヌの実装
 #include <boost/noncopyable.hpp> #include <boost/asio.hpp> #include <boost/shared_ptr.hpp> #include <boost/thread.hpp> #include <boost/make_shared.hpp> #include <boost/bind.hpp> #include <boost/enable_shared_from_this.hpp> #include <boost/array.hpp> namespace Network { namespace Private { class Connection : private boost::noncopyable , public boost::enable_shared_from_this<Connection> { public: Connection(boost::asio::io_service &ioService) : Strand(ioService) , Socket(ioService) { } boost::asio::ip::tcp::socket& GetSocket() { return Socket; } void Start() { Socket.async_read_some(boost::asio::buffer(Buffer), Strand.wrap( boost::bind(&Connection::HandleRead, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred) )); } void HandleRead(boost::system::error_code const &error, std::size_t bytes) { if (error) return; std::vector<boost::asio::const_buffer> Buffers; Buffers.push_back(boost::asio::const_buffer(Buffer.data(), bytes)); boost::asio::async_write(Socket, Buffers, Strand.wrap( boost::bind(&Connection::HandleWrite, shared_from_this(), boost::asio::placeholders::error) )); } void HandleWrite(boost::system::error_code const &error) { if (error) return; boost::system::error_code Code; Socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, Code); } private: boost::array<char, 4096> Buffer; boost::asio::io_service::strand Strand; boost::asio::ip::tcp::socket Socket; }; } class EchoServer : private boost::noncopyable { public: EchoServer(std::string const& locAddr, std::string const& port, unsigned threadsCount) : Acceptor(IoService) , Threads(threadsCount) { boost::asio::ip::tcp::resolver Resolver(IoService); boost::asio::ip::tcp::resolver::query Query(locAddr, port); boost::asio::ip::tcp::endpoint Endpoint = *Resolver.resolve(Query); Acceptor.open(Endpoint.protocol()); Acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true)); Acceptor.bind(Endpoint); Acceptor.listen(); StartAccept(); std::generate(Threads.begin(), Threads.end(), boost::bind( &boost::make_shared<boost::thread, boost::function<void ()> const &>, boost::function<void ()>(boost::bind(&boost::asio::io_service::run, &IoService)) )); } ~EchoServer() { std::for_each(Threads.begin(), Threads.end(), boost::bind(&boost::asio::io_service::stop, &IoService)); std::for_each(Threads.begin(), Threads.end(), boost::bind(&boost::thread::join, _1)); } private: boost::asio::io_service IoService; boost::asio::ip::tcp::acceptor Acceptor; typedef boost::shared_ptr<Private::Connection> ConnectionPtr; ConnectionPtr NewConnection; typedef boost::shared_ptr<boost::thread> ThreadPtr; typedef std::vector<ThreadPtr> ThreadPool; ThreadPool Threads; void StartAccept() { NewConnection = boost::make_shared<Private::Connection, boost::asio::io_service &>(IoService); Acceptor.async_accept(NewConnection->GetSocket(), boost::bind(&EchoServer::HandleAccept, this, boost::asio::placeholders::error)); } void HandleAccept(boost::system::error_code const &error) { if (!error) NewConnection->Start(); StartAccept(); } }; } int main() { try { Network::EchoServer Srv("127.0.0.1", "5555", 4); std::cin.get(); } catch (std::exception const &e) { std::cerr << e.what() << std::endl; } return 0; }
      
      







テスト䞭



受け取った工芞品を比范する時が来たした...

すべおが開発およびテストされたプラットフォヌムは、4GBのRAMずUbuntu 12.04デスクトップを実行する2コアプロセッサを搭茉した通垞のラップトップです。

たず、テスト甚のナヌティリティを配眮したす。

 sudo apt-get install apache2-utils
      
      



この方法でテストしたす
 ab -c 100 -k -r -t 5 "http://127.0.0.1:5555/test.jpg"
      
      



すべおのサヌバヌに぀いお、4぀のワヌクフロヌ、100の䞊列接続、2496629バむトの送信甚ファむル、および5秒の掚定時間間隔が蚭定されたした。

結果

Epollの実装
ベンチマヌク127.0.0.1忍耐匷く

完了した2150リク゚スト



サヌバヌ゜フトりェアMyTestHttpServer

サヌバヌのホスト名127.0.0.1

サヌバヌポヌト5555



ドキュメントパス/test.jpg

ドキュメントの長さ2496629バむト



同時実行レベル100

テストにかかった時間5.017秒

完党なリク゚スト2150

倱敗したリク゚スト0

曞き蟌み゚ラヌ0

キヌプアラむブ芁求0

転送された合蚈5389312814バむト

転送されるHTML5388981758バむト

1秒あたりのリク゚スト428.54 [/ sec]平均

リク゚ストあたりの時間233.348 [ms]平均

リク゚ストあたりの時間2.333 [ms]平均、すべおの同時リク゚スト党䜓

転送速床1049037.42 [キロバむト/秒]受信



接続時間ミリ秒

最小平均[±sd]最倧䞭倮倀

接続0 0 0.5 0 3

凊理74226 58.2 229 364

埅機䞭2 133 64.8 141 264

合蚈77226 58.1 229 364



Libeventの実装
ベンチマヌク127.0.0.1忍耐匷く

完了した1653リク゚スト



サヌバヌ゜フトりェア

サヌバヌのホスト名127.0.0.1

サヌバヌポヌト5555



ドキュメントパス/test.jpg

ドキュメントの長さ2496629バむト



同時実行レベル100

テストにかかった時間5.008秒

完党なリク゚スト1653

倱敗したリク゚スト0

曞き蟌み゚ラヌ0

キヌプアラむブリク゚スト1653

転送された合蚈4263404830バむト

転送されるHTML4263207306バむト

1秒あたりのリク゚スト330.05 [/秒]平均

リク゚ストあたりの時間302.987 [ms]平均

リク゚ストあたりの時間3.030 [ms]平均、すべおの同時リク゚スト党䜓

転送速床831304.15 [キロバむト/秒]受信



接続時間ミリ秒

最小平均[±sd]最倧䞭倮倀

接続0 53 223.3 0 1000

凊理3 228 275.5 62 904

埅機䞭0 11 42.5 5 639

合蚈3 280 417.9 62 1864



boost.asioでの実装
ベンチマヌク127.0.0.1忍耐匷く

終了した639リク゚スト



サヌバヌ゜フトりェア

サヌバヌのホスト名127.0.0.1

サヌバヌポヌト5555



ドキュメントパス/test.jpg

ドキュメントの長さ2496629バむト



同時実行レベル100

テストにかかった時間5.001秒

完党なリク゚スト639

倱敗したリク゚スト0

曞き蟌み゚ラヌ0

キヌプアラむブ芁求0

転送された合蚈1655047414バむト

転送されるHTML1654999464バむト

1秒あたりのリク゚スト127.78 [/ sec]平均

リク゚ストあたりの時間782.584 [ms]平均

リク゚ストあたりの時間7.826 [ms]平均、すべおの同時リク゚スト党䜓

転送速床323205.36 [Kバむト/秒]受信



接続時間ミリ秒

最小平均[±sd]最倧䞭倮倀

接続0 0 1.1 0 4

凊理286 724 120.0 689 1106

埅機䞭12 364 101.0 394 532

合蚈286724 120.0 689 1106



結果は衚にたずめられおいたす。

゚ポヌル libevent boost.asio
完党なリク゚スト 2150 1653 639
転送された合蚈バむト 5389312814 4263404830 1655047414
転送されたHTMLバむト 5388981758 4263207306 1654999464
1秒あたりのリク゚スト[秒]平均 428.54 330.05 127.78
リク゚ストあたりの時間[ms]平均 233.348 302.987 782.584
受信した転送速床[キロバむト/秒] 1049037.42 831304.15 323205.36


嘘には、嘘、露骚な嘘、統蚈の3皮類がありたす。 認めざるを埗ないが、結果は私を喜ばせざるを埗ない。 結果に特別な泚意を払うべきではないず思いたすが、サヌバヌ゜フトりェア甚の開発ツヌルの遞択を決定する際に圹立぀かもしれないサポヌト情報ずしおそれらを芋るこずができたす。 結果を正確にするために、サヌバヌハヌドりェア䞊で耇数の実行、ネットワヌク䞊の他のマシンで実行䞭のクラむアントなどを配眮するこずをお勧めしたす。

100䞊列ク゚リ-これは小さいように芋えたすが、そのような控えめな条件でテストするには十分です。 もちろん、䜕千もの䞊列ク゚リの結果を確認したいのですが、すでに他の芁因がありたす。 そのような芁因の1぀は、プロセスで同時に開くこずができるファむル蚘述子の数です。 getrlimitおよびsetrlimit関数を呌び出すこずにより、いく぀かのプロセスパラメヌタを芋぀けお蚭定できたす。 プロセスごずに割り圓おられおいるファむル蚘述子の数を調べるには、rlimit構造䜓のフラグRLIMIT_NOFILEを指定しおgetrlimitを呌び出すこずができたす。 オペレヌティングシステムの堎合、これらはデフォルトでプロセスごずに1024個のファむル蚘述子であり、プロセスに蚭定できる最倧4096個です。 
 , , . Linux





WebServer «» , , . . , - , - , - - . , epoll . , , . « », , , C++, C, stl, .

, libevent, , , , . .

, boost . . boost.asio . «» , , .

Linux (aio) , «» .



SVN . , . しかし , . « » , , — , — , — . :)



«» API .









ご枅聎ありがずうございたした




All Articles