Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

The Quick and Easy Way to Add Web Interfaces to C/C++ Applications

4.81/5 (32 votes)
6 Jan 2011CPOL7 min read 131.1K   1.6K  
The webonization of the netstat command, netstat in a browser
browser.jpg

Introduction

Recently, we needed a way to monitor a remote system's IPv4-TCP connection table without having to periodically log on to the system and run the "netstat" command. The solution that I came up with, a lightweight netstat like server, provides real-time connection monitoring viewable from any internet browser. The project is a nice example of how to add web-interfaces to applications that only have text UIs.

Background

We used the Microsoft function GetTcpTable for gathering connection data. A Microsoft example, which illustrated how to use the function, listed below, provided a nice starting point.

C++
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    }

Webonization

For the purpose of this discussion, we define webonization as the process of converting an application's outward facing interfaces to web enabled interfaces. To avoid the complexities of client/server development, we used the Snorkel SDK to webonize the Microsoft example.

In this project, our web-facing interface is HTTP based, and we use an HTML table to display IPv4-TCP connection tables.

C++
.
.   
.
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

We begin with the first couple of lines where we replace the example's version of MALLOC and FREE with versions that use the Snorkel APIs snorkel_mem_alloc and snorkel_mem_free, respectively. These functions use thread heap storage instead of process heap storage, the storage type used by HeapAlloc. The Snorkel API calls provide lock free memory allocation and de-allocation, as well as thread-currency, providing better overall performance. You can find more on locality and NUMA architectures on the web.

We changed main in the example to a subroutine-callback called MainPage. The callback receives as its arguments a pointer to an HTTP-header and a connection object (output stream). We do not call MainPage directly; a connection-handler calls the function. Connection-handlers are threads assigned by the Snorkel runtime to process incoming HTTP requests. Without going into too much detail, Snorkel assigns a separate connection-handler for each HTTP request. MainPage is part of a pairing known as an overloaded URI in Snorkel lingo. An overloaded URI is a URI associated with a function. In main, we associated the function MainPage with the URIs "/index.html" and "/". When a user enters either "http://server_addres/" or "http://server_addres /index.html" into a browser, the associated connection-handler calls MainPage to fulfill the request.

To provide port filtering functionality, that is, filtering content based on a provided port number, we enlist the HTTP query string. Query strings are strings that appear after a URI proceeded by a '?'. For example, the URI "http://localhost:8082?7188" limits output to connections associated with port 7188, and the URI "http://localhost:8082" lists all active connections within the IPv4-TCP table. Snorkel stores query-strings in the HTTP variable QUERY. On lines 119-121, we use the function snorkel_obj_get to retrieve the query value, if it exists.

In the rest of the routine, we replace calls to the printf function with calls to the Snorkel API snorkel_printf. Both functions use the same format delimiters; however, unlike the printf function, which directs output to the display, snorkel_printf directs output to the HTTP response page.

We rely on the macros ERROR_STRING, HTP_ERROR, and HTTP_SUCCESS to pass back the callback's status to the handler thread. The Snorkel API provides two methods for returning callback errors, the macro ERROR_STRING or the macro HTTP_ERROR. The ERROR_STRING macro allows developers to specify their own error message for display in the client's browser, and the HTTP_ERROR macro uses a generic message. If there are no errors, we return HTTP_SUCCESS.

C++
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    }

We derive our main from Snorkel boilerplate code (see Snorkel Developer Guide). On lines 468 - 482, we initialize the Snorkel API and create our server object, specifying a thread pool size of two (the second argument of snorkel_obj_create).

Next, on lines 484-494, we create a listener, binding it to a user defined port or the default port number 8082. On lines 501-512, we map our MainPage callback to "index.html", and on line 530, we start the server. Since the embedded server runs on its own thread, we prompt the user for input using fgets to prevent the program from immediately exiting.

Running the Server

You can run the server from the command line or by double clicking it from within the explorer. If a port number is not specified, port 8082 is used. The default refresh rate for an HTML page is five seconds. You can change the refresh rate by specifying the -r option on the command line.

Syntax:

netstatsrv [port] [-h] [-r refresh_rate_in_seconds]

To view the IPv4-TCP table locally, launch the server and start up a web browser. From within the browser, enter the URI "http://localhost/8082". To filter on a port number, enter the URI followed by a '?' and the port number. For example, "http://localhost/8082?8080" will only display table entries related to port 8080.

Final Comments

The source file for this project, netstatsrv.c, is located in the "c" directory of the attached bundle.

I am providing a limited version of the Snorkel SDK used in this project to CodeProject members free of charge. The provided SDK includes binaries for SunOS, Linux, and Microsoft Windows platforms. It also includes this example, along with other examples and full documentation on how to use the Snorkel SDK. Even though Snorkel supports SSL, I have removed SSL versions of the runtime libraries due to US trade laws. The developers guide is located in the doc folder of the attached bundle, you may want to read the guide prior to playing around with this project. If you have any questions and or additional interests in the Snorkel SDK, feel free to contact me at wcapers64@gmail.com.

Related Article(s)

History

  • 6.18.2010

    Made a minor fix to the syntax function and updated Snorkel to the latest version 1.0. Snorkel 1.0 includes:

    • Performance optimization enhancements
    • Support for keep-alive
    • Support for zero-copy (sendfile)
    • Exposed thread governor overload -- now users can create more handlers than there are cores by overloading the governor
    • Minor bug fixes
    • Added built in page to display information about embedded server. To access page, append root URL with /about or /snorkel.
  • 6.21.2010

    I normally don't post updates to my libraries this fast, but this time I could not resist the temptation. Build 1.0.1 received a significant performance boost over the weekend, on the Windows side, thanks to changes in how file system information is cached. The changes significantly improved requests per second and average transfer rate by reducing I/O blocking. When bench marked against other web-servers, using Apache's ab test, the performance differences were significant enough to merit this early update.

  • 6.21.2010

    Noticed and corrected defect on UNIXes

  • 6.25.2010 -- Snorkel 1.0.2 update
    • Fixed minor thread-heap allocator defect for allocations pushed outside of thread-heap
    • Added thread-heap integrity check and auto-repair features
    • Completed testing and enabled additional performance enhancements that were introduced but disabled for 1.0.1

    Note to adopters of Snorkel: Snorkel is heavily tested on a daily basis, and bugs are often detected and fixed before they are detected in the field. Until there is an official site for Snorkel updates, I will keep the version bundled with this article and other articles that use the library up to date with the latest version of the API.

  • 7.8.2010 -- Snorkel 1.0.4 update
    • Corrected file mismatch between runtime library files in the bin directory and the lib directory for wintel
    • I added ability to toggle off/on the size field transmitted by snorkel_printf for non-HTTP streams. To enable or disable the feature, which is on by default for non-HTTP streams, use the following command:
      C++
      snorkel_obj_set (snorkel_obj_t non_http_stream, 
      	snorkel_attrib_cbprintf, int state (enabled=1,disabled=0))
    • Added email function. Syntax:
      C++
      snorkel_smtp_message (char *smtp_server, int port, 
      	char *from, char *to, char *format_string, arg1, arg2,...argN)

      Note: Function works just like printf.

    • Exposed a few more attributes
    • Minor bug fixes
  • 7.20.2010
    • Updated source code
  • 1.3.2011 – Snorkel 2.0.0.1 update (still free)
    • New improved API
    • Faster performance
    • Added new APIs Aqua (Service SDK) Sailfish (C/C++ application server)
    • Platform support MAC OSX, SunOS, Debian Linux, Windows
    • New download site: http://snorkelembedded.webs.com

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)