一个简单的 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请求处理器进行详细的定制,这超出了本文的范围。