I’ve had various stabs over a couple of years at building a reliable web-server for an MBED device. I’ve had several problems including: handling multiple concurrent requests, intermittent failure when opening and closing sockets, handling the limitation of a 1024 byte HTTP buffer and aborted connections reported by the browser.
I’ve decided to get to the bottom of this – starting with some very basic tests and building up to figure out if a reliable web server can be achieved. Clearly some things will never be possible – the speed of access to data on an SD card means that the server will never be very fast at serving files – and there is very little RAM memory available on most MBED hardware so caching won’t be a solution to this.
However, there are some things I’m really keen to find out (beyond stability) which will determine whether a web-server is a good solution for my requirements. One thing is how fast a request (e.g. POST) can be handled and how much data can be transferred in a single request. This is because one purpose I’d like to put an MBED web-server to is as a controller for the Spidey Wall that I have written about in recent posts.
To achieve this I’d need to transfer around 5k of data per “frame” of animation (there are around 1700 LEDS and each has an RGB value). But the MBED has limited RAM and I think I will only be able to send around 1K per REST request so that means 5 packets per “frame”. If I want a “frame-rate” of say 20 per second I’d need to transfer I need to send around 100 x 1Kbyte packets per second.
Test #1 Can I get a reliable TCP connection?
To test this I started with a simple Echo-back program from the MBED cookbook.
#include "mbed.h" #include "EthernetInterface.h" #define ECHO_SERVER_PORT 7 int main (void) { EthernetInterface eth; eth.init(); //Use DHCP eth.connect(); printf("\nServer IP Address is %s\r\n", eth.getIPAddress()); // TCP Socket server TCPSocketServer server; server.bind(ECHO_SERVER_PORT); server.listen(); while (true) { printf("Wait for new connection...\r\n"); TCPSocketConnection client; server.accept(client); client.set_blocking(false, 1500); // Timeout after (1.5)s printf("Connection from: %s\r\n", client.get_address()); char buffer[1024]; while (true) { int n = client.receive(buffer, sizeof(buffer)); if (n <= 0) break; buffer[n] = '\0'; send_all(buffer, n); // Echo back if (n <= 0) break; } client.close(); } }
And a Python program to test from a PC.
import socket import time def testSend(numToSend): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("192.168.0.227", 7)) msg = bytearray() for i in range(1000): msg.append(i % 255) numPackets = 0 totalPacketLen = 0 checkSum = 0 for i in range(numToSend): s.send(bytes(msg, 'UTF-8')) rtn = s.recv(1024) for byte in msg: checkSum += ord(byte) totalPacketLen += len(msg) numPackets += 1 print("Packets " + str(numPackets) + " TotalLen " + str(totalPacketLen) + " CheckSum " + "{:08x}".format(checkSum)) curTime = time.time() nPackets = 100 testSend(nPackets) print("Elapsed " + '{0:0.1f}'.format(((time.time()-curTime)/nPackets)*1000000) + "us per packet")
The end result of this test shows that no packets are dropped (even if 10000 are sent) and the round-trip with an MBED LPC1768 takes around 2.6mS on average per packet.
This is pretty good and I would hope is an indication that the idea of using a web-server to animate the Spidey Wall might be a goer.
Note that the MBED is very sensitive (unsurprisingly) to having it’s buffer overrun by receiving too much data. The LPC1768 seems to limit the number of bytes it can comfortably receive to 1024 (this is regardless of the size of the buffer passed into client.receive() ) – anything more and the connection is reset and seems to become unstable – at least I haven’t managed to figure out how to recover it once it has entered this state.
Test #2 Can I serve a single HTTP file quickly and reliably?
The test code here has almost the same structure as the TCP socket test code but the response returned is an HTTP formatted response. I have found that browsers will wait until a socket times out if certain headers aren’t present in the returned HTTP response. So, for instance, the “Connection: keep-alive” header must always be present and the Content-Length and Content-Type must be there if any content is returned. Furthermore the Content-Type must be correct or the browser will wait for more to be sent before giving up on a socket time-out.
#include "mbed.h" #include "EthernetInterface.h" #define HTTP_SERVER_PORT 80 RawSerial pc(USBTX, USBRX); void runServer (void const* arg) { pc.baud(115200); EthernetInterface eth; eth.init(); //Use DHCP eth.connect(); printf("\r\nServer IP Address is %s\r\n", eth.getIPAddress()); // TCP Socket server TCPSocketServer server; server.bind(HTTP_SERVER_PORT); server.listen(); while (true) { printf("Wait for new connection...\r\n"); TCPSocketConnection client; server.accept(client); client.set_blocking(false, 1500); // Timeout after 1.5s printf("Connection from: %s\r\n", client.get_address()); char buffer[1024]; while (true) { Timer t1; t1.start(); int n = client.receive(buffer, sizeof(buffer)); t1.stop(); if (n <= 0) break; // Terminate received message buffer[n] = '\0'; // printf(buffer); // printf("Received %d bytes\r\n",n); // Formulate a response - if GET / then send HELLO otherwise 404 Not Found static char* respHello = "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, GET, OPTIONS\r\nAccess-Control-Allow-Headers:accept, content-type\r\nContent-Length: 5\r\nContent-Type: text/html; charset=utf-8\r\n\r\nHELLO\r\n"; static char* respNotFound = "HTTP/1.1 404 Not Found\r\nConnection: keep-alive\r\n\r\n"; static char* respOption = "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, GET, OPTIONS\r\nAccess-Control-Allow-Headers:accept, content-type\r\nContent-Length: 0\r\nContent-Type: text/html; charset=utf-8\r\n\r\n"; static char* respPost = "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, GET, OPTIONS\r\nAccess-Control-Allow-Headers:accept, content-type\r\nContent-Length: 2\r\nContent-Type: text/html; charset=utf-8\r\n\r\nOK\r\n"; char* respMsg = respNotFound; if (strstr(buffer, "GET /cmd ") != NULL) respMsg = respHello; else if (strstr(buffer, "OPTIONS /cmd ") != NULL) respMsg = respOption; else if (strstr(buffer, "POST /cmd ") != NULL) respMsg = respPost; Timer t2; t2.start(); int sentN = client.send_all(respMsg, strlen(respMsg)); t2.stop(); printf("Sent response %d T1 %fs T2 %fs\r\n", sentN, t1.read(), t2.read()); if (sentN <= 0) break; } client.close(); printf("Conn Closed\r\n"); } } int main() { runServer(""); }
This will respond with “HELLO” to a browser that navigates to /cmd on the server. The Chrome browser shows information about the time taken for the server to respond. It looks as though things are working fine for my simple server (now that I have the right headers):
A 3ms response time doesn’t seem too bad at all.
Test #3 How fast can I POST to it?
Here I’ve moved to an HTML file opened directly in a desktop browser. When the link “Click to Send” is clicked a stream of 10000 POST requests are sent at 10ms intervals to the MBED server each containing a payload of 512 bytes. This is approximately half the data rate I will need to animate the Spidey Wall at 20 frames per second. The server seems to behave properly with no response failures or disconnections and the average response time reported by Chrome is 2ms which seems fine and consistent with the raw TCP tests earlier.
Click To Send
Test #4 Running the server in a thread?
Next I wanted to see if the server would run in an MBED thread. MBED has an RTOS which is capable of running the TCP stack and I have had success getting a server to run in this way but suspected that there was a performance hit. I modified the MBED code by replacing the main() function with:
int main() { Thread httpServer(&runServer, NULL, osPriorityNormal, (DEFAULT_STACK_SIZE * 3)); while(true) { } }
Unfortunately when running the server code in a thread responses slow down a lot. From around 2ms average to around 30ms average.
Test #5 Limiting requests to 1024 bytes
So far my web server attempts have not involved request packets longer than the buffer size of the MBED (i.e. 1024 bytes for an LPC1768). This, however, presents a problem as the headers on an HTTP cross-origin request are pretty long – on Chrome I see 364 bytes! Here’s an example:
POST /clear HTTP/1.1 Host: 192.168.0.227 Connection: keep-alive Content-Length: 0 Accept: */* Origin: null User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36 Content-Type: text/plain Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.8
So this leaves only a little over 600 bytes for the payload. In addition, if I want to send a whole string of RGB values to the Spidey Wall then I have to know how big a payload I can send and then split the messages up into little bits so that none of them overflow the buffer.
To test this I have had to generate packets which are small enough to be carried in a single message in all cases. This has taken a bit of work and I’ve made the following test:
The result shows that I can carry RGB information for 900 LEDs split across 5 messages. The first 4 messages carry only data and fill the buffer. The final message carries data and the ShowLeds command which sends the data to the LED strip.
I’m not really happy with this solution though because the overhead of communicating 5 messages at the application level isn’t ideal and I can’t be sure that the solution will work with all browsers as the headers sent by the browser may differ from device to device resulting in unpredictable buffer overflows and failure.
Test #6 Handling requests longer than 1024 bytes!
I’ve realised that this pain can be alleviated if I implement some code to check the Content-Length header on the initial request and then expect the packet to be delivered in pieces. The code I had to implement on the server was a bit long to include here but the whole of the server code is published as an MBED library here.
What this means is that I can have a simple REST server which implements an API such as:
http://hostname/clear http://hostname/showleds http://hostname/fill?start=20&len=10&r1=128&g1=64&b1=223 http://hostname/rawfill?start=100
The last REST command requires a payload which contains the RGB values (using contentType: ‘application/octet-stream’) of each LED that is to be set. So I can set the entire 1686 LEDS in the Spidey Wall with a single /rawfill command. Here’s some code which sends the RawFill command cross-origin using JQuery.
Click To Send
And from a performance perspective all seems well. The test code above sends 30000 "frames" (setting all 1686 LEDS) and I'm seeing a frame rate of around 20 frames per second which was my initial target. I also know that this is now limited by the time to send the data (using a serial protocol like SPI) to the addressable strips rather than by the web server. I could start looking at bumping the SPI frequency from 500kHz to 1MHz but I think I might just leave that for another day!
The code on the server side to handle the chunking of messages from an application point of view can now actually be quite simple:
// RawFill - arguments for start LED (e.g. /rawfill?start=0) // - payload of message contains binary data for RGB (1 byte for each) for each LED to be set char* lightwallRawFill(int method, char*cmdStr, char* argStr, char* msgBuffer, int msgLen, int contentLen, unsigned char* pPayload, int payloadLen, int splitPayloadPos) { drawingManager.RawFill(argStr, pPayload, payloadLen, splitPayloadPos); return generalRespStr; } void DrawingManager::RawFill(char* args, unsigned char* payload, int payloadLen, int payloadOffset) { int startLed = GetIntFromNameValPair(args, "start=", -1); if (startLed != -1 && payloadLen > 0) { int numLeds = payloadLen / 3; int fromLed = startLed + (payloadOffset / 3); pLedStrip->RawFill(fromLed, numLeds, payload); } }
These two functions are from different classes but is should be clear that all that's needed is to work out the startLed for each chunk of received data by using the payloadOffset (i.e. the position in the payload of the current chunk received).
Completing the picture
So I've pulled together some of the previous work I have done on an MBED web server and incorporated the things I'd learned from these tests. I made sure that the response headers were correct in all cases - in some places I wasn't returning a Content-length header and this caused the client browser to time-out on the requests which meant a 1.5s delay.
What I've learned is that, within certain limitations, it is possible to get a fast (and so far reliable) web server running on an MBED LPC1768. The handling of the 1024 buffer limit has been overcome somewhat by implementing the chunking mechanism on requests. This does require the application to handle a single request in multiple parts but with the Spidey Wall in mind this hasn't been a problem as each part of a longer request is discrete from the others.
The full MBED project code is published here on MBED.