65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (29投票s)

2010年3月29日

CPOL

11分钟阅读

viewsIcon

111757

downloadIcon

1504

使用 C 编写浏览器源代码编辑器。

引言

我一直认为,能够通过 Web 浏览器在远程系统上导航和编辑源文件是一件很棒的事情。这并不是说如今不存在用于远程访问文件系统和文件的工具。甚至可能存在一些提供类似功能的 Web 浏览器工具。尽管如此,这似乎是一项有趣的挑战。

我对解决方案的主要要求是尽可能少地使用 CSS 和 JavaScript;我在这些领域的技能有限。我也希望该应用程序是用纯 C 编写的。最终,我找到了一个利用现有嵌入式 Web 服务器库 Snorkel 功能的解决方案。具体来说,我使用了 Snorkel SDK 来编写一个轻量级的 Web 服务器,该服务器通过 HTTP 接口公开远程文件系统。

Snorkel 开发者指南

Using the Code

browsereditor1.PNG

上图说明了项目的设计。该解决方案使用两个组件:一个服务器组件用于接受传入的请求,一个插件用于处理查看和编辑源文件的请求。

我们从服务器组件开始。

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 来启用基于浏览器的目录导航。由于我们没有向服务器提供索引文件的位置,它将在浏览器中显示目录列表,而不是产生默认的错误“未找到页面”。

browsereditor2.png

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。

browsereditor3.png

在将插件加载到内存后,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 函数中,我们测试是否存在包含 editdownload 的查询字符串。我们使用查询字符串来确定用户希望如何处理映射的文件类型。查询字符串是附加到 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    }

browsereditor4.png

edit_page 函数中,我们以 HTML 表单的形式发送 HTTP 回复,其中包含一个编辑字段中的关联文件内容以及一个不可编辑字段中的关联文件名。为了写入回复,我们结合使用了 snorkel_printfsnorkel_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) &lt; 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 响应。

browsereditor5.png

此项目(file_server.cfile_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
© . All rights reserved.