为C/C++应用程序添加Web界面的快速简便方法






4.81/5 (29投票s)
netstat命令的Web化,浏览器中的netstat

引言
最近,我们需要一种方法来监控远程系统的IPv4-TCP连接表,而无需定期登录系统并运行“netstat”命令。我提出的解决方案是一个轻量级的netstat类服务器,它提供实时连接监控,可从任何互联网浏览器查看。该项目是演示如何为仅具有文本UI的应用程序添加Web界面的一个很好的例子。
背景
我们使用Microsoft函数GetTcpTable
来收集连接数据。一个Microsoft示例,如下所示,演示了如何使用该函数,提供了一个很好的起点。
1 // Need to link with Iphlpapi.lib and Ws2_32.lib
2 #include <winsock2.h>
3 #include <ws2tcpip.h>
4 #include <iphlpapi.h>
5 #include <stdio.h>
6
7 #pragma comment(lib, "iphlpapi.lib")
8 #pragma comment(lib, "ws2_32.lib")
9
10 #define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x))
11 #define FREE(x) HeapFree(GetProcessHeap(), 0, (x))
12
13 /* Note: could also use malloc() and free() */
14
15 int main()
16 {
17
18 // Declare and initialize variables
19 PMIB_TCPTABLE pTcpTable;
20 DWORD dwSize = 0;
21 DWORD dwRetVal = 0;
22
23 char szLocalAddr[128];
24 char szRemoteAddr[128];
25
26 struct in_addr IpAddr;
27
28 int i;
29
30 pTcpTable = (MIB_TCPTABLE *) MALLOC(sizeof (MIB_TCPTABLE));
31 if (pTcpTable == NULL) {
32 printf("Error allocating memory\n");
33 return 1;
34 }
35
36 dwSize = sizeof (MIB_TCPTABLE);
37 // Make an initial call to GetTcpTable to
38 // get the necessary size into the dwSize variable
39 if ((dwRetVal = GetTcpTable(pTcpTable, &dwSize, TRUE)) ==
40 ERROR_INSUFFICIENT_BUFFER) {
41 FREE(pTcpTable);
42 pTcpTable = (MIB_TCPTABLE *) MALLOC(dwSize);
43 if (pTcpTable == NULL) {
44 printf("Error allocating memory\n");
45 return 1;
46 }
47 }
48 // Make a second call to GetTcpTable to get
49 // the actual data we require
50 if ((dwRetVal = GetTcpTable(pTcpTable, &dwSize, TRUE)) == NO_ERROR) {
51 printf("\tNumber of entries: %d\n", (int) pTcpTable->dwNumEntries);
52 for (i = 0; i < (int) pTcpTable->dwNumEntries; i++) {
53 IpAddr.S_un.S_addr = (u_long) pTcpTable->table[i].dwLocalAddr;
54 strcpy_s(szLocalAddr, sizeof (szLocalAddr), inet_ntoa(IpAddr));
55 IpAddr.S_un.S_addr = (u_long) pTcpTable->table[i].dwRemoteAddr;
56 strcpy_s(szRemoteAddr, sizeof (szRemoteAddr), inet_ntoa(IpAddr));
57
58 printf("\n\tTCP[%d] State: %ld - ", i,
59 pTcpTable->table[i].dwState);
60 switch (pTcpTable->table[i].dwState) {
61 case MIB_TCP_STATE_CLOSED:
62 printf("CLOSED\n");
63 break;
64 case MIB_TCP_STATE_LISTEN:
65 printf("LISTEN\n");
66 break;
67 case MIB_TCP_STATE_SYN_SENT:
68 printf("SYN-SENT\n");
69 break;
70 case MIB_TCP_STATE_SYN_RCVD:
71 printf("SYN-RECEIVED\n");
72 break;
73 case MIB_TCP_STATE_ESTAB:
74 printf("ESTABLISHED\n");
75 break;
76 case MIB_TCP_STATE_FIN_WAIT1:
77 printf("FIN-WAIT-1\n");
78 break;
79 case MIB_TCP_STATE_FIN_WAIT2:
80 printf("FIN-WAIT-2 \n");
81 break;
82 case MIB_TCP_STATE_CLOSE_WAIT:
83 printf("CLOSE-WAIT\n");
84 break;
85 case MIB_TCP_STATE_CLOSING:
86 printf("CLOSING\n");
87 break;
88 case MIB_TCP_STATE_LAST_ACK:
89 printf("LAST-ACK\n");
90 break;
91 case MIB_TCP_STATE_TIME_WAIT:
92 printf("TIME-WAIT\n");
93 break;
94 case MIB_TCP_STATE_DELETE_TCB:
95 printf("DELETE-TCB\n");
96 break;
97 default:
98 printf("UNKNOWN dwState value\n");
99 break;
100 }
101 printf("\tTCP[%d] Local Addr: %s\n", i, szLocalAddr);
102 printf("\tTCP[%d] Local Port: %d \n", i,
103 ntohs((u_short)pTcpTable->table[i].dwLocalPort));
104 printf("\tTCP[%d] Remote Addr: %s\n", i, szRemoteAddr);
105 printf("\tTCP[%d] Remote Port: %d\n", i,
106 ntohs((u_short)pTcpTable->table[i].dwRemotePort));
107 }
108 } else {
109 printf("\tGetTcpTable failed with %d\n", dwRetVal);
110 FREE(pTcpTable);
111 return 1;
112 }
113
114 return 0;
115 }
Web化
出于本次讨论的目的,我们将Web化定义为将应用程序的对外接口转换为Web启用接口的过程。为了避免客户端/服务器开发的复杂性,我们使用Snorkel SDK来Web化Microsoft示例。
在这个项目中,我们的Web界面是基于HTTP的,我们使用HTML表格来显示IPv4-TCP连接表。
.
.
.
39 #define MALLOC(x) snorkel_mem_alloc ((x))
40 #define FREE(x) snorkel_mem_free ((x))
.
.
.
61 /*
62 Source for MainPage based on MSDN example
63 msdn.microsoft.com/en-us/library/aa366026(VS.85).aspx
64 */
65 call_status_t
66 MainPage (snorkel_obj_t http,
67 snorkel_obj_t outstream
68 )
69 {
70
71 PMIB_TCPTABLE pTcpTable;
.
.
.
75 char szquery[2048];
76 char szLocalAddr[128];
77 char szRemoteAddr[128];
78 u_short localPort = 0;
79 u_short remotePort = 0;
80 u_short filter = 0;
81 int header = 0;
82
83 struct in_addr IpAddr;
84
85 int i;
86
87
88
89 pTcpTable =
90 (MIB_TCPTABLE *)
91 MALLOC (sizeof (MIB_TCPTABLE));
92 if (pTcpTable == NULL)
93 {
94 return
95 ERROR_STRING ("Error allocating memory");
96 }
97
98 dwSize = sizeof (MIB_TCPTABLE);
99
100 /* Make an initial call to GetTcpTable to
101 get the necessary size into the dwSize variable */
102 if ((dwRetVal =
103 GetTcpTable (pTcpTable, &dwSize,
104 TRUE)) ==
105 ERROR_INSUFFICIENT_BUFFER)
106 {
107 FREE (pTcpTable);
108 pTcpTable =
109 (MIB_TCPTABLE *) MALLOC (dwSize);
110 if (pTcpTable == NULL)
111 {
112 return
113 ERROR_STRING
114 ("Error allocating memory\n");
115 }
116 }
117
118 szquery[0] = 0;
119 snorkel_obj_get (http, snorkel_attrib_header,
120 "QUERY", szquery,
121 (int) sizeof (szquery));
122
123 if (strlen (szquery) > 0)
124 filter = atoi (szquery);
125
126 /* Make a second call to GetTcpTable to get
127 the actual data we require */
128 if ((dwRetVal =
129 GetTcpTable (pTcpTable, &dwSize,
130 TRUE)) == NO_ERROR)
131 {
132
133 if (filter)
134 snorkel_printf (outstream,
135 "<html><header><meta http-equiv=\"refresh\""
136 "content=\"%d\"></header><body><h2>"
137 "MONITORING PORT %d</h2><hr>\r\n",
138 g_autoRefreshInSeconds,
139 filter);
140 else
141 snorkel_printf (outstream,
142 "<html><header><meta http-equiv=\"refresh\""
143 "content=\"%d\"></header><body><h2>"
144 "MONITORING ALL PORTS</h2><hr>\r\n",
145 g_autoRefreshInSeconds);
146
147
148 for (i = 0;
149 i < (int) pTcpTable->dwNumEntries; i++)
150 {
151 IpAddr.S_un.S_addr =
152 (u_long) pTcpTable->table[i].
153 dwLocalAddr;
154 strcpy_s (szLocalAddr,
155 sizeof (szLocalAddr),
156 inet_ntoa (IpAddr));
157 IpAddr.S_un.S_addr =
158 (u_long) pTcpTable->table[i].
159 dwRemoteAddr;
160 strcpy_s (szRemoteAddr,
161 sizeof (szRemoteAddr),
162 inet_ntoa (IpAddr));
163
164 localPort = ntohs ((u_short) pTcpTable->
165 table[i].
166 dwLocalPort);
167 remotePort =
168 ntohs ((u_short) pTcpTable->table[i].
169 dwRemotePort);
170
171 if (filter &&
172 (filter != localPort
173 && filter != remotePort))
174 continue;
175
176 if (!header)
177 {
178 snorkel_printf (outstream,
179 "<div style=\"overflow:auto; width: 100%%; height: 80%%;"
180 "padding:0px; margin: 0px\">\r\n");
181 snorkel_printf (outstream,
182 "<table width=\"90%%\" cellpadding=\"0\">\r\n");
183 snorkel_printf (outstream,
184 "<tr><th align=\"left\">Protocol</th>"
185 "<th align=\"left\">State</th>"
186 "<th align=\"left\">Local Address</th>"
187 "<th align=\"left\">Local Port</th>"
188 "<th align=\"left\">Remote Address</th>"
189 "<th align=\"left\">Remote Port</th></tr>\r\n");
190 header = 1;
191 }
192
193 snorkel_printf (outstream,
194 "<tr><td align=\"left\">tcp</td><td align=\"left\">%02ld - ",
195 pTcpTable->table[i].
196 dwState);
197
198
199 switch (pTcpTable->table[i].dwState)
200 {
201 case MIB_TCP_STATE_CLOSED:
202 snorkel_printf (outstream,
203 "CLOSED</td>");
204 break;
205 case MIB_TCP_STATE_LISTEN:
206 snorkel_printf (outstream,
207 "LISTEN</td>");
208 break;
.
.
.
249 default:
250 snorkel_printf (outstream,
251 "UNKNOWN dwState value</td>");
252 break;
253 }
254
255
256 snorkel_printf (outstream,
257 "<td align=\"left\">%s</td>",
258 szLocalAddr);
259 snorkel_printf (outstream,
260 "<td align=\"left\">%d</td>",
261 localPort);
262 snorkel_printf (outstream,
263 "<td align=\"left\">%s</td>",
264 szRemoteAddr);
265 snorkel_printf (outstream,
266 "<td align=\"left\">%d</td></tr>\r\n",
267 remotePort);
268
269
270 }
271
272 }
273 else
274 {
275 char szError[80];
276 sprintf_s (szError, sizeof (szError),
277 "GetTcpTable failed with %d",
278 dwRetVal);
279 FREE (pTcpTable);
280 return ERROR_STRING (szError);
281 }
282
283
284 /* udp stuff */
285 pUdpTable =
286 (MIB_UDPTABLE *)
287 MALLOC (sizeof (MIB_UDPTABLE));
288
.
.
.
292 return
293 ERROR_STRING
294 ("Error allocating memory\n");
295 return HTTP_SUCCESS;
296 }
297
298 dwSize = sizeof (MIB_UDPTABLE);
299
300 /* Make an initial call to GetUdpTable to
301 get the necessary size into the dwSize variable */
302 if ((dwRetVal =
303 GetUdpTable (pUdpTable, &dwSize,
304 TRUE)) ==
305 ERROR_INSUFFICIENT_BUFFER)
306 {
307 FREE (pUdpTable);
308 pUdpTable =
309 (MIB_UDPTABLE *) MALLOC (dwSize);
310 if (pUdpTable == NULL)
311 {
312 if (!header)
313 return
314 ERROR_STRING
315 ("Error allocating memory\n");
316 else
317 return HTTP_SUCCESS;
318 }
319 }
320
321
322 /* Make a second call to GetTcpTable to get
323 the actual data we require */
324 if ((dwRetVal =
325 GetUdpTable (pUdpTable, &dwSize,
326 TRUE)) == NO_ERROR)
327 {
328
329
330
331 for (i = 0;
332 i < (int) pUdpTable->dwNumEntries; i++)
333 {
334 IpAddr.S_un.S_addr =
335 (u_long) pUdpTable->table[i].
336 dwLocalAddr;
337 strcpy_s (szLocalAddr,
338 sizeof (szLocalAddr),
339 inet_ntoa (IpAddr));
340
341 localPort = ntohs ((u_short) pUdpTable->
342 table[i].
343 dwLocalPort);
344
345 if (filter && (filter != localPort))
346 continue;
347
348 if (!header)
349 {
350 snorkel_printf (outstream,
351 "<div style=\"overflow:auto; width: 100%%; height: 80%%;"
352 "padding:0px; margin: 0px\">\r\n");
353 snorkel_printf (outstream,
354 "<table width=\"90%%\" cellpadding=\"0\">\r\n");
355 snorkel_printf (outstream,
356 "<tr><th align=\"left\">Protocol</th>"
357 "<th align=\"left\">State</th>"
358 "<th align=\"left\">Local Address</th>"
359 "<th align=\"left\">Local Port</th>"
360 "<th align=\"left\">Remote Address</th>"
361 "<th align=\"left\">Remote Port</th></tr>\r\n");
362 header = 1;
363 }
364
365 snorkel_printf (outstream,
366 "<tr><td align=\"left\">udp</td>"
367 "<td align=\"left\">** - ****");
368
369 snorkel_printf (outstream,
370 "<td align=\"left\">%s</td>",
371 szLocalAddr);
372 snorkel_printf (outstream,
373 "<td align=\"left\">%d</td>",
374 localPort);
375 snorkel_printf (outstream,
376 "<td align=\"left\">*****</td>");
377 snorkel_printf (outstream,
378 "<td align=\"left\">*****</td></tr>\r\n");
379
380
381 }
382 }
383 else
384 {
385 FREE (pUdpTable);
386 }
387
388 /*
389 *
390 * the following must be included to adhere to Snorkel License usage agreement
391 *
392 */
393 if (!header && filter)
394 {
395 snorkel_printf (outstream,
396 "<h3>There are no services listening on port %d</h3>",
397 filter);
398 snorkel_printf (outstream,
399 "<hr><address>Server powered by Snorkel</address>"
400 "</body></html>\r\n");
401 }
402 else
403 snorkel_printf (outstream,
404 "</table></div><hr><address>Server powered by Snorkel"
405 "</address></body></html>\r\n");
406
407 return HTTP_SUCCESS;
408 }
409
我们从前几行开始,用使用Snorkel API snorkel_mem_alloc
和snorkel_mem_free
的版本分别替换示例中的MALLOC
和FREE
。这些函数使用线程堆存储而不是进程堆存储,后者是HeapAlloc
使用的存储类型。Snorkel API调用提供无锁内存分配和释放,以及线程并发性,从而提供更好的整体性能。您可以在网上找到更多关于局部性和NUMA架构的信息。
我们把示例中的main
改成了名为MainPage
的子例程回调。回调接收指向HTTP头和连接对象(输出流)的指针作为其参数。我们不直接调用MainPage
;连接处理器调用该函数。连接处理器是Snorkel运行时分配用于处理传入HTTP请求的线程。不深入细节,Snorkel为每个HTTP请求分配一个单独的连接处理器。MainPage
是Snorkel术语中称为重载URI的配对的一部分。重载URI是与函数关联的URI。在main
中,我们将函数MainPage
与URI“/index.html”和“/”关联。当用户在浏览器中输入“http://server_address/”或“http://server_address/index.html”时,关联的连接处理器调用MainPage
来满足请求。
为了提供端口过滤功能,即根据提供的端口号过滤内容,我们利用HTTP查询字符串。查询字符串是出现在URI后面,由“?”引导的字符串。例如,URI“https://:8082?7188”将输出限制为与端口7188关联的连接,而URI“https://:8082”则列出IPv4-TCP表中所有活动的连接。Snorkel将查询字符串存储在HTTP变量QUERY
中。在第119-121行,我们使用函数snorkel_obj_get
来检索查询值(如果存在)。
在例程的其余部分,我们将对printf
函数的调用替换为对Snorkel API snorkel_printf
的调用。两个函数使用相同的格式分隔符;然而,与将输出定向到显示器的printf
函数不同,snorkel_printf
将输出定向到HTTP响应页面。
我们依赖宏ERROR_STRING
、HTP_ERROR
和HTTP_SUCCESS
来将回调的状态传回给处理程序线程。Snorkel API提供了两种返回回调错误的方法:宏ERROR_STRING
或宏HTTP_ERROR
。ERROR_STRING
宏允许开发人员指定自己的错误消息以便在客户端浏览器中显示,而HTTP_ERROR
宏则使用通用消息。如果没有错误,我们返回HTTP_SUCCESS
。
411 void
412 main (int argc, char *argv[])
413 {
414 int port = 8082;
415 int i = 0;
416 char *pszLog = 0;
417 char szExit[20];
418 snorkel_obj_t http = 0, logobj = 0;
419
420 fprintf (stderr,
421 "\nnetstat server -- a remote port monitoring server\n\n");
422
423 for (i = 1; i < argc; i++)
424 {
425 if (argv[i][0] == '-')
426 {
427 switch (argv[i][1])
428 {
429 case 'l':
430 if (i + 1 >= argc)
431 syntax (argv[0]);
432 pszLog = argv[i + 1];
433 i++;
434 break;
435 case 'r':
436 if (i + 1 >= argc)
437 syntax (argv[0]);
438 g_autoRefreshInSeconds =
439 atoi (argv[i + 1]);
440 i++;
441 break;
442 default:
443 syntax (argv[0]);
444 break;
445 }
446 }
447 else
448 {
449 port = atoi (argv[i]);
450 break;
451 }
452 }
453
454
455 if (pszLog)
456 {
457 logobj =
458 snorkel_obj_create (snorkel_obj_log,
459 pszLog);
460 if (!logobj)
461 {
462 perror ("could not create log file\n");
463 exit (1);
464 }
465 snorkel_debug (1);
466 }
467
468 if (snorkel_init () != SNORKEL_SUCCESS)
469 {
470 perror ("could not initialize snorkel\n");
471 exit (1);
472 }
473
474 http =
475 snorkel_obj_create (snorkel_obj_server, 2,
476 NULL);
477 if (!http)
478 {
479 perror
480 ("could not create server object!\n");
481 exit (1);
482 }
483
484 if (snorkel_obj_set (http, /* server object */
485 snorkel_attrib_listener, /* attribute */
486 port, /* port number */
487 0 /* SSL support */ )
488 != SNORKEL_SUCCESS)
489 {
490 fprintf (stderr,
491 "could not create listener\n");
492 snorkel_obj_destroy (http);
493 exit (1);
494 }
495
496 /*
497 *
498 * overload the URI http://index.html
499 *
500 */
501 if (snorkel_obj_set (http, /* server object */
502 snorkel_attrib_uri, /* attribute type */
503 GET, /* method */
504 "/index.html", /* uri */
505 encodingtype_text, /* encoding */
506 MainPage) !=
507 SNORKEL_SUCCESS)
508 {
509 perror ("could not overload index.html");
510 snorkel_obj_destroy (http);
511 exit (1);
512 }
513
514 if (snorkel_obj_set
515 (http, snorkel_attrib_ipvers, IPVERS_IPV4,
516 SOCK_SET) != SNORKEL_SUCCESS)
517 {
518 fprintf (stderr,
519 "error could not set ip version\n");
520 exit (1);
521 }
522
523 /*
524 *
525 * start the server
526 *
527 */
528 fprintf (stderr,
529 "\n\n[HTTP] starting embedded server\n");
530 if (snorkel_obj_start (http) != SNORKEL_SUCCESS)
531 {
532 perror ("could not start server\n");
533 snorkel_obj_destroy (http);
534 exit (1);
535 }
536
537 /*
538 *
539 * do something while server runs
540 * as a separate thread
541 *
542 */
543 fprintf (stderr, "\n[HTTP] started.\n\n"
544 "--hit enter to terminate--\n");
545 fgets (szExit, sizeof (szExit), stdin);
546
547 fprintf (stderr, "[HTTP] bye\n");
548
549 /*
550 *
551 * graceful clean up
552 *
553 */
554 snorkel_obj_destroy (http);
555 exit (0);
556 }
我们的main
源自Snorkel样板代码(参见Snorkel开发者指南)。在第468 - 482行,我们初始化Snorkel API并创建服务器对象,指定线程池大小为二(snorkel_obj_create
的第二个参数)。
接着,在484-494行,我们创建一个监听器,将其绑定到用户定义的端口或默认端口号8082。在501-512行,我们将MainPage
回调映射到“index.html”,并在530行启动服务器。由于嵌入式服务器在其自己的线程上运行,我们使用fgets
提示用户输入以防止程序立即退出。
运行服务器
您可以从命令行运行服务器,或者在资源管理器中双击它。如果未指定端口号,则使用端口8082。HTML页面的默认刷新率为五秒。您可以通过在命令行上指定-r选项来更改刷新率。
语法
netstatsrv [port] [-h] [-r refresh_rate_in_seconds]
要在本地查看 IPv4-TCP 表,请启动服务器并打开网页浏览器。在浏览器中,输入 URI "https:///8082"。要按端口号过滤,请输入 URI,后跟一个 '?' 和端口号。例如,"https:///8082?8080" 将只显示与端口 8080 相关的表条目。
最终评论
此项目的源文件,netstatsrv.c,位于附加包的“c”目录中。
我正在向CodeProject成员免费提供此项目中使用的Snorkel SDK的有限版本。提供的SDK包含适用于SunOS、Linux和Microsoft Windows平台的二进制文件。它还包括此示例以及其他示例和关于如何使用Snorkel SDK的完整文档。尽管Snorkel支持SSL,但我由于美国贸易法而删除了SSL版本的运行时库。开发者指南位于附加包的doc文件夹中,您可能希望在试用此项目之前阅读该指南。如果您对Snorkel SDK有任何疑问或额外兴趣,请随时通过wcapers64@gmail.com与我联系。
相关文章
历史
- 6.18.2010
对语法函数进行了小修补,并将Snorkel更新到最新版本1.0。Snorkel 1.0 包括
- 性能优化增强
- 支持keep-alive
- 支持零拷贝(sendfile)
- 暴露了线程管理器过载功能——现在用户可以通过过载管理器来创建比核心更多的处理器
- 次要 bug 修复
- 添加了内置页面以显示有关嵌入式服务器的信息。要访问该页面,请在根URL后附加/about或/snorkel。
- 6.21.2010
我通常不会这么快发布库的更新,但这次我无法抵挡诱惑。由于文件系统信息缓存方式的改变,版本1.0.1在上周末在Windows端获得了显著的性能提升。这些改变通过减少I/O阻塞,显著提高了每秒请求数和平均传输速率。当使用Apache的ab测试与其他Web服务器进行基准测试时,性能差异足以证明这次早期更新。
- 6.21.2010
发现并纠正了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目录中运行时库文件之间的文件不匹配问题
- 我添加了在非HTTP流中切换
snorkel_printf
传输大小字段的功能。要启用或禁用该功能(非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 修复
- 7.20.2010
- 更新了源代码
- 2011年1月3日 – Snorkel 2.0.0.1 更新(仍然免费)
- 全新改进的API
- 更快的性能
- 新增API:Aqua (服务SDK) 和 Sailfish (C/C++ 应用服务器)
- 平台支持:MAC OSX、SunOS、Debian Linux、Windows
- 新下载网站:http://snorkelembedded.webs.com