使用 C 和嵌入式 Web 服务器提供基于浏览器的源编辑器






4.81/5 (29投票s)
使用 C 编写浏览器源代码编辑器。
引言
我一直认为,能够通过 Web 浏览器在远程系统上导航和编辑源文件是一件很棒的事情。这并不是说如今不存在用于远程访问文件系统和文件的工具。甚至可能存在一些提供类似功能的 Web 浏览器工具。尽管如此,这似乎是一项有趣的挑战。
我对解决方案的主要要求是尽可能少地使用 CSS 和 JavaScript;我在这些领域的技能有限。我也希望该应用程序是用纯 C 编写的。最终,我找到了一个利用现有嵌入式 Web 服务器库 Snorkel 功能的解决方案。具体来说,我使用了 Snorkel SDK 来编写一个轻量级的 Web 服务器,该服务器通过 HTTP 接口公开远程文件系统。
Snorkel 开发者指南
Using the Code
上图说明了项目的设计。该解决方案使用两个组件:一个服务器组件用于接受传入的请求,一个插件用于处理查看和编辑源文件的请求。
我们从服务器组件开始。
17 void
18 main (int argc, char *argv[])
19 {
20 int i = 1;
21 int http_port = 0;
22 int https_port = 0;
23 char *pszIndex = 0;
.
.
27 snorkel_obj_t http = 0;
.
.
.
114 /*
115 *
116 * always call first to initialize
117 * snorkel API
118 *
119 */
120 if (snorkel_init () != SNORKEL_SUCCESS)
121 {
122 perror ("could not initialize snorkel\n");
123 exit (1);
124 }
125
126 /*
127 *
128 * create a server object
129 *
130 */
131 http = snorkel_obj_create (snorkel_obj_server,
132 2, pszIndex);
133 if (!http)
134 {
135 perror ("could not create server object!\n");
136 exit (1);
137 }
138
.
.
.
146 if(snorkel_obj_set(http,snorkel_attrib_bubbles,NULL)
147 != SNORKEL_SUCCESS)
148 {
149 fprintf (stderr,
150 "error encountered setting bubbles!");
151 exit (1);
152 }
153
154 snorkel_obj_set (http, snorkel_attrib_show_dir, 1);
156 /*
157 * add listeners
158 *
159 */
160 if (http_port) /* http port */
161 {
162 if (snorkel_obj_set (http,
163 snorkel_attrib_listener,
164 http_port, 0) != SNORKEL_SUCCESS)
165 {
166 fprintf (stderr,
167 "error could not add listener for port %d\n",
168 http_port);
169 exit (1);
170 }
171 }
.
.
.
230 fprintf (stderr,
231 "\n\n[HTTPS] starting embedded server...\n");
232
233 /*
234 *
235 * start server, no more editing or creation
236 * of objects allowed after this point
237 *
238 */
239 if (snorkel_obj_start (http) != SNORKEL_SUCCESS)
240 {
241 perror ("could not start server\n");
242 snorkel_obj_destroy (http);
243 if (logobj)
244 snorkel_obj_destroy (logobj);
245 exit (1);
246 }
.
.
.
256 fprintf (stderr,
257 "\n[HTTP] started.\n\n--hit enter to terminate--\n");
258 fgets (szExit, sizeof (szExit), stdin);
.
.
.
279 }
在 main
函数中,我们通过调用 snorkel_init
来初始化 Snorkel API。在调用任何 Snorkel API 之前,必须调用 snorkel_init
。接下来,我们通过调用 snorkel_obj_create
函数创建一个 HTTP 对象。snorkel_obj_create
函数为所有 Snorkel 对象提供对象创建功能。与 C++ 和 Java 不同,Snorkel 对象不是从类派生的。它们更像是 Windows 句柄,是通过 void 指针封装的、相关数据被隐藏起来的对象。该函数接受三个参数:对象类型 (snorkel_obj_server
)、要创建的线程数(两个)以及一个指向以 null 结尾的字符串的指针,该字符串包含我们要导出的目录的完全限定路径。
在第 146 行,我们指定了插件目录。通过为第三个参数传递 NULL
指针,我们指示 API 使用默认的插件目录:(当前目录)/bubbles。
插件,在 Snorkel API 中称为 bubbles,是自包含的运行时组件,它们导出具有 URI 映射的函数来处理 HTTP 请求。在此示例中,用于处理与文件编辑和查看相关的 HTTP-GET 和 HTTP-POST 的服务器逻辑驻留在插件 (bubble) 中。将功能放入 bubble 中可以被其他基于 Snorkel 的嵌入式服务器重用。
与任何 Web 服务器一样,需要一个索引文件来解析浏览器发送的初始 HTTP 请求。默认情况下,如果索引目录(在第 131 行提供的目录)不包含索引文件 (index.html),则会发生错误。Snorkel API 提供了通过浏览器进行目录导航的功能,但默认情况下它是禁用的。在第 154 行,我们通过调用 snorkel_obj_set
并设置服务器属性 snorkel_attrib_show_dir
来启用基于浏览器的目录导航。由于我们没有向服务器提供索引文件的位置,它将在浏览器中显示目录列表,而不是产生默认的错误“未找到页面”。
Snorkel 运行时提供的目录列表允许从根目录(在第 131 行指定的目录)及其所有子目录进行文件导航。目录列表包含文件和子目录的链接。由方括号分隔的名称表示目录链接。列表标题(名称、大小和修改日期)也是链接;选择它们可以根据标题类型对目录列表进行排序。例如,选择“名称”标题会按文件名对列表进行排序。
在我们可以发出启动服务器的指令之前,我们需要定义一个监听器。Snorkel 监听器是处理用户定义端口上的 TCP/IP 协议请求的对象。Snorkel 服务器对象可以支持多个监听器。监听器可以监听并处理来自 HTTP、HTTPS 或专有协议客户端的请求。在第 160-170 行,我们定义了一个分配给端口 http_port
的监听器,该值是从命令行选项获取的。
最后,在第 239 行,我们调用 snorkel_obj_start
来启动嵌入式 Web 服务器。snorkel_obj_start
API 会将服务器作为一个单独的线程启动,并将控制权返回给调用例程。为了防止应用程序退出,我们使用 fgets
命令等待用户的输入以确定何时退出。程序的完整列表可以在附件包的“c”目录中找到,文件名为 file_server.c。
在定义了服务器组件之后,我们接下来定义插件——我们的 Snorkel bubble。
在将插件加载到内存后,Snorkel 运行时会检查 bubble_main
函数。如果函数存在,运行时会调用该函数,并将关联的服务器对象(我们在服务器的 main 函数中创建的对象)传递给它。在 bubble_main
中,我们调用 API snorkel_obj_set
并使用属性 snorkel_attrib_mime
来将每种文件类型与内容类型和回调函数 (view_uri
) 相关联。我们还将任何以“update.html”结尾的 URI 与 save_file
函数相关联。当嵌入式服务器遇到对关联文件类型(即与映射函数关联的文件类型)的请求时,它会调用该函数,传递 HTTP 请求对象、连接对象以及与所请求 URI 关联的 URL。在此解决方案中,运行时会为包含匹配映射文件类型的任何文件调用 view_uri
函数。
742 byte_t SNORKEL_EXPORT
743 bubble_main (snorkel_obj_t server)
744 {
745 int i = 0;
746
747 snorkel_obj_set (server, snorkel_attrib_mime, "c",
748 "text/html",
749 encodingtype_text,
750 view_uri);
751 snorkel_obj_set (server, snorkel_attrib_mime, "cpp",
752 "text/html",
753 encodingtype_text,
754 view_uri);
755 snorkel_obj_set (server, snorkel_attrib_mime, "h";,
756 "text/html",
757 encodingtype_text,
758 view_uri);
.
.
.
815 snorkel_obj_set (server,
816 snorkel_attrib_uri, POST,
817 "*update.html", encodingtype_text,
818 save_file);
.
.
.
831 return 1;
832 }
在 view_uri
函数中,我们测试是否存在包含 edit 或 download 的查询字符串。我们使用查询字符串来确定用户希望如何处理映射的文件类型。查询字符串是附加到 URI 后并以“?”开头的任何字符串。例如,如果 HTTP 请求包含 URI“https:///c/source.c?edit”,则查询字符串是“edit”。Snorkel 运行时将查询字符串视为头部元素,并将其存储在 HTTP 头部变量“QUERY
”中。在第 643-645 行,我们使用 API 函数 snorkel_obj_get
和属性 snorkel_attrib_header
从 HTTP 请求头部检索查询字符串值。
593 call_status_t SNORKEL_EXPORT
594 view_uri (snorkel_obj_t http,
595 snorkel_obj_t connection, char *pszurl)
596 {
.
.
.
642
643 if (snorkel_obj_get
644 (http, snorkel_attrib_header, "QUERY", szquery,
645 (int) sizeof (szquery)) == SNORKEL_SUCCESS)
646 {
647 if (strcmp (szquery, "edit") == 0)
648 return edit_page (http, connection, pszurl);
649 else if (strcmp (szquery, "download") == 0)
650 {
651 if (snorkel_file_stream
652 (connection, pszurl, 0,
653 SNORKEL_BINARY) == SNORKEL_ERROR)
654 return HTTP_ERROR;
655 return HTTP_SUCCESS;
656 }
657 }
.
.
.
738 return SNORKEL_SUCCESS;
739 }
如果查询字符串等于“edit”,我们将 HTTP 请求对象、连接对象和 URL 传递给 edit_page
函数。
407 call_status_t
408 edit_page (snorkel_obj_t http,
409 snorkel_obj_t connection, char *pszurl)
410 {
.
.
.
443 if (stat (pszurl, &stf) != 0)
444 return
445 ERROR_STRING ("resource could not be located\r\n");
446
447 strftime(sztime,sizeof(sztime),"%m/%d/%y %I:%M:%S %p",
448 localtime (&stf.st_mtime));
449
450 snorkel_obj_get (http, snorkel_attrib_uri, szuri,
451 (int) sizeof (szuri));
452 pszfile = strrchr (szuri, '/');
453 pszfile++;
454
455 if (snorkel_printf(connection,header,szuri, sztime) ==
456 SNORKEL_ERROR)
457 return HTTP_ERROR;
458
459
460 if (snorkel_file_stream
461 (connection, pszurl, 0,
462 SNORKEL_UUENCODE) == SNORKEL_ERROR)
463 return HTTP_ERROR;
464
465 if (snorkel_printf (connection, footer, pszfile) ==
466 SNORKEL_ERROR)
467 return HTTP_ERROR;
468
469 return HTTP_SUCCESS;
470 }
在 edit_page
函数中,我们以 HTML 表单的形式发送 HTTP 回复,其中包含一个编辑字段中的关联文件内容以及一个不可编辑字段中的关联文件名。为了写入回复,我们结合使用了 snorkel_printf
和 snorkel_file_stream
函数。snorkel_printf
函数的工作方式类似于 C 的 fprintf
函数,使用连接对象作为打开的流。我们使用该函数来编写 HTML 的头部和尾部。snorkel_file_stream
函数将 URL 引用的源文件流式传输到客户端,并进行 base64 编码。
我们在 bubble_main
的第 815-818 行将更新按钮“Submit”与 save_file
函数相关联。如果用户从 edit_page
表单中选择更新按钮,服务器会调用导出的 save_file
函数来将文件修改保存到关联的 URL。
472 call_status_t SNORKEL_EXPORT
473 save_file (snorkel_obj_t http, snorkel_obj_t con)
474 {
.
.
.
490 if (snorkel_obj_get
491 (http, snorkel_attrib_local_url_path, szurl_file,
492 sizeof (szurl_file)) != SNORKEL_SUCCESS)
493 return HTTP_ERROR;
494
495
496 if (snorkel_obj_get
497 (http, snorkel_attrib_uri_path, szuri_file,
498 sizeof (szuri_file)) != SNORKEL_SUCCESS)
499 return HTTP_ERROR;
500
501 szuri_path[0] = 0;
502 strcat (szuri_path, szuri_file);
503
504 if (snorkel_obj_get
505 (http, snorkel_attrib_post, "filename", szfile,
506 sizeof (szfile)) == SNORKEL_ERROR)
507 return HTTP_ERROR;
508
509
510 if (snorkel_obj_get
511 (http, snorkel_attrib_post_ref, "contents", &psz,
512 &cbpsz) == SNORKEL_ERROR)
513 return HTTP_ERROR;
514
515 #if defined(WIN32) || defined(WIN64)
516 strcat (szurl_file, "\\");
517 #else
518 strcat (szurl_file, "/");
519 #endif
520 if (szuri_file[strlen (szuri_file) - 1] != '/')
521 strcat (szuri_file, "/");
522
523 strcat (szurl_file, szfile);
524 strcat (szuri_file, szfile);
525
526 fd = fopen (szurl_file, "wb");
527 if (!fd)
528 {
529 return
530 ERROR_STRING ("The file could not be saved!\r\n");
531 }
532
533 ptr = psz;
534 while (ptr)
535 {
536 char *temp = ptr;
537 ptr = strstr (ptr, "\r\n");
538 if (ptr && ptr != temp)
539 {
540 *ptr = 0;
541 if ( fprintf(fd, "%s\n";, temp) < 0)
542 {
543 fclose (fd);
544 ERROR_STRING
545 ("I/O error encountered updating file.\r\n");
546 }
547 ptr += 2;
548 }
549 else if (ptr && ptr == temp)
550 {
551 if (fprintf (fd, "\n") < 0 )
552 {
553 fclose (fd);
554 return
555 ERROR_STRING
556 ("I/O error encountered updating file.\r\n");
557 }
558 ptr += 2;
559 }
560 else if (temp && strlen (temp) > 0)
561 {
562 if (fprintf (fd, "%s\n", temp) < 0)
563 {
564 fclose (fd);
565 return
566 ERROR_STRING
567 ("I/O error encountered updating file.\r\n");
568 }
569 }
570 i++;
571 }
572
573 fclose (fd);
574 snorkel_printf (con, pszsuccess,
575 szuri_file, i);
576 return HTTP_SUCCESS;
577
578 }
为了写入文件,save_file
函数会获取存储在 edit_page
表单的不可编辑字段中的文件名,并使用 snorkel_obj_get
API 及其 snorkel_attrib_post
和 snorkel_attrib_post_ref
属性来获取修改后的文件内容。所有发布表单的数据都存储在一个可以通过这些属性访问的表中。snorkel_attrib_post
属性通过将变量值复制到提供的缓冲区来获取 HTTP-POST 变量值,而 snorkel_attrib_post_ref
则返回指向存储在 HTTP-POST 变量中的数据的指针。我们使用后者来避免分配足够大的缓冲区来存储修改后文件内容的需求。为了保存文件,save_file
函数会打开在 filename
变量中提供的文件,并写入 contents
变量中包含的内容。
如果 view_uri
获取的查询字符串值为“download”,view_uri
会逐行将 URI 引用的文件流式传输回请求客户端,确保正确的行终止。这似乎效率不高,但由于 Snorkel 运行时,事实并非如此。Snorkel 运行时会自动缓冲小型 I/O 发送,以减少对 Socket 层调用的次数。
最后,如果 URI 不包含查询字符串,view_uri
会检查扩展名并根据文件类型格式化 HTML 响应。
此项目(file_server.c 和 file_server_plugin.c)的源文件位于附件包的“c”目录中。
运行服务器
要运行服务器,请解压附件包的内容。
在 Linux 上
- 将
LD_LIBRARY_PATH
追加到包含 deployment_directory/lib/Linux 目录。 - 切换目录到 deployment_dir/bin/Linux。
- 输入命令“fsrv -p 8080 -i deployment_directory”。
在 Windows 上
- 切换目录到 deployment_directory\bin\wintel。
- 输入命令“fsrv -p 8080 -i deployment_directory”。
启动浏览器,并将其指向 http://server_hostname:8080。
结论
我将此项目使用的 Snorkel 运行时库的有限版本免费提供给 CodeProject 会员。提供的 SDK 包括 Linux、SunOS 和 Windows 平台的二进制文件。它还包含示例和开发者指南。尽管 Snorkel 支持 SSL,但由于美国贸易法,我已删除运行时库的 SSL 版本。开发者指南位于附件包的 doc 文件夹中,您可能想在玩这个项目之前阅读该指南。如果您有任何关于所提供 SDK 的问题或进一步的兴趣,请随时通过 wcapers64@gmail.com 与我联系。
历史
- 2010 年 3 月 30 日 - 进行了一些语法更正,并添加了 Snorkel 开发者指南的直接链接。
- 2010 年 4 月 8 日 - 添加了 Windows 2K 版本的 Snorkel 运行时。注意:Windows 2000 运行时不利用线程亲和性,因为操作系统实现中不存在支持的 API。
- 2010 年 4 月 9 日 - 根据要求,对 Windows 2K 版本进行了更正。
- 2010 年 4 月 9 日 - 对开发者指南的第 24 页进行了一些更正。
- 2010 年 4 月 14 日 - 更新了 Snorkel 运行时。此更新修复了当连接被突然断开时可能发生的
CLOSE_WAIT
问题。还公开了监听器的 linger 和 timeout 属性,请参阅修改后的 file_server.c 版本和/或开发者指南了解如何使用。更新了包中的开发者指南。 - 2010 年 4 月 22 日 - 由于上次更新的 SDK 构建过程中出现错误,Windows 2K (snorkel32_2k.dll) 版本的运行时成为了事实上的运行时版本。这是因为 Wintel 平台的 Windows 2K 和非 Windows 2K 版本的链接库共享相同的库名 snorkel32.lib。在此 SDK 更新中,Windows 2K 版本正确地使用了链接库名 snorkel32_2k.lib。注意,通过更改其名称,这仍然允许将 snorkel32_2k.dll 替换 snorkel32.dll。
- 2010 年 5 月 18 日 - 纠正了关于二进制数据流和 MIME-URL 回调的一个问题。该缺陷不会影响流式传输非二进制内容(如 HTML、XML、文本等)的回调。向 Snorkel 运行时添加了网络性能增强功能,以提高服务器在高负载条件下的响应速度。
注意:2010 年 5 月 18 日的更新包含对文件服务器 bubble 的扩展,以包含 GNU Plot 文件。此修改促进了浏览器中的数据绘图。请暂缓使用新功能,并等待配套文章。我不会在此页面上回答有关新功能的问题。
- 2010 年 6 月 16 日 - 将 Snorkel 运行时更新到 1.0,并添加了其他命令行选项以访问新功能。
Snorkel 1.0 包括
- 支持 keep-alive。
- 支持零拷贝(sendfile)。
- 暴露了线程管理器过载——现在用户可以创建比核心数更多的处理器。
- 小的 Bug 修复。
- 添加了内置页面以显示有关嵌入式服务器的信息。要访问页面,请在根 URI 后附加 /about 或 /snorkel。
- 2010 年 6 月 21 日
我通常不会这么快地发布库更新,但这次我无法抗拒诱惑。由于在文件系统信息缓存方式上的更改,Windows 平台上的 Build 1.0.1 在周末获得了显著的性能提升。这些更改通过减少 I/O 阻塞,显着提高了每秒请求数和平均传输速率。当使用 Apache 的 ab 测试与其他 Web 服务器进行基准测试时,性能差异足以值得进行此早期更新。
- 2010 年 6 月 21 日
在 UNIX 系统上发现了并纠正了缺陷。
- 2010 年 6 月 25 日 -- Snorkel 1.0.2 更新
- 修复了线程堆分配器在分配推送到线程堆之外时的一个小缺陷。
- 添加了线程堆完整性检查和自动修复功能。
- 完成了测试,并启用了 1.0.1 版本中引入但未启用的其他性能增强功能。
Snorkel 采纳者须知:Snorkel 每天都会经过严格测试,并且通常在野外发现之前就会检测到并修复错误。在 Snorkel 更新的官方网站出现之前,我会将此文章和其他使用该库的文章中捆绑的版本与最新的 API 版本保持同步。
- 2010 年 7 月 8 日 -- Snorkel 1.0.4 更新
- 纠正了 Wintel 平台 bin 目录和 lib 目录中的运行时库文件之间的文件不匹配问题。
- 我添加了切换
snorkel_printf
为非 HTTP 流传输的“大小”字段开/关的功能。要启用或禁用该功能(默认情况下对非 HTTP 流是启用的),请使用以下命令snorkel_obj_set (snorkel_obj_t non_http_stream, snorkel_attrib_cbprintf, int state (enabled=1,disabled=0))
。 - 添加了电子邮件函数。语法:
snorkel_smtp_message (char *smtp_server, int port, char *from, char *to, char *format_string, arg1, arg2,...argN)
。注意:该函数的工作方式与printf
类似。 - 公开了更多属性。
- 小的 Bug 修复。
- 2010 年 7 月 21 日 -- Snorkel 1.0.5 更新
- 2011 年 1 月 6 日 – Snorkel 2.0.0.1 更新(仍然免费)
- 新的改进的 API
- 更快的性能
- 添加了新的 API Aqua(服务 SDK)和 Sailfish(C/C++ 应用程序服务器)
- 平台支持 MAC OSX、SunOS、Debian Linux、Windows
- 新下载网站:http://snorkelembedded.webs.com