Sans Pareil Technologies, Inc.

Key To Your Business

HttpClient



HttpClient is a simple class for HTTP interactions from an Arduino board. The native Arduino libraries provide EthernetClient and WiFiClient classes for building network client applications. Unfortunately, these are separate classes in different libraries and does not provide a common API for a sketch to perform network operations. The HttpClient class abstracts this difference and provides a uniform interface that can be used from sketches (the library does need to be initialised with the type of networking in use for the current board).

SHttpClient.h

Definition of the HttpClient class. The filename is prefixed with an “S” to avoid clashes with other classes in the Arduino suite of libraries.

    1: #ifndef SPT_NET_HTTPCLIENT_H
    2: #define SPT_NET_HTTPCLIENT_H
    3:
    4: #if defined( ARDUINO )
    5: #include "AutoPtr.h"
    6: #include "RefCountedObject.h"
    7: #include "HttpRequest.h"
    8: #include "../StandardCplusplus/map"
    9: #else
   10: #include <AutoPtr.h>
   11: #include <RefCountedObject.h>
   12: #include <net/HttpRequest.h>
   13: #include <map>
   14: #endif
   15:
   16: namespace spt
   17: {
   18:   /**
   19:    * Namespace for classes that provide network services and require
   20:    * a network connection to work.
   21:    */
   22:   namespace net
   23:   {
   24:     /**
   25:      * @brief A HTTP Client class for use with either ethernet or wifi.
   26:      * Before use, the client should be initialised with the network
   27:      * type used by the device ({@link spt::net::initNetworkType}.
   28:      */
   29:     class HttpClient : public RefCountedObject
   30:     {
   31:     public:
   32:       /// Type for auto pointer to a http client instance.
   33:       typedef AutoPtr<HttpClient> Ptr;
   34:
   35:       /// Default constructor.
   36:       HttpClient() : RefCountedObject() {}
   37:
   38:       /// Destructor for sub-classes
   39:       virtual ~HttpClient() {}
   40:
   41:       /**
   42:        * @brief Factory method for creating concrete instances based on initialisation.
   43:        * @return An instance that uses either ethernet or wifi to connect
   44:        *   to the network.  Callers must delete the returned instance.
   45:        */
   46:       static Ptr create();
   47:
   48:       /// Make a socket connection to the specified server on specified port (default 80)
   49:       virtual int16_t connect( const std::string& server, uint16_t port = 80 ) = 0;
   50:
   51:       /// Check to see if the client is connected to the server
   52:       virtual uint8_t connected() = 0;
   53:
   54:       /**
   55:        * @brief Perform a GET request using information in the request object.
   56:        * @param request The request object that encapsulates the uri and
   57:        *   other relevant information
   58:        * @return The HTTP response code from server.
   59:        */
   60:       virtual uint16_t get( const HttpRequest& request ) = 0;
   61:
   62:       /**
   63:        * @brief Perform a POST request using information in the request object.
   64:        * @param request The request object that encapsulates the uri and
   65:        *   other relevant information
   66:        * @return The HTTP response code from server.
   67:        */
   68:       virtual uint16_t post( const HttpRequest& request ) = 0;
   69:
   70:       /**
   71:        * @brief Read a line from the HTTP response.
   72:        *
   73:        * A line can be either a header or content.  Use to process raw
   74:        * HTTP response line by line.
   75:        * @return A line (content until newline character) of text from raw response.
   76:        */
   77:       virtual const std::string readLine() = 0;
   78:
   79:       /// Return a map of the HTTP response headers
   80:       virtual HttpRequest::Map readHeaders() = 0;
   81:
   82:       /**
   83:        * @brief Read the entire contents of the server response body.
   84:        * Note: This method also reads headers.  If headers have already
   85:        * been read, it may end up losing some of the response body.
   86:        *
   87:        * WARNING: Use with caution.  Can run embedded devices
   88:        * out of memory very easily.
   89:        *
   90:        * @return The entire http response body content.
   91:        */
   92:       virtual const std::string readBody() = 0;
   93:
   94:     protected:
   95:       /**
   96:        * @brief Send the specified request headers to the HTTP server.
   97:        *
   98:        * @param headers The map of headers to send to the server.
   99:        * @param close Flag indicating whether HTTP keep-alive is NOT to be used.
  100:        */
  101:       virtual void writeHeaders( const HttpRequest& request, bool close = true ) = 0;
  102:     };
  103:
  104:     /// Enumeration of network connection types for device
  105:     enum NetworkType { Ethernet = 0, WiFi = 1 };
  106:
  107:     /**
  108:      * Initialise the API to use the specified type.
  109:      * {@link HttpClient::create} uses this type to create appropriate
  110:      * implementation.
  111:      */
  112:     void initNetworkType( NetworkType type );
  113:
  114:   } // namespace net
  115: } // namespace spt
  116:
  117: #endif // SPT_NET_HTTPCLIENT_H



SHttpClient.cpp

Implementation of the HttpClient interface. The implementation is a templated sub-class of the standard EthernetClient or WiFiClient depending upon which type of networking is in use.

    1: #include "SHttpClient.h"
    2:
    3: #if defined( ARDUINO )
    4: #include "../StandardCplusplus/iostream"
    5: #include <EthernetClient.h>
    6: #include <WiFiClient.h>
    7: #else
    8: #include <iostream>
    9: #endif
   10:
   11: namespace spt
   12: {
   13:   namespace net
   14:   {
   15:     namespace data
   16:     {
   17:       bool networkTypeInitialised = false;
   18:       spt::net::NetworkType networkType;
   19:     }
   20:
   21:     template <typename C>
   22:     class HttpClientImpl : public HttpClient, C
   23:     {
   24:     public:
   25:       HttpClientImpl() : HttpClient(), C(), server() {}
   26:
   27:       int16_t connect( const std::string& srvr, uint16_t port )
   28:       {
   29:         server = srvr;
   30:         return C::connect( server.c_str(), port );
   31:       }
   32:
   33:       uint8_t connected() { return C::connected(); }
   34:
   35:       uint16_t get( const HttpRequest& request )
   36:       {
   37:         uint16_t status = 0;
   38:
   39:         C::print( F( "GET " ) );
   40:         C::print( request.getUri().c_str() );
   41:         const std::string& parameters = request.getParamters();
   42:         if ( parameters.size() > 0 )
   43:         {
   44:           C::print( F( "?") );
   45:           C::print( parameters.c_str() );
   46:         }
   47:         C::println( F( " HTTP/1.1" ) );
   48:
   49:         C::print( F( "Host: " ) );
   50:         C::println( server.c_str() );
   51:         writeHeaders( request );
   52:
   53:         if ( connected() )
   54:         {
   55:           const std::string& line = readLine();
   56:           if ( line.length() > 14 ) status =  atoi( line.substr( 9, 3 ).c_str() );
   57:         }
   58:
   59:         return status;
   60:       }
   61:
   62:       uint16_t post( const HttpRequest& request )
   63:       {
   64:         uint16_t status = 0;
   65:
   66:         C::print( F( "POST " ) );
   67:         C::print( request.getUri().c_str() );
   68:         C::println( F( " HTTP/1.1" ) );
   69:         C::print( F( "Host: " ) );
   70:         C::println( server.c_str() );
   71:
   72:         if ( request.getBody().size() > 0 )
   73:         {
   74:           C::print( F( "Content-Length: " ) );
   75:           C::println( request.getBody().length() );
   76:         }
   77:
   78:         writeHeaders( request );
   79:
   80:         const std::string& parameters = request.getParamters();
   81:         if ( parameters.size() > 0 ) C::println( parameters.c_str() );
   82:
   83:         if ( request.getBody().size() > 0 ) C::println( request.getBody().c_str() );
   84:         C::println();
   85:
   86:         if ( connected() )
   87:         {
   88:           const std::string& line = readLine();
   89:           if ( line.length() > 14 ) status =  atoi( line.substr( 9, 3 ).c_str() );
   90:         }
   91:
   92:         return status;
   93:       }
   94:
   95:
   96:       const std::string readLine()
   97:       {
   98:         std::string line;
   99:
  100:         while ( connected() )
  101:         {
  102:           int bytes = C::available();
  103:           int16_t c = ' ';
  104:
  105:           if ( bytes )
  106:           {
  107:             line.reserve( bytes );
  108:             c = C::timedRead();
  109:             while ( c >= 0 && c != '\n' )
  110:             {
  111:               line += static_cast<char>( c );
  112:               c = C::timedRead();
  113:             }
  114:           }
  115:
  116:           if ( c == '\n' ) break;
  117:         }
  118:
  119:         return line;
  120:       }
  121:
  122:
  123:       HttpRequest::Map readHeaders()
  124:       {
  125:         HttpRequest::Map m;
  126:
  127:         while ( connected() )
  128:         {
  129:           const std::string& line = readLine();
  130:
  131:           std::size_t found = line.find( ":" );
  132:           if ( found != std::string::npos )
  133:           {
  134:             const std::string& key = line.substr( 0, found );
  135:             const std::string& value = line.substr( found + 2 );
  136:             m.insert( std::pair<std::string,std::string>( key, value ) );
  137:           }
  138:
  139:           if ( line.length() <= 1 ) break;
  140:         }
  141:
  142:         return m;
  143:       }
  144:
  145:
  146:       const std::string readBody()
  147:       {
  148:         std::string content;
  149:
  150:         if ( connected() ) readHeaders();
  151:         while ( connected() )
  152:         {
  153:           const std::string& line = readLine();
  154:           if ( line.length() == 0 ) break;
  155:           content.append( line );
  156:         }
  157:
  158:         return content;
  159:       }
  160:
  161:
  162:     protected:
  163:       void writeHeaders( const HttpRequest& request, bool close = true )
  164:       {
  165:         for ( HttpRequest::Iterator iter = request.beginHeaders();
  166:             iter != request.endHeaders(); ++iter )
  167:         {
  168:           C::print( iter->first.c_str() );
  169:           C::print( ": " );
  170:           C::println( iter->second.c_str() );
  171:         }
  172:
  173:         if ( close ) C::println( F( "Connection: close" ) );
  174:         C::println();
  175:       }
  176:
  177:
  178:     private:
  179:       std::string server;
  180:     };
  181:   }
  182: }
  183:
  184: using std::string;
  185: using spt::net::HttpClient;
  186:
  187:
  188: HttpClient::Ptr HttpClient::create()
  189: {
  190:   switch ( spt::net::data::networkType )
  191:   {
  192:     case spt::net::Ethernet:
  193:       return new spt::net::HttpClientImpl<EthernetClient>;
  194:       break;
  195:     case spt::net::WiFi:
  196:       return new spt::net::HttpClientImpl<WiFiClient>;
  197:       break;
  198:     default: return Ptr();
  199:   }
  200: }
  201:
  202:
  203: void spt::net::initNetworkType( spt::net::NetworkType type )
  204: {
  205:   if ( ! spt::net::data::networkTypeInitialised )
  206:   {
  207:     spt::net::data::networkType = type;
  208:     spt::net::data::networkTypeInitialised = true;
  209:   }
  210: }



HttpClientTest.cpp

Unit test suite for the HttpClient class.

    1: #if defined( ARDUINO )
    2: #include "tut.hpp"
    3: #include "SPT.h"
    4: #include "SHttpClient.h"
    5: #else
    6: #include <SPT.h>
    7: #include <tut/tut.hpp>
    8: #include <net/SHttpClient.h>
    9: #endif
   10:
   11: using spt::net::HttpClient;
   12: using spt::net::HttpRequest;
   13:
   14:
   15: namespace tut
   16: {
   17:   struct HttpClientTestData
   18:   {
   19:     const std::string server = "sptci.com";
   20:     HttpClient::Ptr client = HttpClient::create();
   21:   };
   22:
   23:   typedef test_group<HttpClientTestData> HttpClientTestGroup;
   24:   typedef HttpClientTestGroup::object HttpClientTest;
   25:   HttpClientTestGroup httpClientTestGroup( "HttpClient test suite" );
   26:
   27:
   28:   template<>
   29:   template<>
   30:   void HttpClientTest::test<1>()
   31:   {
   32:     set_test_name( "connect" );
   33:     ensure( "[1] Unable to connect", client->connect( server ) );
   34:     client->disconnect();
   35:     std::cout << F( "Free SRAM: " ) << spt::freeRam() << std::endl;
   36:   }
   37:
   38:
   39:   template<>
   40:   template<>
   41:   void HttpClientTest::test<2>()
   42:   {
   43:     set_test_name( "get" );
   44:
   45:     if ( client->connect( server ) )
   46:     {
   47:       HttpRequest request( "/" );
   48:       request.setHeader( "User-Agent", "SPT Library" );
   49:
   50:       ensure( "[2] response code not 200", client->get( request ) == uint16_t( 200 ) );
   51:       client->disconnect();
   52:     }
   53:     else fail( "[2] Unable to connect" );
   54:     std::cout << F( "Free SRAM: " ) << spt::freeRam() << std::endl;
   55:   }
   56:
   57:
   58:   template<>
   59:   template<>
   60:   void HttpClientTest::test<3>()
   61:   {
   62:     set_test_name( "getWithParameters" );
   63:
   64:     if ( client->connect( "ixquick.com" ) )
   65:     {
   66:       HttpRequest request( "/do/search" );
   67:       request.setParameter( "query", "arduino" );
   68:       request.setParameter( "cat", "web" );
   69:       request.setParameter( "language", "english" );
   70:
   71:       ensure( "[3] response code not 301", client->get( request ) == uint16_t( 301 ) );
   72:
   73:       client->readHeaders();
   74:       const HttpRequest::Map& headers = client->readHeaders();
   75:       ensure( "[3] No headers received", headers.size() > 0 );
   76:       ensure( "[3] no bytes available to read", client->available() > 0 );
   77:
   78:       uint64_t length = uint64_t( 0 );
   79:       while ( client->connected() )
   80:       {
   81:         const std::string& line = client->readLine();
   82:         if ( line.size() == 0 ) break;
   83:         length += uint64_t( line.length() );
   84:       }
   85:       ensure( "[3] No body received", length > 0 );
   86:
   87:       client->disconnect();
   88:     }
   89:     else fail( "[3] Unable to connect" );
   90:     std::cout << F( "Free SRAM: " ) << spt::freeRam() << std::endl;
   91:   }
   92:
   93:
   94:   template<>
   95:   template<>
   96:   void HttpClientTest::test<4>()
   97:   {
   98:     set_test_name( "post" );
   99:
  100:     if ( client->connect( server ) )
  101:     {
  102:       HttpRequest request( "/service/json/search" );
  103:       request.setParameter( "term", "bson" );
  104:       request.setParameter( "offset", "0" );
  105:       request.setParameter( "pageSize", "10" );
  106:
  107:       ensure( "[4] response code not 200", client->post( request ) == uint16_t( 200 ) );
  108:
  109:       const HttpRequest::Map& headers = client->readHeaders();
  110:       ensure( "[4] No headers received", headers.size() > 0 );
  111:       ensure( "[4] No content type header", headers.find( "Content-Type" ) != headers.end() );
  112:       const std::string type( "application/json" );
  113:       const std::string& htype = headers.find( "Content-Type" )->second;
  114:       ensure( "[4] Content type header invalid", type == htype );
  115:
  116:       int16_t contentLength = spt::fromString<int16_t>(
  117:           headers.find( "Content-Length" )->second );
  118:       ensure( "[4] Content length invalid", contentLength > 0 );
  119:
  120:       int16_t length = 0;
  121:       while ( client->connected() )
  122:       {
  123:         const std::string& line = client->readLine();
  124:         if ( line.size() == 0 ) break;
  125:         length += line.size() + 1;
  126:       }
  127:       ensure( "[4] No body received", length > 0 );
  128:       ensure( "[4] content length header not same as body length", contentLength == length );
  129:
  130:       client->disconnect();
  131:     }
  132:     else fail( "[4] Unable to connect" );
  133:     std::cout << F( "Free SRAM: " ) << spt::freeRam() << std::endl;
  134:   }
  135: }