Windows Sockets 流 第三部分 - HTTP 服务器





5.00/5 (2投票s)
用 20 行或更少的代码创建一个 HTTP 服务器
引言
本系列上一篇文章介绍了如何创建多线程 TCP 服务器。下一步是创建一个嵌入式 HTTP 服务器。
嵌入式服务器可以快速访问应用程序内的变量或结构,并能为用户界面设计带来完全不同的体验。
此服务器可以做什么
- 提供对应用程序数据结构的访问
- 提供 HTML 和其他媒体文件的服务
- 通过基本身份验证进行访问控制
它缺少什么
- 无 SSL 支持
- 无摘要身份验证
- 无日志记录
背景
简要回顾一下我们目前的状态:我们从一个作为 Windows 套接字包装器的 sock
对象开始。基于它,tcpserver
对象实现了一个监听新连接的多线程 TCP 服务器。当客户端连接时,它会创建一个新的线程来处理该连接。应用程序不与这些连接线程进行太多交互;所有配置都通过主服务器对象完成。
连接线程实现 HTTP 协议,可以通过 SSI 类型接口检索变量。它们还可以调用特定的应用程序函数。
httpd 对象
这是监听新 HTTP 连接的服务器对象。它继承自 tcpserver
,并在新客户端连接时创建一个 http_connection
线程对象。
使用此 HTTP 服务器的最简单的应用程序是
using namespace mlib;
int main (int argc, char **argv)
{
httpd server (8080);
server.start ();
while (!_kbhit())
;
server.terminate ();
}
主应用程序线程创建服务器对象并启动它。默认情况下,服务器监听端口 80,但在本例中,它将监听端口 8080。主线程等待按键,然后毫不留情地关闭服务器。
现在,让我们创建一个文件 index.html,内容如下
<html>
<body>Hello world!</body>
</html>
您可以尝试将浏览器连接到 https://:8080
,作为响应,服务器将发送 index.html 文件的内容。
用户变量
您可以使用 add_var
函数使应用程序变量可供服务器访问。要了解其工作原理,首先让我们修改极简服务器
using namespace mlib;
int main (int argc, char **argv)
{
int answer = 42;
httpd server (8080);
server.add_var ("computer_answer", "%d", &answer);
server.start ();
while (!_kbhit())
;
server.terminate ();
}
现在我们创建一个文件 page1.shtml,内容如下
<html>
<body>
The answer to the "Ultimate Question of Life, the Universe, and Everything",<br/>
is <!--#echo var="computer_answer" -->.
<body/>
<html/>
毫不意外,如果您导航到 https://:8080/page1.shtml
,您将看到类似以下的页面
add_var
函数的签名是
void add_var (const char *name, const char *fmt, void *addr, double multiplier=1.);
其中
name
是变量在 SSI 'var
' 构造中显示的外部名称。fmt
是用于生成 SSI 替换string
的printf
样式的格式。addr
是变量的地址。multiplier
是用于缩放浮点值的可选参数。
http_connection 对象
当客户端连接到服务器时,服务器会创建一个 http_connection
对象。此对象继承自 mlib::thread
并实现 HTTP 协议。在许多情况下,您不需要直接与这些对象交互。但是,在创建URI 处理函数时,您确实需要使用它们。
URI 处理程序
有时,简单的 SSI 机制不够灵活。在这种情况下,您可以直接注册一个函数,该函数将在响应 URI 请求时被调用。要添加处理程序,您需要调用服务器对象的 add_handler
函数。
例如,让我们先在另一个网页 page2.shtml 中添加一个按钮
<html>
<body>
The answer to the "Ultimate Question of Life, the Universe, and Everything",<br/>
is <!--#echo var="computer_answer" -->.
<br/>
<form method="post" action="author.cgi">
<input type="submit" value="Who Said That?" />
</form>
<body/>
<html/>
如果我们访问该页面,我们应该会看到这个
现在我们必须将处理程序函数添加到我们的程序中
int author (const char *uri, http_connection& client, void*)
{
//send response headers
client.respond (200); //200 = OK
//send response body
client.out() << "<html><body>Douglas Adams, Hitchhicker's Guide to The Galaxy</body></html>";
return 1;
}
int main (int argc, char **argv)
{
int answer = 42;
httpd server (8080);
server add_var ("computer_answer", "%d", &answer);
server add_handler ("author.cgi", auhtor);
server.start ();
while (!_kbhit())
;
server.terminate ();
}
主程序通过提供 URI 和要注册的函数来注册响应函数。响应 URI "https://:8080/author.cgi" 的 POST 请求,将调用 URI 处理程序函数,并接收对 http_connection
对象的引用。
它首先调用连接对象的 respond()
方法发送适当的响应代码和标头,然后流出响应的 HTML 文本。out()
方法返回用于与客户端通信的套接字流。我们的函数现在必须做的就是流出网页的内容。最终结果是页面如下图所示
处理程序函数可以返回 0
,表示它不想处理 URI 请求。在这种情况下,正常的请求处理将继续。
HTTP 交换的结构
这是对 HTTP 客户端连接过程中发生的各种操作的详细介绍。
首先,监听服务器会启动一个新线程作为 http_connection
对象。线程接收一个 Windows 套接字,该套接字立即转换为 sockstream
对象。
连接线程开始读取字符,直到遇到第一个 <CR><LF>
序列。如果请求格式正确,此时缓冲区应该包含
HTTP version space method uri <CR><LF>
在验证请求语法后,线程继续读取字符(请求头),直到遇到一个空行。这标志着请求头的结束和美好友谊请求体的开始。对于可能包含主体的 POST
或 PUT
请求,线程将读取“Content-Length
”标头指示的字符数。
现在是分派请求的时候了。首先,它会根据父 httpd
对象维护的处理程序函数表来检查 URI。如果找到,将调用处理程序,并且循环重复,直到连接套接字关闭。
如果 URI 没有注册处理程序函数,线程会检查是否能找到与请求 URI 匹配的文件。如果找到文件,下一步是检查文件扩展名是否在父对象维护的 MIME 类型列表中。默认情况下,该列表包含一些基本的 HTML、文本和图像类型。对于普通文件,文件内容会发送回客户端,从而完成请求周期。
对于 SHTML 文件,连接线程开始读取和解析文件内容,查找 SSI 'echo
' 构造。
<!--- echo var=varname --->
如果找到这样的构造,它会检查父对象维护的变量表,获取当前值,并根据其关联的 printf
格式对其进行格式化(浮点值也会被缩放)。一旦文件被发送,循环会再次重复,直到套接字关闭。
对象描述
为了更好地理解您可以使用这个小型 HTTP 服务器实现的功能,这里简要描述了 httpd
和 http_connection
对象提供的方法。详细描述可以在 doxygen 生成的文档中找到。
httpd 对象的方法
初始化 httpd
对象时,构造函数以服务器将要监听的端口号作为参数。端口之后无法更改。它还接受一个可选参数,代表最大并发连接数。如果保留为默认值,连接数将不受限制。
httpd (unsigned short port=HTTPD_DEFAULT_PORT, unsigned int maxconn=0);
端口号可以随时检索,并且可以在服务器启动之前更改。
unsigned short port ()
返回服务器正在监听的端口号。void port (unsigned short portnum)
更改服务器正在监听的端口号。这仅在服务器启动之前生效。
一组方法允许您控制在每次响应中传输的 HTTP 标头。
void add_ohdr (const char *hdr, const char *value)
添加一个带有给定值的新标头。void remove_ohdr (const char *hdr)
移除现有的响应标头。
可以使用 docroot
函数来设置或更改服务器发送的文件(docroot
)的来源。
void docroot (const char *path)
设置当前来源。const char* docroot () const
返回当前来源。
使用函数设置默认文档的名称。
void default_uri (const char *name)
设置默认文件名,当请求不包含文件名时发送。默认情况下,它是 index.html。
可以使用别名来更改服务器(连接客户端可见)的文件结构。该函数
void add_alias (const char* uri, const char* path);
为给定的 URI 段添加创建一个别名。它通过用 path
替换传入请求中的 uri
字符串来工作。例如,如果 docroot 设置为 c:\local_folder\,在调用后
add_alias ("doc", "documentation");
像 /doc/project1/filename.html 这样的 URI 将被映射到 c:\local_folder\documentation\project1\filename.html。
服务器维护一个 MIME 类型表,用于映射文件扩展名和 MIME 类型。反过来,MIME 类型用于填充 Content-Type
标头的值。可以使用以下方法修改此表:
void add_mime_type (const char *ext, const char *type, bool shtml=false)
添加额外的 MIME 类型。void delete_mime_type (const char *ext)
删除 MIME 类型。
如果 add_mime_type
函数的 shtml
参数为 true
,则具有该扩展名的文件将被解析为 SHTML 文件并扫描 SSI echo 构造。
我们之前已经看到过 add_var
函数,它添加了可以通过 SSI 构造访问的应用程序变量。为避免竞争条件,所有变量的访问都受到临界区的保护。
void acquire_varlock ()
进入保护所有变量的临界区。void release_varlock ()
离开临界区。bool try_varlock ()
尝试进入临界区并返回true
,如果成功。
作为一种访问控制方法,服务器提供了基本的用户身份验证,具有各种“域”,允许不同用户访问。
void add_realm (const char *realm, const char *uri)
添加一个域并指定域所覆盖的 URI。任何以uri
字符串开头的 URI 都被视为该域的一部分。bool add_user (const char *realm, const char *username, const char *pwd)
将用户添加到具有域访问权限的用户列表中。bool remove_user(const char *realm, const char *username)
从具有域访问权限的用户列表中删除用户。
http_connection 对象的方法
http_connection
对象具有访问请求不同部分的方法。
const char* get_uri ()
返回整个 URI(例如 https://:8080/author.cgi)。const char* get_method ()
返回请求的 HTTP 动词('GET
', 'POST
' 等)。const char* get_query ()
返回 URI 的查询部分('?
' 之后和 '#
' 之前的所有内容)。const char* get_body ()
返回查询的主体(对于POST
请求)。
请求标头也可以通过多种方法获得。
const char* get_ihdr (const char *hdr)
返回输入(接收)标头的内容。const char* get_ohdr (const char *hdr)
返回输出(发送)标头的内容。const char* get_all_ihdr ()
返回所有输入(接收)的标头。void add_ohdr (const char *hdr, const char *value)
添加一个新的输出(发送标头)或修改标头的值。
可以使用以下方法解析 URL 编码的查询。
bool has_qparam (const char* key)
如果查询包含指定参数,则返回true
。const std::string& get_qparam (const char* key)
返回 URL 编码的查询参数的值。
同样,可以使用以下方法解析 URL 编码的请求主体。
bool has_bparam (const char* key)
如果请求主体包含指定参数,则返回true
。const std::string& get_bparam (const char* key)
返回 URL 编码的请求主体参数的值。
结论
现在您拥有了一个小巧而灵活的 HTTP 服务器,您可以轻松地将其集成到您的应用程序中。本系列的最后一篇文章将介绍“JSON 桥接”,以及它如何使将网页集成到您的应用程序中更加容易。
为完整起见,以下是本系列先前文章的链接:
历史
- 2020年7月6日 初始版本