A simple web server mainly based on ideas from Jasper Schuurmans Netduino web server
Dependents: RdBlindsServer SpideyWallWeb RdGasUseMonitor
A fast and reliable web server for MBED! http://robdobson.com/2015/08/a-reliable-mbed-webserver/
It has a very neat way to implement REST commands and can serve files from local storage (on LPC1768 for instance) and from SD cards. It also has a caching facility which is particularly useful for serving files from local storage.
The server can be run in the main() thread (and has a sub-2ms response time if this is done) or in a mbed-rtos thread which increases the response time to (a still respectable) 30ms or so.
The latest project that uses this is here - https://developer.mbed.org/users/Bobty/code/SpideyWallWeb/
int main (void) { // Ethernet interface EthernetInterface::init(); // Connect ethernet EthernetInterface::connect(); // Init the web server pc.printf("Starting web server\r\n"); char* baseWebFolder = "/sd/"; // should be /sd/ for SDcard files - not used for local file system RdWebServer webServer; // Add commands to handle the home page and favicon webServer.addCommand("", RdWebServerCmdDef::CMD_LOCALFILE, NULL, "index.htm", true); webServer.addCommand("favicon.ico", RdWebServerCmdDef::CMD_LOCALFILE, NULL, NULL, true); // Add the lightwall control commands webServer.addCommand("name", RdWebServerCmdDef::CMD_CALLBACK, &lightwallGetSystemName); webServer.addCommand("clear", RdWebServerCmdDef::CMD_CALLBACK, &lightwallClear); webServer.addCommand("rawfill", RdWebServerCmdDef::CMD_CALLBACK, &lightwallRawFill); webServer.addCommand("fill", RdWebServerCmdDef::CMD_CALLBACK, &lightwallFill); webServer.addCommand("showleds", RdWebServerCmdDef::CMD_CALLBACK, &lightwallShowLeds); // Start the server webServer.init(WEBPORT, &led4, baseWebFolder); webServer.run(); } // Get system name - No arguments required char* lightwallGetSystemName(int method, char*cmdStr, char* argStr, char* msgBuffer, int msgLen, int contentLen, unsigned char* pPayload, int payloadLen, int splitPayloadPos) { // Perform any required actions here .... // ... // Return the system name return systemName; }
This server was originally based on a Netduino web server from Jasper Schuurmans but has been optimised for speed.
Diff: RdWebServer.cpp
- Revision:
- 17:080f2bed8b36
- Parent:
- 16:0248bbfdb6c1
- Child:
- 18:5de680c4cfcb
--- a/RdWebServer.cpp Tue May 05 22:14:16 2015 +0000 +++ b/RdWebServer.cpp Mon May 11 11:17:15 2015 +0000 @@ -4,14 +4,13 @@ More details at http://robdobson.com/2013/10/moving-my-window-shades-control-to-mbed/ */ -#define RDWEB_DEBUG 5 +//#define RDWEB_DEBUG 5 #include "RdWebServer.h" const int MAX_CMDSTR_LEN = 100; const int MAX_ARGSTR_LEN = 100; const int MAX_FILENAME_LEN = (MAX_ARGSTR_LEN + 20); -const int MAX_MIMETYPE_LEN = 50; RdWebServer::RdWebServer() { @@ -33,6 +32,23 @@ _pStatusLed = pStatusLed; _pBaseWebFolder = pBaseWebFolder; + // The sample web server on MBED site turns off blocking after the accept has happened + // I've tried this but it doesn't seem to yield a reliable server + _blockingOnAccept = true; + _blockingOnReceive = true; + + // This is the same as the default in the socket.cpp file + _timeoutOnBlocking = 1500; + + // Currently tested using close connection after send with a single file which works fine + // If closeConnAfterSend is set false then the socket connection remains open and only + // one client can access the server at a time + // Need to test with the closeConnAfterSend seetting true when trying to download multiple files + // for a website as previous experience would indicate that requests might be missed in this scenario + // although all other settings may not have been the same + _closeConnAfterSend = true; + _closeConnOnReceiveFail = true; + // Setup tcp socket _serverSocket.set_blocking(true); if(_serverSocket.bind(port)< 0) @@ -56,31 +72,13 @@ if (!_initOk) return; - // The sample web server on MBED site turns off blocking after the accept has happened - // I've tried this but it doesn't seem to yield a reliable server - bool blockingOnAccept = true; - bool blockingOnReceive = true; - - // This is the same as the default in the socket.cpp file - int timeoutOnBlocking = 1500; - - // Currently tested using close connection after send with a single file which works fine - // If closeConnAfterSend is set false then the socket connection remains open and only - // one client can access the server at a time - // Need to test with the closeConnAfterSend seetting true when trying to download multiple files - // for a website as previous experience would indicate that requests might be missed in this scenario - // although all other settings may not have been the same - bool closeConnAfterSend = true; - bool closeConnOnReceiveFail = true; - const char* closeConnStr = "Connection: Close\r\n"; - // Start accepting connections while (true) { TCPSocketConnection clientSocketConn; // Accept connection if available RD_INFO("Waiting for TCP connection\r\n"); - clientSocketConn.set_blocking(blockingOnAccept, timeoutOnBlocking); + clientSocketConn.set_blocking(_blockingOnAccept, _timeoutOnBlocking); if(_serverSocket.accept(clientSocketConn)<0) { RD_WARN("TCP Socket failed to accept connection\n\r"); @@ -97,12 +95,12 @@ while(clientSocketConn.is_connected() && !forcedClosed) { // Receive data - clientSocketConn.set_blocking(blockingOnReceive, timeoutOnBlocking); + clientSocketConn.set_blocking(_blockingOnReceive, _timeoutOnBlocking); int rxLen = clientSocketConn.receive(_buffer, HTTPD_MAX_REQ_LENGTH); if (rxLen == -1) { RD_DBG("clientSocketConn.receive() returned %d\r\n", rxLen); - if (closeConnOnReceiveFail) + if (_closeConnOnReceiveFail) { int closeRet = clientSocketConn.close(); RD_DBG("Failed receive connection close() ret %d is connected %d\r\n", closeRet, clientSocketConn.is_connected()); @@ -118,6 +116,9 @@ if (rxLen > HTTPD_MAX_REQ_LENGTH) { RD_DBG("clientSocketConn.receive() returned %d - too long\r\n", rxLen); + formHTTPHeader("413 Request Entity Too Large", "text/plain", 0); +// sprintf(_httpHeader,"HTTP/1.1 413 Request Entity Too Large \r\nContent-Type: text\r\nContent-Length: 0\r\n%s\r\n", closeConnAfterSend ? closeConnStr : ""); + int sentRet = clientSocketConn.send(_httpHeader,strlen(_httpHeader)); continue; } @@ -126,15 +127,25 @@ RD_DBG("%s\r\n", _buffer); // Send a response - char* content = "HELLO\r\n"; - int sz = strlen(content); - sprintf(_httpHeader,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n%s\r\n", sz, closeConnAfterSend ? closeConnStr : ""); - int sentRet = clientSocketConn.send(_httpHeader,strlen(_httpHeader)); - int sentRet2 = clientSocketConn.send(content, strlen(content)); +// char* content = "HELLO\r\n"; +// int sz = strlen(content); +// sprintf(_httpHeader,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n%s\r\n", sz, closeConnAfterSend ? closeConnStr : ""); +// int sentRet = clientSocketConn.send(_httpHeader,strlen(_httpHeader)); +// int sentRet2 = clientSocketConn.send(content, strlen(content)); +// RD_DBG("Sent %s header ret %d content ret %d now connected %d\r\n", content, sentRet, sentRet2, clientSocketConn.is_connected()); + + // Handle received message + if (handleReceivedHttp(clientSocketConn)) + { + // OK + } + else + { + // FAIL + } - RD_DBG("Sent %s header ret %d content ret %d now connected %d\r\n", content, sentRet, sentRet2, clientSocketConn.is_connected()); - - if (closeConnAfterSend) + // Close connection if required + if (_closeConnAfterSend) { int closeRet = clientSocketConn.close(); RD_DBG("After send connection close() ret %d is connected %d\r\n", closeRet, clientSocketConn.is_connected()); @@ -169,8 +180,8 @@ // else if (rxLen > HTTPD_MAX_REQ_LENGTH) // { // sprintf(_httpHeader,"HTTP/1.1 413 Request Entity Too Large \r\nContent-Type: text\r\nConnection: Close\r\n\r\n"); -// clientSocketConn.send_all(_httpHeader,strlen(_httpHeader)); -// clientSocketConn.send_all(_buffer, rxLen); +// clientSocketConn.send(_httpHeader,strlen(_httpHeader)); +// clientSocketConn.send(_buffer, rxLen); // break; // } // _buffer[rxLen] = '\0'; @@ -199,7 +210,7 @@ // bool RdWebServer::handleReceivedHttp(TCPSocketConnection &clientSocketConn) { - bool closeConn = true; + bool handledOk = false; RD_DBG("Received Data: %d\n\r\n\r%.*s\n\r",strlen(_buffer),strlen(_buffer),_buffer); int method = METHOD_OTHER; if (strncmp(_buffer, "GET ", 4) == 0) @@ -224,50 +235,37 @@ if ((*it)->_cmdType == RdWebServerCmdDef::CMD_CALLBACK) { char* respStr = ((*it)->_callback)(method, cmdStr, argStr); - clientSocketConn.send_all(respStr, strlen(respStr)); + clientSocketConn.send(respStr, strlen(respStr)); } - else if ((*it)->_cmdType == RdWebServerCmdDef::CMD_LOCALFILE) + else if ( ((*it)->_cmdType == RdWebServerCmdDef::CMD_LOCALFILE) || + ((*it)->_cmdType == RdWebServerCmdDef::CMD_SDORUSBFILE) ) { -#ifdef SUPPORT_LOCAL_FILE_CACHE - if ((*it)->_substFileName[0] != '\0') - { - char combinedFileName[MAX_FILENAME_LEN]; - strcpy(combinedFileName, (*it)->_substFileName); - if (combinedFileName[strlen(combinedFileName)-1] == '*') - strcpy(combinedFileName+strlen(combinedFileName)-1, argStr); - handleLocalFileRequest(combinedFileName, argStr, clientSocketConn, _httpHeader, (*it)->_bCacheIfPossible); - } + char combinedFileName[MAX_FILENAME_LEN]; + strcpy(combinedFileName, (*it)->_substFileName); + if (strlen(combinedFileName) == 0) + strcpy(combinedFileName, cmdStr); + else if (combinedFileName[strlen(combinedFileName)-1] == '*') + strcpy(combinedFileName+strlen(combinedFileName)-1, argStr); + if ((*it)->_cmdType == RdWebServerCmdDef::CMD_LOCALFILE) + handledOk = handleLocalFileRequest(combinedFileName, argStr, clientSocketConn, (*it)->_bCacheIfPossible); else -#endif - { - handleLocalFileRequest(cmdStr, argStr, clientSocketConn, _httpHeader, (*it)->_bCacheIfPossible); - } - } - else if ((*it)->_cmdType == RdWebServerCmdDef::CMD_SDORUSBFILE) - { -#ifdef SUPPORT_LOCAL_FILE_CACHE - if ((*it)->_substFileName[0] != '\0') - { - char combinedFileName[MAX_FILENAME_LEN]; - strcpy(combinedFileName, (*it)->_substFileName); - if (combinedFileName[strlen(combinedFileName)-1] == '*') - strcpy(combinedFileName+strlen(combinedFileName)-1, argStr); - closeConn = handleSDFileRequest(combinedFileName, argStr, clientSocketConn, _httpHeader); - } - else -#endif - { - closeConn = handleSDFileRequest(cmdStr, argStr, clientSocketConn, _httpHeader); - } + handledOk = handleSDFileRequest(combinedFileName, argStr, clientSocketConn); + } break; } } // If command not found see if it is a local file if (!cmdFound) - closeConn = handleSDFileRequest(cmdStr, argStr, clientSocketConn, _httpHeader); + { + char combinedFileName[MAX_FILENAME_LEN]; + strcpy(combinedFileName, cmdStr); + strcat(combinedFileName, "/"); + strcat(combinedFileName, argStr); + handledOk = handleSDFileRequest(cmdStr, argStr, clientSocketConn); + } } - return closeConn; + return handledOk; } void RdWebServer::addCommand(char* pCmdStr, int cmdType, CmdCallbackType callback, char* substFileName, bool cacheIfPossible) @@ -275,9 +273,35 @@ _commands.push_back(new RdWebServerCmdDef(pCmdStr, cmdType, callback, substFileName, cacheIfPossible)); } -bool RdWebServer::handleSDFileRequest(char* inFileName, char* argStr, TCPSocketConnection &clientSocketConn, char* httpHeader) +void RdWebServer::formHTTPHeader(const char* rsltCode, const char* contentType, int contentLen) +{ + const char* closeConnStr = "\r\nConnection: Close"; + if(contentLen != -1) + sprintf(_httpHeader, "HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d%s\r\n\r\n", rsltCode, contentType, contentLen, _closeConnAfterSend ? closeConnStr : ""); + else + sprintf(_httpHeader, "HTTP/1.1 %s\r\nContent-Type: %s%s\r\n\r\n", rsltCode, contentType, _closeConnAfterSend ? closeConnStr : ""); +} + +char* getMimeTypeStr(char* filename) { - bool closeConn = true; + char* mimeType = "text/plain"; + char *pDot = strrchr(filename, '.'); + if (pDot && (!strcmp(pDot, ".html") || !strcmp(pDot, ".htm"))) + mimeType = "text/html"; + else if (pDot && !strcmp(pDot, ".css")) + mimeType = "text/css"; + else if (pDot && !strcmp(pDot, ".js")) + mimeType = "application/javascript"; + else if (pDot && !strcmp(pDot, ".png")) + mimeType = "image/png"; + else if (pDot && (!strcmp(pDot, ".jpeg") || !strcmp(pDot, ".jpeg"))) + mimeType = "image/jpeg"; + return mimeType; +} + +bool RdWebServer::handleSDFileRequest(char* inFileName, char* argStr, TCPSocketConnection &clientSocketConn) +{ + bool handledOk = false; const int HTTPD_MAX_FNAME_LENGTH = 127; char filename[HTTPD_MAX_FNAME_LENGTH+1]; @@ -291,22 +315,23 @@ DIR *d = opendir(filename); if (d != NULL) { - sprintf(httpHeader,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: Close\r\n\r\n"); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); - sprintf(httpHeader,"<html><head><title>Directory Listing</title></head><body><h1>%s</h1><ul>", inFileName); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); + formHTTPHeader("200 OK", "text/html", -1); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); + sprintf(_httpHeader,"<html><head><title>Directory Listing</title></head><body><h1>%s</h1><ul>", inFileName); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); struct dirent *p; while((p = readdir(d)) != NULL) { RD_INFO("%s\r\n", p->d_name); - sprintf(httpHeader,"<li>%s</li>", p->d_name); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); + sprintf(_httpHeader,"<li>%s</li>", p->d_name); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); } } closedir(d); RD_DBG("Directory closed\n"); - sprintf(httpHeader,"</ul></body></html>"); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); + sprintf(_httpHeader,"</ul></body></html>"); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); + handledOk = true; } else #endif @@ -318,51 +343,49 @@ if (fp == NULL) { RD_WARN("Filename %s not found\r\n", filename); - sprintf(httpHeader,"HTTP/1.1 404 Not Found \r\nContent-Type: text\r\nContent-Length: 0\r\n\r\n"); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); - closeConn = false; + formHTTPHeader("404 Not Found", "text/plain", 0); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); + handledOk = true; } else { RD_INFO("Sending file %s\r\n", filename); + // Find file length fseek(fp, 0L, SEEK_END); int sz = ftell(fp); fseek(fp, 0L, SEEK_SET); - sprintf(httpHeader,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n", sz); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); - // MIME type - char *pDot = strrchr(filename, '.'); - char mimeType[MAX_MIMETYPE_LEN+1]; - if (pDot && (!strcmp(pDot, ".html") || !strcmp(pDot, ".htm"))) - strcpy(mimeType, "Content-Type: text/html\r\n"); - else if (pDot && !strcmp(pDot, ".css")) - strcpy(mimeType, "Content-Type: text/css\r\n"); - if (pDot && !strcmp(pDot, ".js")) - strcpy(mimeType, "Content-Type: application/javascript\r\n"); -// clientSocketConn.send_all(mimeType,strlen(mimeType)); + // Get mime type + char* mimeType = getMimeTypeStr(filename); RD_INFO("MIME TYPE %s", mimeType); + // Form header + formHTTPHeader("200 OK", mimeType, sz); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); // Read file in blocks and send int rdCnt = 0; char fileBuf[1024]; while ((rdCnt = fread(fileBuf, sizeof( char ), 1024, fp)) == 1024) { - clientSocketConn.send_all(fileBuf, rdCnt); + clientSocketConn.send(fileBuf, rdCnt); } - clientSocketConn.send_all(fileBuf, rdCnt); + clientSocketConn.send(fileBuf, rdCnt); fclose(fp); - closeConn = false; + handledOk = true; } } - return closeConn; + return handledOk; } #ifdef SUPPORT_LOCAL_FILE_CACHE -void RdWebServer::sendFromCache(RdFileCacheEntry* pCacheEntry, TCPSocketConnection &clientSocketConn, char* httpHeader) +void RdWebServer::sendFromCache(RdFileCacheEntry* pCacheEntry, TCPSocketConnection &clientSocketConn) { RD_INFO("Sending file %s from cache %d bytes\r\n", pCacheEntry->_fileName, pCacheEntry->_nFileLen); - sprintf(httpHeader,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: Close\r\n\r\n"); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); + // Get mime type + char* mimeType = getMimeTypeStr(pCacheEntry->_fileName); + RD_INFO("MIME TYPE %s", mimeType); + // Form header + formHTTPHeader("200 OK", mimeType, pCacheEntry->_nFileLen); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); char* pMem = pCacheEntry->_pFileContent; int nLenLeft = pCacheEntry->_nFileLen; int blkSize = 2048; @@ -370,14 +393,14 @@ { if (blkSize > nLenLeft) blkSize = nLenLeft; - clientSocketConn.send_all(pMem, blkSize); + clientSocketConn.send(pMem, blkSize); pMem += blkSize; nLenLeft -= blkSize; } } #endif -void RdWebServer::handleLocalFileRequest(char* inFileName, char* argStr, TCPSocketConnection &clientSocketConn, char* httpHeader, bool bCacheIfPossible) +bool RdWebServer::handleLocalFileRequest(char* inFileName, char* argStr, TCPSocketConnection &clientSocketConn, bool bCacheIfPossible) { #ifdef SUPPORT_LOCAL_FILESYSTEM @@ -401,7 +424,7 @@ if ((*it)->_bCacheValid) { sendFromCache(*it, clientSocketConn, httpHeader); - return; + return true; } bTryToCache = false; // don't try to cache as cacheing must have already failed } @@ -419,7 +442,7 @@ if (bCacheSuccess) { sendFromCache(pCacheEntry, clientSocketConn, httpHeader); - return; + return true; } } } @@ -431,31 +454,40 @@ if (fp == NULL) { RD_WARN("Local file %s not found\r\n", localFilename); - sprintf(httpHeader,"HTTP/1.1 404 Not Found \r\nContent-Type: text\r\nConnection: Close\r\n\r\n"); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); - clientSocketConn.send_all(reqFileNameStr,strlen(reqFileNameStr)); + formHTTPHeader("404 Not Found", "text/plain", 0); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); + return true; } else { RD_INFO("Sending file %s from disk\r\n", localFilename); - sprintf(httpHeader,"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: Close\r\n\r\n"); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); + // Find file length + fseek(fp, 0L, SEEK_END); + int sz = ftell(fp); + fseek(fp, 0L, SEEK_SET); + // Get mime type + char* mimeType = getMimeTypeStr(localFilename); + RD_INFO("MIME TYPE %s", mimeType); + // Form header + formHTTPHeader("200 OK", mimeType, sz); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); int rdCnt = 0; - char fileBuf[2000]; - while ((rdCnt = fread(fileBuf, sizeof( char ), 2000, fp)) == 2000) + while ((rdCnt = fread(_httpHeader, sizeof( char ), sizeof(_httpHeader), fp)) == sizeof(_httpHeader)) { - clientSocketConn.send_all(fileBuf, rdCnt); + clientSocketConn.send(_httpHeader, rdCnt); } - clientSocketConn.send_all(fileBuf, rdCnt); + if (rdCnt != 0) + clientSocketConn.send(_httpHeader, rdCnt); fclose(fp); + return true; } #else RD_WARN("Local file system not supported\r\n"); - sprintf(httpHeader,"HTTP/1.1 404 Not Found \r\nContent-Type: text\r\nConnection: Close\r\n\r\n"); - clientSocketConn.send_all(httpHeader,strlen(httpHeader)); - clientSocketConn.send_all(inFileName,strlen(inFileName)); + formHTTPHeader("404 Not Found", "text/plain", 0); + clientSocketConn.send(_httpHeader,strlen(_httpHeader)); + return true; #endif }