A multi clients web server

This forum is about you. Feel free to discuss anything is related to embedded and electronics, your awesome projects, your ideas, your announcements, not necessarily related to ChibiOS but to embedded in general. This forum is NOT for support.
Post Reply
gallegojm
Posts: 42
Joined: Mon Apr 06, 2015 2:02 am

A multi clients web server

Post by gallegojm »

My goal is to explore the possibilities of a server running on an Olimex STM32-E407 board with a SD card able to serve several clients at a time.

The function responsible for analyzing html queries is very simplified and recognizes only the phrase GET /<filename> where filename is a text file (html) or a jpg or ico. Images are important as they take a lot of time for transmitting and give me time to see how the program reacts.
A special request is "GET /status" that display a short page with the status of each http web threads: number, phase and name of last file. So you can open 192.168.1.20/status in a window to see what appends.

My main difficulty is to assemble threads, priorities, connection buffers and good configuration of lwip and fatfs.
You have to modify _FS_SHARE defined in ffconf.h and MEMP_NUM_NETCONN defined in lwipopts.h (see web/web.h for explanation)

It works, but become slow when accessing two or more images from the same or different browsers.
I would appreciate testing and ideas on how to better the speed.
I am afraid that the problem is inherent to the memory card that become slow when trying to read multiple files at the same time.
Or perhaps a parameter that I have to change in the configuration files.

To test, I save a few pictures on the memory card and then create html files like:

Code: Select all

<html>
<h2>Thank you for testing!</h2>
<img src="image1.jpg" alt="image1.jpg" width="320" />
<img src="image2.jpg" alt="image2.jpg" width="320" />
</html>

Runs on an Olimex STM32-E407 board.
Build with ChibiStudio (chibios 2.6.7)
I publish the entire code at Github and put here the 3 most important files:

main.c

Code: Select all

#include "ch.h"
#include "hal.h"

#include "lwipthread.h"
#include "ff.h"
#include "web/web.h"

//===========================================================================
//   FatFs related
//===========================================================================

//  FS object.
static FATFS SDC_FS;

//===========================================================================
//   Green LED blinker thread
//===========================================================================

static WORKING_AREA( waThread1, 128 );

static msg_t Thread1( void *arg )
{
  (void )arg;
  chRegSetThreadName( "blinker" );
  while( TRUE )
  {
    palTogglePad( GPIOC, GPIOC_LED );
    chThdSleepMilliseconds( 500 );
  }
  return RDY_OK;
}


//===========================================================================
//   Main  code
//===========================================================================

int main( void )
{
  halInit();
  chSysInit();

  //  Mount the SD card
  sdcStart( &SDCD1, NULL );
  sdcConnect( &SDCD1 );
  f_mount( 0, & SDC_FS );

  //  Creates the blinker thread.
  chThdCreateStatic( waThread1, sizeof( waThread1 ), NORMALPRIO, Thread1, NULL );

  //  Creates the LWIP threads (it changes priority internally).
  chThdCreateStatic( wa_lwip_thread, LWIP_THREAD_STACK_SIZE, NORMALPRIO + 3,
                     lwip_thread, NULL );

  // Initialize web server
  http_server();

   //  Normal main() thread activity
  while( TRUE )
  {
    chThdSleepMilliseconds( 1000 );
  }
}


web.h

Code: Select all

#ifndef _WEB_H_
#define _WEB_H_

#define WEB_THREAD_PORT           80

// number of clients we want to serve simultaneously
#define WEB_NBR_CLIENTS           8
#if WEB_NBR_CLIENTS > _FS_SHARE
  #error "_FS_SHARE defined in ffconf.h must be >= WEB_NBR_CLIENTS"
#endif
#if WEB_NBR_CLIENTS + 1 > MEMP_NUM_NETCONN
  #error "MEMP_NUM_NETCONN defined in lwipopts.h must be >= WEB_NBR_CLIENTS + 1"
#endif

// size of file buffer for reading a file
#define SD_BUF_SIZE               512

// max length of a file name
#define FNAME_LEN                 32

#define WEB_THREAD_STACK_SIZE     (1024 + SD_BUF_SIZE)

#define WEB_THREAD_PRIORITY       (LOWPRIO + 2)

// define a structure of parameters for an http web thread
struct server_stru
{
  uint8_t num;
  uint32_t fase;
  char fname[ FNAME_LEN ];
};

extern WORKING_AREA( wa_http_web[ WEB_NBR_CLIENTS ], WEB_THREAD_STACK_SIZE );
extern struct server_stru ss[ WEB_NBR_CLIENTS ];

#ifdef __cplusplus
extern "C" {
#endif
  msg_t http_web( void *p );
  bool http_server( void );
#ifdef __cplusplus
}
#endif

#endif // _WEB_H_


web.c

Code: Select all

#include <string.h>
#include "ch.h"
#include "lwip/opt.h"
#include "lwip/arch.h"
#include "lwip/api.h"
#include "ff.h"
#include "web.h"

//  Stack areas for the http web threads.
WORKING_AREA( wa_http_web[ WEB_NBR_CLIENTS ], WEB_THREAD_STACK_SIZE );

//  array of parameters for each http web thread
struct server_stru ss[ WEB_NBR_CLIENTS ];

// structure for connection listening for clients
struct netconn *srvconn;

static const char h1[] = "HTTP/1.1 200 OK\r\nContent-type: ";
static const char h2i[] = "image/x-icon";
static const char h2t[] = "text\r\n";
static const char h3[] = "/html\r\n";
static const char h4[] = "Content-Length: ";
static const char h5[] = "\r\n";
static const char h6[] = "Connection: close\r\n\r\n";

static const char h10[] = "HTTP/1.1 200 OK\r\nContent-type: text\r\n\r\n";
static const char h11[] = "<html><h2>Server status</h2>";
static const char h12[] = "<meta http-equiv=\"refresh\" content=\"1\">";
static const char h13[] = " ";
static const char h14[] = "<br>";
static const char h15[] = "</html>\r\n\n";

static const char h20[] = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n";
static const char h21[] = "<html><h2><center>Error: 404 Not Found: ";
static const char h22[] = "</center></h2></html>\r\n\n";

// Convert an integer to string
//
// parameters:
//   s: string where the conversion is made (must be large enough
//   i: integer t convert
//   z: size of returned string (leading space filled with '0'
//
// return pointer to string

char * i2strZ( char * s, uint32_t i, uint8_t z )
{
  char * psi = s + ( z > 0 ? z : sizeof( s ));

  * -- psi = 0;
  if( i == 0 )
    * -- psi = '0';
  for( ; i; i /= 10 )
    * -- psi = '0' + i% 10;
  if( z > 0 )
    while( psi > s )
      * -- psi = '0';
  return psi;
}

// Very incomplete http server routine

void http_server_serve( struct netconn * newconn, uint8_t tnum )
{
  struct netbuf *inbuf;
  char *buf;
  u16_t buflen;
  err_t err;
  u16_t i;
  char  c;
  FIL   file;
  u32_t filesize;
  u8_t  rbuf[ SD_BUF_SIZE ];           // data buffer for reading the file
  u16_t nb;
  char  fname[ FNAME_LEN ];
  char  str[ 15 ];
  char * pstr;
  char * pext;

  // Read the data from the port, waiting 2 s if nothing yet there.
  // We assume the request (the part we care about) is in one netbuf
  netconn_set_recvtimeout( newconn, 2000 );
  err = netconn_recv( newconn, &inbuf );

  if( err == ERR_OK )
  {
    ss[ tnum ].fase = 100;
    // Get a pointer on the data and his length
    netbuf_data( inbuf, (void **) & buf, & buflen );

    // Check for HTTP GET command
    if( ! strncmp( buf, "GET /", 5 ))
    {
      // read next word
      i = 0;
      while( i < FNAME_LEN )
      {
        c = buf[ i + 5 ];
        if( c == ' ' )
          break;
        fname[ i ++ ] = c;
      }
      fname[ i ] = 0;
      // Discard the rest of received data
      netbuf_delete( inbuf );
      // Is it special command 'status' ?
      if( ! strcmp( fname, "status" ) )
      {
        // Send a status of all http web threads
        netconn_write( newconn, h10, sizeof( h10 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h11, sizeof( h11 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h12, sizeof( h12 ) - 1, NETCONN_NOCOPY );
        for( i = 0; i < WEB_NBR_CLIENTS; i ++ )
        {
          // number of the thread
          pstr = i2strZ( str, i, 0 );
          netconn_write( newconn, pstr, strlen( pstr ), NETCONN_COPY );
          netconn_write( newconn, h13, sizeof( h13 ) - 1, NETCONN_NOCOPY );
          // phase of the thread
          pstr = i2strZ( str, ss[ i ].fase, 0 );
          netconn_write( newconn, pstr, strlen( pstr ), NETCONN_COPY );
          netconn_write( newconn, h13, sizeof( h13 ) - 1, NETCONN_NOCOPY );
          // name of last file opened by the thread
          netconn_write( newconn, ss[ i ].fname, strlen( ss[ i ].fname ), NETCONN_COPY );
          netconn_write( newconn, h14, sizeof( h14 ) - 1, NETCONN_NOCOPY );
        }
        netconn_write( newconn, h15, sizeof( h15 ) - 1, NETCONN_NOCOPY );
      }
      // If we can open the file...
      else if( f_open( & file, fname, FA_READ ) == FR_OK )
      {
        strcpy( ss[ tnum ].fname, fname );
        filesize = f_size( & file );
        pstr = i2strZ( str, filesize, 0 );
        pext = strrchr( fname, '.' );

        netconn_write( newconn, h1, sizeof( h1 ) - 1, NETCONN_NOCOPY );
        if( ! strcmp( pext, ".jpg" ) || ! strcmp( pext, ".ico" ) )
          netconn_write( newconn, h2i, sizeof( h2i ) - 1, NETCONN_NOCOPY );
        else
          netconn_write( newconn, h2t, sizeof( h2t ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h3, sizeof( h3 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h4, sizeof( h4 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, pstr, strlen( pstr ), NETCONN_COPY );
        netconn_write( newconn, h5, sizeof( h5 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h6, sizeof( h6 ) - 1, NETCONN_NOCOPY );

        while( f_read( & file, rbuf, SD_BUF_SIZE, (UINT *) & nb ) == FR_OK && nb > 0 )
        {
          netconn_write( newconn, rbuf, nb, NETCONN_COPY );
          ss[ tnum ].fase ++;
        }

        f_close( & file );
        ss[ tnum ].fase = 10;
      }
      // file does not exists
      else
      {
        netconn_write( newconn, h20, sizeof( h20 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h21, sizeof( h21 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, fname, strlen( fname ), NETCONN_COPY );
        netconn_write( newconn, h22, sizeof( h22 ) - 1, NETCONN_NOCOPY );
      }
    }
    else
      netbuf_delete( inbuf );
  }
  // Give the web browser time to receive the data
  chThdSleepMilliseconds( 100 );
  // Close the connection
  netconn_close( newconn );
}

//  HTTP web thread.

msg_t http_web( void * p )
{
  struct server_stru * ss = p;
  struct netconn *webconn;
  char tn[] = "http_web_n";

  // names thread as http_web_1, http_web_2, ...
  tn[ strlen( tn ) - 1 ] = ss->num + '1';
  chRegSetThreadName( tn );

  while( TRUE )
  {
    if( netconn_accept( srvconn, & webconn ) == ERR_OK ) // checks for new client and create a connection to serve it...
    {
      ss->fase = 1;
      http_server_serve( webconn, ss->num );   // ... call the http function...
      ss->fase = 2;
      netconn_delete( webconn );      // ... delete the connection.
      ss->fase = 0;
    }
  }
  return RDY_OK;
}

//  HTTP server initialization.

bool http_server( void )
{
  uint8_t i;

  // Create a TCP connection handle
  srvconn = netconn_new( NETCONN_TCP );
  LWIP_ERROR( "http_server: invalid srvconn", (srvconn != NULL), return false; );

  // Bind to port 80 (HTTP) with default IP address
  netconn_bind( srvconn, NULL, WEB_THREAD_PORT );

  // Put the connection into LISTEN state
  netconn_listen( srvconn );

  //  Initialize web thread' parameters for each thread
  for( i = 0; i < WEB_NBR_CLIENTS; i ++ )
  {
    ss[ i ].num = i;
    ss[ i ].fase = 0;
    ss[ i ].fname[ 0 ] = 0;
  }

  //  Creates the HTTP Web threads
  for( i = 0; i < WEB_NBR_CLIENTS; i ++ )
    chThdCreateStatic( wa_http_web[ i ], sizeof( wa_http_web[ i ] ),
                       WEB_THREAD_PRIORITY, http_web, & ss[ i ] );
  return true;
}
User avatar
Giovanni
Site Admin
Posts: 14891
Joined: Wed May 27, 2009 8:48 am
Has thanked: 1202 times
Been thanked: 996 times

Re: A multi clients web server

Post by Giovanni »

Hi,

Ideally you would have a "pool of threads" that are activated in order to serve HTTP transactions. This should ensure maximum performance. The thread parameter would be a pointer to a transaction descriptor structure.

The thread, would start, serve the transaction on a socket and then terminate returning into the pool.

Giovanni
gallegojm
Posts: 42
Joined: Mon Apr 06, 2015 2:02 am

Re: A multi clients web server

Post by gallegojm »

This is my new version of a multi clients web server.
It runs under Chibios 3
The slowness of first version is corrected.

The entire code is at GitHub

It is build from the demo RT-STM32F407-OLIMEX_E407-LWIP-FATFS-USB, so I would describe there, in a simplified version, what I have done.

As I do not see in the forum any other example of aray of threads, I think this can help more neophyte users than me and I would appreciate if more experimented users than me can correct any error or suggest a better methode.

I have to modify some parameters.
In ffconf.h

Code: Select all

#define _FS_REENTRANT     1     /* 0:Disable or 1:Enable */


In lwipopts.h

Code: Select all

#define MEM_SIZE          6400
#define MEMP_NUM_TCP_PCB  8     /must be >= WEB_NBR_CLIENTS
#define MEMP_NUM_NETBUF   8     /must be >= WEB_NBR_CLIENTS
#define MEMP_NUM_NETCONN  9     /must be >= (WEB_NBR_CLIENTS + 1)
#define LWIP_SO_RCVTIMEO  1

WEB_NBR_CLIENTS is the number of clients we want to serve simultaneously
and is defined in web.h

MEM_SIZE (the size of the heap memory for lwip) is the parameter that has the major impact on speed when there is more than one thread transmitting.
I do not know if there is a method to calculate the best value.
I just compare the rate when transmitting a single connection with the sum of the rates of simultaneous connections.
With the value of 6400, I can load simultaneously 4 images without a decrease of the sum of the rates.

The simplified web.h file:

Code: Select all

#define WEB_THREAD_PORT           80
// number of clients we want to serve simultaneously
#define WEB_NBR_CLIENTS           8
// size of file buffer for reading a file
#define SD_BUF_SIZE               512
// max length of a file name
#define FNAME_LEN                 32
#define SERVER_THREAD_STACK_SIZE  256
#define WEB_THREAD_STACK_SIZE     ( 896 + SD_BUF_SIZE + FNAME_LEN )
#define WEB_THREAD_PRIORITY       (LOWPRIO + 2)
// define a structure of parameters for an http web thread
struct server_stru
{
  struct netconn *webconn;
  binary_semaphore_t semrequest;
};
extern THD_WORKING_AREA( wa_http_server, SERVER_THREAD_STACK_SIZE );
#ifdef __cplusplus
extern "C" {
#endif
  THD_FUNCTION( http_server, p );
#ifdef __cplusplus
}
#endif


and the simplified web.c file:
the includes:

Code: Select all

#include <string.h>
#include "ch.h"
#include "lwip/opt.h"
#include "lwip/arch.h"
#include "lwip/api.h"
#include "ff.h"
#include "web.h"


the global variables:

Code: Select all

//  Stack area for the http server thread.
THD_WORKING_AREA( wa_http_server, SERVER_THREAD_STACK_SIZE );
//  array of stack areas for the http web threads.
THD_WORKING_AREA( wa_http_web[ WEB_NBR_CLIENTS ], WEB_THREAD_STACK_SIZE );
//  array of parameters for each http web thread
struct server_stru ss[ WEB_NBR_CLIENTS ];


the HTTP server thread:

Code: Select all

THD_FUNCTION( http_server, p )
{
  struct netconn *srvconn;
  uint8_t i;
  (void ) p;

  //  Initialize web thread' parameters for each thread
  for( i = 0; i < WEB_NBR_CLIENTS; i ++ )
  {
    ss[ i ].webconn = NULL;
    chBSemObjectInit( & ss[ i ].semrequest, true );
  }

  //  Creates the HTTP Web threads
  for( i = 0; i < WEB_NBR_CLIENTS; i ++ )
    chThdCreateStatic( wa_http_web[ i ], sizeof( wa_http_web[ i ] ),
                       WEB_THREAD_PRIORITY, http_web, & ss[ i ] );

  // Create a TCP connection handle
  srvconn = netconn_new( NETCONN_TCP );
  LWIP_ERROR( "http_server: invalid srvconn", (srvconn != NULL), return; );

  // Bind to port 80 (HTTP) with default IP address
  netconn_bind( srvconn, NULL, WEB_THREAD_PORT );

  // Put the connection into LISTEN state
  netconn_listen( srvconn );

  //  Goes to the final priority after initialization
  chThdSetPriority( WEB_THREAD_PRIORITY );

  // Listen for a new client request, cyclically on each connection
  while( true )
  {
    for( i = 0; i < WEB_NBR_CLIENTS; i ++ ) // Look for the first connection not used
      if( ss[ i ].webconn == NULL )
        break;

    if( i == WEB_NBR_CLIENTS )              // All connections occupied
      chThdSleepMilliseconds( 300 );
    else if( netconn_accept( srvconn, & ss[ i ].webconn ) == ERR_OK ) // wait for a new client request
      chBSemSignal( & ss[ i ].semrequest ); // wake up the corresponding thread
  }
}


the HTTP web thread:

Code: Select all

THD_FUNCTION( http_web, p )
{
  struct server_stru * pss = p;

  while( true )
  {
    // wait to listen to new client
    if( chBSemWait( & pss->semrequest ) == MSG_OK )
    {
      http_server_serve( pss->webconn );  // call the http function
      netconn_delete( pss->webconn );     // delete the connexion.
      pss->webconn = NULL;
    }
  }
}


server_stru is a structure including the two parameters passed by the http server thread to each web thread.
This structure include a semaphore that wake up the corresponding web thread when a new request occurs.

I have to insert a delay in the loop of the http server thread in the case all connections are used.
Without this delay, all web threads stop working (they must have the same priority).
As the led continues to blink, I suppose that the "for" loop responsible for looking for an unused connection don't let web threads time to become active.
But I do not understand while a chThdYield() is not sufficient instead of the chThdSleepMilliseconds().

the simplified http server routine (not important for the multithreading' point of view):

Code: Select all

static const char h1[] = "HTTP/1.1 200 OK\r\nContent-type: ";
static const char h2i[] = "image/x-icon";
static const char h2t[] = "text\r\n";
static const char h3[] = "/html\r\n";
static const char h6[] = "Connection: close\r\n\r\n";

static const char h20[] = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n";
static const char h21[] = "<html><h2><center>Error: 404 Not Found: ";
static const char h22[] = "</center></h2></html>\r\n\n";

void http_server_serve( struct netconn * newconn )
{
  struct netbuf *inbuf;
  char *buf;
  u16_t buflen;
  err_t err;
  u16_t i;
  char  c;
  FIL   file;
  u8_t  rbuf[ SD_BUF_SIZE ];           // data buffer for reading the file
  u16_t nb;
  char  fname[ FNAME_LEN ];
  char * pext;

  // Read the data from the port, waiting 2 s if nothing yet there.
  // We assume the request (the part we care about) is in one netbuf
  netconn_set_recvtimeout( newconn, 2000 );
  err = netconn_recv( newconn, &inbuf );

  if( err == ERR_OK )
  {
    // Get a pointer on the data and his length
    netbuf_data( inbuf, (void **) & buf, & buflen );

    // Check for HTTP GET command
    if( ! strncmp( buf, "GET /", 5 ))
    {
      // read next word
      i = 0;
      while( i < FNAME_LEN )
      {
        c = buf[ i + 5 ];
        if( c == ' ' || c == 0 )
          break;
        fname[ i ++ ] = c;
      }
      fname[ i ] = 0;
      if( strlen( fname ) == 0 )
        strcpy( fname, "index.html" );
      // Discard the rest of received data
      netbuf_delete( inbuf );

      // If we can open the file...
      if( f_open( & file, fname, FA_READ ) == FR_OK )
      {
        pext = strrchr( fname, '.' );

        netconn_write( newconn, h1, sizeof( h1 ) - 1, NETCONN_NOCOPY );
        if( ! strcmp( pext, ".jpg" ) || ! strcmp( pext, ".ico" ) )
          netconn_write( newconn, h2i, sizeof( h2i ) - 1, NETCONN_NOCOPY );
        else
          netconn_write( newconn, h2t, sizeof( h2t ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h3, sizeof( h3 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h6, sizeof( h6 ) - 1, NETCONN_NOCOPY );

        while( f_read( & file, rbuf, SD_BUF_SIZE, (UINT *) & nb ) == FR_OK && nb > 0 )
          netconn_write( newconn, rbuf, nb, NETCONN_COPY );

        f_close( & file );
      }

      // file does not exists
      else
      {
        netconn_write( newconn, h20, sizeof( h20 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, h21, sizeof( h21 ) - 1, NETCONN_NOCOPY );
        netconn_write( newconn, fname, strlen( fname ), NETCONN_COPY );
        netconn_write( newconn, h22, sizeof( h22 ) - 1, NETCONN_NOCOPY );
      }
    }
    else
      netbuf_delete( inbuf );
  }
  // Give the web browser time to receive the data
  chThdSleepMilliseconds( 100 );
  // Close the connection
  netconn_close( newconn );
}

The complete code, with the "status" function is at GitHub
User avatar
Giovanni
Site Admin
Posts: 14891
Joined: Wed May 27, 2009 8:48 am
Has thanked: 1202 times
Been thanked: 996 times

Re: A multi clients web server

Post by Giovanni »

Hi,

You could use a "memory pool" object in order to store the working areas. The protection with the semaphore is OK. Threads would be created using chThdCreateFromPool().

Giovanni
Post Reply