一個簡單的 C++ 嵌入 Web 服務器
一個簡單的 C++ 嵌入 Web 服務器
引言
你有一兩個網頁吧?不一定是多么神奇的東西,但一個通過幾個HTML標簽作出的簡潔的演示就可以。你有一個需要遠程控制的復雜的C++ Windows 桌面應用程序吧?所以,不需要學習一個全新的技術,讓我們一起為您的應用添加WEB頁面吧。
Webem是一個可以嵌入你的C++應用程序的WEB服務器。它可以輕松地實現一個從任何地方都能訪問的瀏覽器GUI。
Webem基于一個簡化版的boost::asio WEB服務器,它可以讓HTML代碼執行C++方法。盡管你不需要查看服務器代碼來使用Webem,但你需要為你的工程下載和使用BOOST庫。我建議如果你從未使用過BOOST,那Webem可能不適合你。
背景
現有的嵌入式C++ web服務器使用起來是一個挑戰,并且有不對Windows友好的趨勢. 它們也不是你想要用來為你的實驗性應用程序加入可以用手機進行監控的能力的那種東西.
我嘗試過 (http://www.webtoolkit.eu/wt) ,但在安裝和學習中挫敗了.
最近我開始使用John Bartas的Webio. 我喜愛其理念,它也運作的很好.
然而,我仍然發現它在使用時過于復雜,并且服務端代碼也難于理解. 我想要的是一個容易使用,基于一個知名web服務器,只做了輕微修改的好東東.
Webio的許多復雜性是有使用一個HTML編譯器來隱藏控制著嵌入于應用程序代碼里面的文件系統的外觀的HTML頁面所造成的. 我更喜歡將HTML頁面放在外部一個通常的視圖中,那樣我就可以不用重新編譯程序,卻可以調整GUI.
我在嘗試調整Webio以符合自己喜好的過程中了解了很多, 最終決定準備去構建一個能切實滿足我自己的需求的東西.
代碼使用
你可以像建立網站一樣來建立的你應用程序GUI-從index.html開始使用HTML新建頁面。
現在你需要使HTML調用你的C++方法。你需要做以下三件事:
-
創建被包含在WEB頁面里的能生成HTML的include(包含)方法。
-
創建當用戶點擊WEB頁面里的按鈕時被webem調用的action(動作)方法。它們可以是簡單的按鈕,或html表單。
-
創建"web控件",即上面兩個的組合,一個include方法生成表單,當用戶點擊按鈕時這個表單會調用action方法-示例程序"Calendar"展示了如何創建一個顯示和更新數據表的控件。
"Hello,world!"
第一步:新建web頁面。你可以隨你喜好將頁面設計的很精心,但是對于我們的第一個“hello,world”應用,頁面當然是越簡單越好:
The Webem Embedded Web server says: <!--#webem hello -->
在尖括號中的文本告訴webem在應用中何處將文本包含進來,“hello”則是指定的應用方法,必需被調用來提供包含的文本。
第二步:新建類,該類返回“hello”:
/// An application class which says hello class cHello { public: char * DisplayHTML() { return "Hello World"; } };
第三步:初始化wenem,配置端口和地址來監聽瀏覽器請求并且去找到index.html作為web頁面的首頁。
// Initialize web server. http::server::cWebem theServer( "0.0.0.0", // address "1570", // port ".\\"); // document root
第四步:用webem注冊應用方法:
cHello hello; // register application method // Whenever server sees <!--#webem hello --> // call cHello::DisplayHTML() and include the HTML returned theServer.RegisterIncludeCode( "hello", boost::bind( &cHello::DisplayHTML, // member function &hello ) ); // instance of class
第五步:最后,你已經準備好了啟動服務。
// run the server theServer.Run();
一個正式的Hello
讓我們來創建一個更儒雅的程序,可通過姓名(如CodeProject,Canadian)來定位網絡。
第一步:新建站點:
What is your name, please? <form action=name.webem> <input name=yourname /><input value="Enter" type=submit /> </form> The Webem Embedded Web server says: <!--#webem hello -->
該表單提供一個文本域,可以使用戶輸入姓名。另外還有一個提交按鈕將姓名提交給服務器。表單屬性“action=name.webem”確保webem服務器會調用應用通過“name”注冊的方法來處理輸入。
第二步:創建應用類:
/// An application class which says hello to the identified user class cHelloForm { string UserName; http::server::cWebem& myWebem; public: cHelloForm( http::server::cWebem& webem ) : myWebem( webem ) { myWebem.RegisterIncludeCode( "hello", boost::bind( &cHelloForm::DisplayHTML, // member function this ) ); // instance of class myWebem.RegisterActionCode( "name", boost::bind( &cHelloForm::Action, // member function this ) ); // instance of class } char * DisplayHTML() { static char buf[1000]; if( UserName.length() ) sprintf_s( buf, 999, "Hello, %s", UserName.c_str() ); else buf[0] = '\0'; return buf; } char * Action() { UserName = myWebem.FindValue("yourname"); return "/index.html"; } };
這個類存儲了對webem服務器的引用. 它允許在其構建時維護對其自身方法的注冊, 并調用cWebem類的FindValue()來提取輸入表單域中的值.
這個類需要注冊兩個方法,一個在表單提交時保存輸入的用戶名,一個用來在頁面被組合起來發送給瀏覽器時展示被存儲的用戶名稱.
動作方法必須返回要在提交按鈕點擊響應中展示的web頁面.
注意所有的動作方法都是有Webem在包含方法之前調用的,所以web頁面總是會展示更新了的數據.
第三步:構建webem,構建應用類并啟動服務器:
// Initialize web server http::server::cWebem theServer( "0.0.0.0", // address "1570", // port ".\\"); // document root // Initialize application code cHelloForm hello( theServer ); // run the server theServer.Run();
你可能需要在其他線程中啟動服務器,這樣你應用能夠繼續在實驗室裝置中記錄日志數據。為了做到這一點,更改對server::run的調用:
boost::thread* pThread = new boost::thread( boost::bind( &http::server::server::run, // member function &theServer ) ); // instance of class
Webem 控件
Webem控件是以標準方式監視顯示和操作應用數據的細節的類,所以應用程序開發者不需要關注生成HTML文本的所有細節。
示例程序使用一個webem來列出一個SQLITE數據表的所有內容,且提供了添加和刪除記錄的能力。在本文的開始是它的截圖。
Unicode
Webem 支持 Unicode 應用程序包含功能, 這意味著你的代碼可以生成和展示中文, 西里爾語,甚至是克靈貢字符. 編寫一個返回寬泛的字符串UTF-16編碼的包含函數, 并使用RegisterIncludeCodeW() 函數 ( 而不是RegisterIncludeCode() ) 來將其注冊,Webem就會在將其發送到瀏覽器之前先轉換成UTF-8 編碼:
class cHello { public: /** Hello to the wide world, returning a wide character UTF-32 encoded string with chinese characters */ wchar_t * DisplayWWHello() { return L"Hello Wide World. Here are some chinese characters: \x751f\x4ea7\x8bbe\x7f6e"; } }; ... theServer.RegisterIncludeCodeW( "wwwhello", boost::bind( &cHello::DisplayWWHello, // member function &hello ) ); // instance of class
如果你想要知道為什么UTF-8和UTF-16是必要的,你看我的一篇博文 環球編碼.
簡單的按鈕動作
有時你可能需要在用戶點擊一個按鈕時運行一個動作,但是你并不需要傳入任何參數。這種情況要設置一個表單看起來有點太麻煩,并且也限制了你可以展示的形式。因此我增加了一個點擊動作請求.
如果你將下面這個東西加入你的htm文件
<a href="http:/index.html/webem_name">button_label</a>
那么當用戶點擊"button_label"時,webem就會調用注冊到"name"上的函數并在index.html進行展示.
要點
本節描述了webem是如何同服務器集成的。要使用webem沒必要按順序讀下去.
boost::asio HTTP 服務器會調用下面的方法:
request_handler::handle_request( const request& req, reply& rep)
這里就是瀏覽器請求被轉換、新頁面被組裝并發送給瀏覽器的地方. 我們需要用一個特別定制的請求處理器來重載這個方法,以便可以回頭注冊的應用程序方法可以被調用.
void cWebemRequestHandler::handle_request( const request& req, reply& rep) { // check for webem action request request req_modified = req; myWebem.CheckForAction( req_modified.uri ); // call base method to do normal handling request_handler::handle_request( req_modified, rep); // Find and include any special cWebem strings myWebem.Include( rep.content ); }
不幸的事, boost::asio 服務器盡管有非常優雅的設計和實現,但其初衷并不是設計用于繼承. 為了讓服務器可以調用webem的請求處理器,我對bootst代碼做了最小化的修改:
-
讓request_handler::handle_request方法編程 virtual 的,以便可以調用特殊化的重載
-
改變服務器構造器以接受一個對于其將會使用的請求處理器的引用.
-
改變服務器參數的書序,以便連接處理器不會在請求處理器之前被初始化.
處理Post請求
boost:asio服務器不能處理常用于輸入密碼的post請求。為不同的瀏覽器添加此特性并使其工作需要為asio請求處理器進行詳細的定制,這超出了本文的范圍。