用現代 C++ 寫一個高效能的伺服器
本文由 伯樂線上 - 袁欣 翻譯,艾凌風 校稿。未經許可,禁止轉載!
英文出處:James Perry。歡迎加入翻譯組。
首先感謝大家對上一篇博文《用 C++ 開啟技術創業之旅》的反饋。在上篇文章中我提到我曾在一天內就憑藉 Facebook 的 Wangle 搭建起一個數據庫引擎的原型,在這裡我會解釋我是如何做到的。到本文最後,你將可以用 Wangle 編寫出一個高效能的C++伺服器。本文也將作為教程整合進 Wangle 的 ReadMe.md 中。
我將展示如何使用現代C++編寫一個Echo伺服器,相當於分散式系統開發中的“Hello World”。這個伺服器會將接收的訊息直接返回。我們同時需要一個可以向我們的伺服器發動訊息的客戶端,在這裡可以發現客戶端的
Wangle是一個用來搭建事件驅動的現代非同步C++服務的C/S應用框架。Wangle最基本的抽象概念就是Pipeline(管線)。能夠理解這種抽象,將會很容易寫出各種複雜的現代C++服務,另一個重要的概念是Service(服務),其可以看作一種更高階的Pipeline,不過超出了本文我們關注的範疇。
PipeLine
pipeline 是 Wangle 中最重要也是最強大的抽象,可以讓使用者在定製 request 和 response 的實現時擁有很大的自由。一個pipeline就是一系列request/response控制程式的巢狀。我試圖尋找一個真實世界中pipeline的類比,唯一我能想到的就是現實世界工廠中的生產線。一條生產線工作在一種順序模式下,所有的工人取得一個物體,並且只新增一種修改,再將其傳送給上游的工人直到整個產品製造完成。這可能不是一個特別好的比喻,因為流水線上產品的流動是單向的,而一個pipeline能控制反方向的資料流動--就好像將成品分解成原材料。
一個Wangle handler可以同時掌控上游和下游的兩個方向的資料流動。當你把所有的handler連線在一起,就可以用一種靈活的方式將原始資料組裝為想要的資料型別或者將已有的資料拆分。
在我們的伺服器的pipeline中大致將會有下面幾種handler:
1.Handler 1 (下文的上游下游是指對一同個handler而言,根據其在pipeline中的位置不同,輸入輸出相反) 上游:將從socket中接收的二進位制資料流寫入一個零拷貝(zero-copy,指省略了Applicaion context和Kernel context之間的上下文切換,避免了CPU對Buffer的冗餘拷貝,直接在Kernel級別進行資料傳輸的技術,詳情請參閱維基百科)的位元組快取中,傳送給handler2
下游:接收一個零拷貝的位元組快取,將其內容寫入socket中
2.Handler2 上游:接收handler1的快取物件,解碼為一個string物件傳遞給handler3 下游:接收handler3的string物件,將其轉碼為一個零拷貝的位元組快取,傳送給handler1
3.Handler3 上游:接收handler2中的string物件,再向下發送至pipeline等待寫回客戶端。string會發回handler2 下游:接收上游的string物件,傳遞給handler2
需要注意的一點是,每一個handler應當只做一件事並且只有一件,如果你有一個handler裡做了多項任務,比如從二進位制流離直接解碼出string,那麼你需要學會將它拆分。這對提升程式碼的可維護性和擴充套件性非常重要。
另外,沒錯,handler不是執行緒安全的,所以不要輕易的在其中使用任何沒有經過mutex,atomic lock保護的資料,如果你確實需要一個執行緒安全的環境,Folly提供了一種免於加鎖的資料結構, Folly依賴於Wangle,你可以很容易的在專案中引入並使用它。
如果你還不是很明白所有的步驟,不用著急,在看到下面的具體實現時你會更加清楚。
Echo Server
下面我會展示伺服器的具體實現。我假定您已經安裝好Wangle。需要注意的是截至目前Wangle還不能在Mac OS上安裝,我建議您可以安裝虛擬機器,使用Ubuntu來安裝Wangle。
這就是echo handler:接收一個string,列印到stdout中,再發送回pipeline。要注意write語句中的定界符不可以省略,因為pipeline會按照位元組解碼。
C++
1 2 3 4 5 6 7 8 9 |
// the main logic of our echo server; receives a string and writes it straight // back class EchoHandler : public HandlerAdapter { public: virtual void read(Context* ctx, std::string msg) override { std::cout << "handling " << msg << std::endl; write(ctx, msg + "rn"); } }; |
Echohandler其實是我們pipeline的最後一個handler,現在我們需要建立一個PipelineFactory來控制所有的request和response。
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// where we define the chain of handlers for each messeage received class EchoPipelineFactory : public PipelineFactory { public: EchoPipeline::Ptr newPipeline(std::shared_ptr sock) { auto pipeline = EchoPipeline::create(); pipeline->addBack(AsyncSocketHandler(sock)); pipeline->addBack(LineBasedFrameDecoder(8192)); pipeline->addBack(StringCodec()); pipeline->addBack(EchoHandler()); pipeline->finalize(); return pipeline; } }; |
pipeline中每一個handler的插入順序都需要嚴格注意,因為它們是按照先後排序的,此處我們有4個handler
1.AsyncSocketHandler: 上游:讀取scoket中的二進位制流轉換成零拷貝位元組快取 下游:將位元組快取內容寫入底層socket 2. LineBasedFrameDecoder: 上游:接收位元組快取,按行分割資料 下游:將位元組快取傳送給AsyncSocketHandler 3. StringCodec: 上游:接收位元組快取,解碼為std:string傳遞給EchoHandler 下游:接收std:string, 編碼為位元組快取,傳遞給LineBasedFrameDecoder 4. EchoHandler: 上游:接收std:string物件,將其寫入pipeline-將訊息返回給Echohandler。 下游:接收一個std:string物件,轉發給StringCodec Handler。 現在我們所需要做的就是將pipeline factory關聯到ServerBootstrap,繫結一個埠,這樣我們已經完成了 基本上所有的工作。
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#include <gflags/gflags.h>
#include <wangle/bootstrap/ServerBootstrap.h> #include <wangle/channel/AsyncSocketHandler.h> #include <wangle/codec/LineBasedFrameDecoder.h> #include <wangle/codec/StringCodec.h>
using namespace folly; using namespace wangle;
DEFINE_int32(port, 8080, "echo server port");
typedef Pipeline<IOBufQueue&, std::string> EchoPipeline;
// the main logic of our echo server; receives a string and writes it straight // back class EchoHandler : public HandlerAdapter<std::string> { public: virtual void read(Context* ctx, std::string msg) override { std::cout << "handling " << msg << std::endl; write(ctx, msg + "\r\n"); } };
// where we define the chain of handlers for each messeage received class EchoPipelineFactory : public PipelineFactory<EchoPipeline> { public: EchoPipeline::Ptr newPipeline(std::shared_ptr<AsyncTransportWrapper> sock) { auto pipeline = EchoPipeline::create(); pipeline->addBack(AsyncSocketHandler(sock)); pipeline->addBack(LineBasedFrameDecoder(8192)); pipeline->addBack(StringCodec()); pipeline->addBack(EchoHandler()); pipeline->finalize(); return pipeline; } };
int main(int argc, char** argv) { google::ParseCommandLineFlags(&argc, &argv, true);
ServerBootstrap<EchoPipeline> server; server.childPipeline(std::make_shared<EchoPipelineFactory>()); server.bind(FLAGS_port); server.waitForStop();
return 0; } |
至此我們一共只寫了48行程式碼就完成了一個高效能的非同步C++伺服器。
Echo Client
echo客戶端的實現與我們的服務端非常類似:
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// the handler for receiving messages back from the server class EchoHandler : public HandlerAdapter { public: virtual void read(Context* ctx, std::string msg) override { std::cout << "received back: " << msg; } virtual void readException(Context* ctx, exception_wrapper e) override { std::cout << exceptionStr(e) << std::endl; close(ctx); } virtual void readEOF(Context* ctx) override { std::cout << "EOF received :(" << std::endl; close(ctx); } }; |
注意我們過載了readException和readEOF兩個方法,還有其他一些方法可以被過載。如果你需要控制某個特別的事件,只需要過載對應的虛擬函式即可。
這是客戶端的pipeline factory的實現,與我們的服務端結構基本一致,只有EventBaseHandler這個handler在服務端程式碼中不曾出現,它可以確保我們可以從任意一個執行緒寫入資料。
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// the handler for receiving messages back from the server class EchoHandler : public HandlerAdapter { public: virtual void read(Context* ctx, std::string msg) override { std::cout << "received back: " << msg; } virtual void readException(Context* ctx, exception_wrapper e) override { std::cout << exceptionStr(e) << std::endl; close(ctx); } virtual void readEOF(Context* ctx) override { std::cout << "EOF received :(" << std::endl; close(ctx); } }; |
客戶端所有的程式碼如下圖所示
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
#include <gflags/gflags.h> #include
#include <wangle/bootstrap/ClientBootstrap.h> #include <wangle/channel/AsyncSocketHandler.h> #include <wangle/channel/EventBaseHandler.h> #include <wangle/codec/LineBasedFrameDecoder.h> #include <wangle/codec/StringCodec.h>
using namespace folly; using namespace wangle;
DEFINE_int32(port, 8080, "echo server port"); DEFINE_string(host, "::1", "echo server address");
typedef Pipeline<folly::IOBufQueue&, std::string> EchoPipeline;
// the handler for receiving messages back from the server class EchoHandler : public HandlerAdapter { public: virtual void read(Context* ctx, std::string msg) override { std::cout << "received back: " << msg; } virtual void readException(Context* ctx, exception_wrapper e) override { std::cout << exceptionStr(e) << std::endl; close(ctx); } virtual void readEOF(Context* ctx) override { std::cout << "EOF received :(" << std::endl; close(ctx); } };
// chains the handlers together to define the response pipeline class EchoPipelineFactory : public PipelineFactory { public: EchoPipeline::Ptr newPipeline(std::shared_ptr sock) { auto pipeline = EchoPipeline::create(); pipeline->addBack(AsyncSocketHandler(sock)); pipeline->addBack( EventBaseHandler()); // ensure we can write from any thread pipeline->addBack(LineBasedFrameDecoder(8192, false)); pipeline->addBack(StringCodec()); pipeline->addBack(EchoHandler()); pipeline->finalize(); return pipeline; } };
int main(int argc, char** argv) { google::ParseCommandLineFlags(&argc, &argv, true);
ClientBootstrap client; client.group(std::make_shared(1)); client.pipelineFactory(std::make_shared()); auto pipeline = client.connect(SocketAddress(FLAGS_host, FLAGS_port)).get();
try { while (true) { std::string line; std::getline(std::cin, line); if (line == "") { break; }
pipeline->write(line + "rn").get(); if (line == "bye") { pipeline->close(); break; } } } catch (const std::exception& e) { std::cout << exceptionStr(e) << std::endl; }
return 0; } |
程式用一個While迴圈不斷監測使用者的輸入,並且依靠呼叫.get() 來同步等待一直到請求被響應。
總結
本文我展示瞭如何用Wangle來編寫一個簡易的高效能C++伺服器。您應該已經掌握了Wangle的一些基本知識,並且有信心寫出自己的C++伺服器。我建議您深入瞭解Wangle中的Service概念,它會有助您開發出更加