A simple web server that can be bound to either the EthernetInterface or the WiflyInterface.
Dependents: Smart-WiFly-WebServer WattEye X10Svr SSDP_Server
Diff: SW_HTTPServer.h
- Revision:
- 3:17928786bdb5
- Parent:
- 2:a29c32190037
- Child:
- 4:f34642902056
--- a/SW_HTTPServer.h Sun Jun 02 18:32:05 2013 +0000 +++ b/SW_HTTPServer.h Mon Jun 24 20:02:01 2013 +0000 @@ -13,43 +13,97 @@ #define PC Serial #endif +/// This is the default buffer size used to send files. You might +/// size this to be a little less than the ethernet frame size +#define FILESEND_BUF_SIZE 1200 + + +/// MAX_HEADER_SIZE is the default size to contain the largest header. This is +/// the size of the URL and query string, and also all the +/// other header information about the client. This can be +/// a couple of K, larger if you have big forms as it includes the +/// form data that is submitted. +#define MAX_HEADER_SIZE 1000 + /// HTTPServer is a simple web server using the WiFly module. /// -/// Partially derived from nweb +/// While simple, it is a capable, web server. The basic mode +/// of operation is for it to serve static web pages from an available +/// file system. +/// +/// The default page is index.htm (compile time defined) +/// standard support to serve a number of standard file types; +/// gif, jpg, jpeg, ico, png, zip, gz, tar, txt, pdf, htm, html +/// (this list is also compile time defined) +/// +/// It can also serve dynamically generated pages, and therefore +/// respond to form submission. Through the dynamic interface it is +/// then quite easy to interact with the hardware, reading the inputs +/// or signaling outputs. +/// +/// @code +/// HTTPServer svr(&wifly, HTTP_SERVER_PORT, "/local/", 30, 10, &pc); +/// svr.RegisterHandler("/dyn1", SimpleDynamicPage); +/// while (true) +/// { +/// svr.Poll(); // this is non blocking process, but variable execution +/// } +/// @endcode +/// +/// This web server used nweb as a starting point, but expanded well beyond there. /// http://www.ibm.com/developerworks/systems/library/es-nweb/sidefile1.html /// -/// Uses a modified WiflyInterface - a number of performance issues -/// were identified and resolved in the local version. +/// @note This server uses a modified version of the mbed WiflyInterface - there +/// were a number of performance issues identified and resolved in the local version. /// -/// Also, if nothing visits its web page for a long time, it seems -/// to disable. Might be a sleep timer I'm not spotting. -/// -/// Given: scheme://domain:port/path?query_string#fragment_id +/// Given: scheme://server:port/path?query_string#fragment_id /// @li scheme is "http" -/// @li domain is whatever IP the server has +/// @li server is whatever IP the server has /// @li port is the registered port /// @li /path is the reference to the file (actual or logical) on the server /// @li query_string is any combination of name=value pairs /// @li fragment_id is a reference to an anchor on the page /// -/// Feature request list: -/// @todo make it non-blocking. -/// @todo hunt down lengthy operations - there seems to be a long timeout somewhere. -/// @todo parse the header similar to the query string, and then make +/// Features: +/// @li Serves static pages from a file system. Many normal filetypes are +/// supported. +/// @li Compile time configurable for the "default" file, typically index.htm. +/// @li Provides a registration interface for dynamically generated pages that +/// can then interact with other hardware. +/// @li Revised to be Non-blocking, however the execution time is variable +/// depending on the actions being performed. +/// +/// Limitations: +/// @li Supports only a single connection at a time. +/// @li Rapid requests for page objects (e.g. embedded images) are lost. Still +/// working to understand this issue. +/// +/// ToDo: +/// @li A web page with served objects (img src=...) is rarely served properly. It +/// might trace to forcing the connection to close, but not yet sure. +/// Explore "Set Uart Rx Data Buffer" in WiFly manual 2.3.65 +/// @li hunt down lengthy operations - there seems to be a long timeout somewhere. +/// @li parse the header similar to the query string, and then make /// those parameters accessible - perhaps like environment vars. -/// @todo move part of the POST method handler to the registered handler, so +/// @li move part of the POST method handler to the registered handler, so /// it can decide if it should allocate the needed memory. -/// @todo Add password capability to some web pages -/// @todo transform the pc serial interface to a log interface, which might +/// @li Add password capability to some web pages +/// @li transform the pc serial interface to a log interface, which might /// be more useful. -/// @todo Add ability to put WiFly in AP mode and then configuration pages +/// @li Add ability to put WiFly in AP mode and then configuration pages /// to find and join a network. -/// @todo Add ability to change/update SW in the WiFly module -/// @todo Add ability to upload a new application to the mbed +/// @li Add ability to change/update SW in the WiFly module +/// @li Add ability to upload a new application to the mbed /// /// History: /// @li 20130530 Initial version /// @li 20130601 Renamed ip_process to Poll +/// @li 20130617 Cleaned up some of the documentation changes +/// @li 20130623 Make it non-blocking. "Poll" takes a variable amount +/// of time, based on whether it is idle, or how much it +/// has to do. +/// @li It now survives overnight and still responds, so the problem with +/// it going offline after long inactivity appears resolved. /// /// @note Copyright © 2013 by Smartware Computing, all rights reserved. /// Individuals may use this application for evaluation or non-commercial @@ -66,26 +120,64 @@ { public: /** - * namevalue pairs for parameters + * name-value pairs for parameters */ - typedef struct { + typedef struct NAMEVALUE { char * name; char * value; } namevalue; /** - * Indicates the purpose of the callback + * Indicates the purpose of the Handler callback + * + * Application code in a dynamic page uses this to determine the state + * and therefore the needed operation to be performed. + * + * @code + * bool SimpleDynamicPage(HTTPServer *svr, HTTPServer::CallBackType type, const char * path, const HTTPServer::namevalue *params, int paramcount) { + * char buf[100]; + * bool ret = false; + * + * switch (type) { + * case HTTPServer::SEND_PAGE: + * svr->header(200, "OK", "Content-Type: text/html\r\n"); + * svr->send("<html><head><title>Dynamic Page</title></head>\r\n"); + * svr->send("<body>\r\n"); + * svr->send("This page was generated dynamically. Create your own name=value pairs on the URL " + * "which uses the GET method.<br/>\r\n"); + * sprintf(buf, "%d parameters passed to {%s}:<br/>\r\n", paramcount, path); + * svr->send(buf); + * for (int i=0; i<paramcount; i++) { + * sprintf(buf, "%d: %s = %s<br/>\r\n", i, params[i].name, params[i].value); + * svr->send(buf); + * } + * svr->send("<br/><a href='/'>back to main</a></body></html>\r\n"); + * ret = true; + * break; + * case HTTPServer::CONTENT_LENGTH_REQUEST: + * ret = true; + * break; + * case HTTPServer::DATA_TRANSFER: + * ret = true; + * break; + * default: + * ret = false; + * break; + * } + * return ret; + * } + * @endcode */ - typedef enum { - CONTENT_LENGTH_REQUEST, ///<!- ask the user if they wish to accept the data - DATA_TRANSFER, - SEND_PAGE, ///<! the activated method should now send the page + typedef enum CALLBACKTYPE { + CONTENT_LENGTH_REQUEST, ///< ask the client if they wish to accept the data + DATA_TRANSFER, ///< not currently used, may allow "chunking" the data to the client + SEND_PAGE, ///< the activated method should now send the page } CallBackType; /** - * callback prototype for custom handler + * This is the prototype for custom handlers that are activated via a callback * - * This callback gets overloaded for a few purposes, which can be identified by the CallBackType parameter + * This callback gets overloaded for a few purposes, which can be identified by the \see CallBackType parameter * @li SEND_PAGE - the callback should now send the html page, using as many svr->send() as needed. * When the callback returns, it should always indicate true. * @li CONTENT_LENGTH_REQUEST - the server is asking the callback if it wants to receive the message, @@ -108,16 +200,20 @@ * @param maxparams defines the maximum number of parameters to a dynamic function (and the memory to support them). * @param maxdynamicpages defines the maximum number of dynamic pages that can be registered. * @param pc is the serial port for debug information (I should transform this to a log interface) + * @param allocforheader is the memory allocation to support the largest expected header from a client + * @param allocforfile is the memory allocation to support sending a file to the client. This is typically sized to fit + * an ethernet frame. */ - HTTPServer(Wifly * wifly, int port = 80, const char * webroot = "/", int maxparams = 30, int maxdynamicpages = 10, PC * pc = NULL); + HTTPServer(Wifly * wifly, int port = 80, const char * webroot = "/", int maxparams = 30, int maxdynamicpages = 10, PC * pc = NULL, + int _allocforheader = MAX_HEADER_SIZE, int _allocforfile = FILESEND_BUF_SIZE); /** - * destructor, which can clean up a few things + * Destructor, which can clean up memory. */ ~HTTPServer(); /** - * the process to call whenever there is free time, as this basically does + * The process to call whenever there is free time, as this basically does * all the work to monitor for connections and handle replies. * * 20130601 Renamed from ip_process to Poll @@ -125,29 +221,40 @@ void Poll(); /** - * Send a header back to the client and automatically appends a "\r\n". Each parameter must - * send a "\r\n" as part of that string. + * Send typical header data, and some optional data back to the client. + * + * This forms and sends the typical header back to the client. It may also send + * optional data (which must end with "\r\n"). It then sends the second newline + * sequence that signals the end of the header. * * @param code is the optional return code; 200 = OK, if not provided then 404 = Not found is returned * @param code_text is the text to align with the code (e.g. 404, "Not Found") * @param content_type is a pointer to "Content-Type: text/html\r\n" (for example) - * @param optional_text is a pointer to any other text that is part of the header + * @param optional_text is a pointer to any other text that is part of the header, which must + * have \r\n termination. */ void header(int code = 404, const char * code_text = "Not Found", const char * content_type = NULL, const char * optional_text = NULL); /** * Send text to the client * + * This sends the specified text to the client. If the number of bytes is not set, + * then it calculates the number of bytes as a string. For binary transfers, the + * number of bytes to send is required for proper operation. + * * @param msg is the text string to send * @param bytes is the number of bytes to send. If not set, then strlen is calculated. */ void send(const char * msg, int bytes = -1); /** - * Send a file to the client, including the header + * Send a referenced file to the client, including the header + * + * This sends a file from the filesystem to the client. It must be of a supported type + * in order to properly create the header. * * @param filename is the fully qualified path and filename - * @param filetype is the header information (e.g. "Content-Type: text/html") + * @param filetype is the header information (e.g. "Content-Type: application/pdf") * @return true if it thinks it sent ok, false otherwise. */ bool SendFile(const char * filename, const char * filetype); @@ -155,6 +262,55 @@ /** * register a handler for a specific URL. * + * This api lets you register a dynamic handler in the web server. This is + * most useful for interactive web pages, rather than simply serving static + * pages. + * + * @code + * + * ... + * svr.RegisterHandler("/dyn1", SimpleDynamicPage);svr.RegisterHandler("/dyn1", SimpleDynamicPage); + * ... + * + * bool SimpleDynamicPage(HTTPServer *svr, HTTPServer::CallBackType type, const char * path, const HTTPServer::namevalue *params, int paramcount) { + * char buf[100]; + * bool ret = false; + * + * switch (type) { + * case HTTPServer::SEND_PAGE: + * svr->header(200, "OK", "Content-Type: text/html\r\n"); + * svr->send("<html><head><title>Dynamic Page</title></head>\r\n"); + * svr->send("<body>\r\n"); + * svr->send("This page was generated dynamically. Create your own name=value pairs on the URL " + * "which uses the GET method.<br/>\r\n"); + * sprintf(buf, "%d parameters passed to {%s}:<br/>\r\n", paramcount, path); + * svr->send(buf); + * for (int i=0; i<paramcount; i++) { + * sprintf(buf, "%d: %s = %s<br/>\r\n", i, params[i].name, params[i].value); + * svr->send(buf); + * } + * svr->send("Stats:<br/>\r\n"); + * sprintf(buf,"Free memory space: %d<br/>\r\n", Free()); + * svr->send(buf); + * sprintf(buf,"Max Header size: %d<br/>\r\n", svr->GetMaxHeaderSize()); + * svr->send(buf); + * svr->send("<br/><a href='/'>back to main</a></body></html>\r\n"); + * ret = true; + * break; + * case HTTPServer::CONTENT_LENGTH_REQUEST: + * ret = true; + * break; + * case HTTPServer::DATA_TRANSFER: + * ret = true; + * break; + * default: + * ret = false; + * break; + * } + * return ret; + * } + * @endcode + * * @param path to register * @param callback of type Handler * @return true if successfully registered @@ -164,6 +320,16 @@ /** * determine if the named file is a supported type (e.g. .htm, .jpg, ...) * + * if you pass in a filename, it will attempt to extract the extension + * and compare that to the list of supported file types. If it finds a + * match, then it will return a pointer to the content-type string. + * + * @code + * fType = GetSupportedType("mypix.jpg"); + * if (fType) { + * ... + * @endcode + * * @param filename is the filename to test, based on the extension * @return pointer to a Content-Type string if supported, or NULL if not. */ @@ -172,15 +338,26 @@ /** * search the available parameters for 'name' and if found, return the 'value' * + * After the querystring is parsed, the server maintains an array of + * name=value pairs. This Get function will search for the passed in name + * and provide access to the value. + * + * @code + * BusOut leds(LED1,LED2,LED3,LED4); + * ... + * leds = atoi(svr->GetParameter("leds")); + * @endcode + * * @param name is the name to search for * @return pointer to the value, or NULL */ const char * GetParameter(const char * name); /** - * parse the text string into name=value parameters. This will directly - * modify the referenced string. If there is a #fragment_id on the end - * of the string, that will be removed. + * Parse the text string into name=value parameters. + * + * This will directly modify the referenced string. If there is a + * #fragment_id on the end of the string, it will be removed. * * @param pString is a pointer to the string. */ @@ -188,8 +365,17 @@ /** * Unescape string converts a coded string "in place" into a normal string + * + * A query string will have a number of characters replaced for communication + * which includes spaces, quotes, question marks and more. Most of them + * will be replaced with a %xx format, where xx is the hex code for the + * character. Since the string will only get shorter when this happens + * the operation is performed in place. + * * this "This%20is%20a%20question%3F%20and%20an%20answer." + * * becomes "This is a question? and an answer." + * * @note '+' is another form of space, so is converted to a space before the %xx * * @param encoded string to be converted @@ -198,21 +384,84 @@ /** * Get the IP address of the remote node to which we are connected. - * @caution this switches the module into, and out of, command mode - * which has quite a time penalty. + * + * This will get the IP address of the remote node to which we are + * currently connected. This is written into the buffer in + * "192.168.100.234" format. If the buffer size is note >= 16 bytes, + * it will set the buffer to null. + * + * @note This switches the module into, and out of, command mode + * which has quite a time penalty. + * + * @param str is the string to write the address into, which should be at + * least as large as "192.168.100.203" (16-bytes). + * @param size of the str buffer must be >=16, so it will not buffer overrun. */ void GetRemoteAddr(char * str, int size); /** - * used to force a connection to close + * This is used to force a connection to close + * + * This switches the module into command mode, performs the close, + * then switches it back to data mode. So, this is a time-expensive + * command. */ void close_connection(); + + /** + * Get the size of the largest header. + * + * This is a diagnostic function, so you can resize the allocated + * buffer for your application. With proper sizing, more of the + * system memory is available for your application. + * + * @code + * sprintf(buf,"Max Header size: %d<br/>\r\n", svr->GetMaxHeaderSize()); + * svr->send(buf); + * @endcode + * + * @returns size in bytes of the larger header measured. + */ + int GetMaxHeaderSize(); + + + /** + * Performance parameter + */ + typedef struct SW_PERFPARAM { + unsigned long long TotalTime_us; + unsigned long Samples; + unsigned long MaxTime_us; + } SW_PerformanceParam; + + /** + * Performance metrics + */ + typedef struct SW_PERFDATA { + SW_PerformanceParam Header; + SW_PerformanceParam SendData; + //SW_PerformanceParam SendFile; + } SW_PerformanceData; + + /** + * Get performance metrics from the web server. + * + * This is a diagnostic function, and gathers data on the internal + * performance of the server, as it works various actions. + * + * @param p is a pointer to a SW_PerformanceData structure to be populated + */ + void GetPerformanceData(SW_PerformanceData * p); + + /** + * Reset performance metrics. + */ + void ResetPerformanceData(); private: Wifly * wifly; char * webroot; PC * pc; - Timer * timer; TCPSocketServer * server; TCPSocketConnection client; char * rewriteWithDefaultFile(char * queryString); @@ -220,14 +469,40 @@ int maxparams; namevalue *params; int paramcount; + int maxheaderbytes; + char * headerbuffer; + int headerbuffersize; - typedef struct { + Timer timer; // for performance metrics gathering + /** + * Records performance data + * + * This will take a pointer to a SW_PerformanceParam, and it will + * take the time when the performance measurement started. It locally + * accesses the current time to measure the elapsed. + * + * @param param is the performance parameter to update + * @param value is the reference time. + * @returns the current time which may be used as the reference time + * for further measurements. + */ + int RecordPerformanceData(SW_PerformanceParam * param, int value); + SW_PerformanceData perfData; + + typedef struct HANDLER { char * path; Handler callback; } handler; int maxdynamicpages; handler *handlers; int handlercount; + + char * queryType; + char * queryString; + char * hostString; + char * contentLength; + char * contentType; + char * postQueryString; /** * Extract the message from the record, by searching for the needle @@ -241,6 +516,10 @@ */ bool Extract(char * rec, char * needle, char ** string); + void SendResponse(); + bool ParseHeader(char * bPtr); + bool CheckDynamicHandlers(); + int HexCharToInt(char c); char HexPairToChar(char * p); };