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.
#ifndef SPT_NET_HTTPCLIENT_H #define SPT_NET_HTTPCLIENT_H #if defined( ARDUINO ) #include "AutoPtr.h" #include "RefCountedObject.h" #include "HttpRequest.h" #include "../StandardCplusplus/map" #else #include <AutoPtr.h> #include <RefCountedObject.h> #include <net/HttpRequest.h> #include <map> #endif namespace spt { /** * Namespace for classes that provide network services and require * a network connection to work. */ namespace net { /** * @brief A HTTP Client class for use with either ethernet or wifi. * Before use, the client should be initialised with the network * type used by the device ({@link spt::net::initNetworkType}. */ class HttpClient : public RefCountedObject { public: /// Type for auto pointer to a http client instance. typedef AutoPtr<HttpClient> Ptr; /// Default constructor. HttpClient() : RefCountedObject() {} /// Destructor for sub-classes virtual ~HttpClient() {} /** * @brief Factory method for creating concrete instances based on initialisation. * @return An instance that uses either ethernet or wifi to connect * to the network. Callers must delete the returned instance. */ static Ptr create(); /// Make a socket connection to the specified server on specified port (default 80) virtual int16_t connect( const std::string& server, uint16_t port = 80 ) = 0; /// Check to see if the client is connected to the server virtual uint8_t connected() = 0; /** * @brief Perform a GET request using information in the request object. * @param request The request object that encapsulates the uri and * other relevant information * @return The HTTP response code from server. */ virtual uint16_t get( const HttpRequest& request ) = 0; /** * @brief Perform a POST request using information in the request object. * @param request The request object that encapsulates the uri and * other relevant information * @return The HTTP response code from server. */ virtual uint16_t post( const HttpRequest& request ) = 0; /** * @brief Read a line from the HTTP response. * * A line can be either a header or content. Use to process raw * HTTP response line by line. * @return A line (content until newline character) of text from raw response. */ virtual const std::string readLine() = 0; /// Return a map of the HTTP response headers virtual HttpRequest::Map readHeaders() = 0; /** * @brief Read the entire contents of the server response body. * Note: This method also reads headers. If headers have already * been read, it may end up losing some of the response body. * * WARNING: Use with caution. Can run embedded devices * out of memory very easily. * * @return The entire http response body content. */ virtual const std::string readBody() = 0; protected: /** * @brief Send the specified request headers to the HTTP server. * * @param headers The map of headers to send to the server. * @param close Flag indicating whether HTTP keep-alive is NOT to be used. */ virtual void writeHeaders( const HttpRequest& request, bool close = true ) = 0; }; /// Enumeration of network connection types for device enum NetworkType { Ethernet = 0, WiFi = 1 }; /** * Initialise the API to use the specified type. * {@link HttpClient::create} uses this type to create appropriate * implementation. */ void initNetworkType( NetworkType type ); } // namespace net } // namespace spt #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.
#include "SHttpClient.h" #if defined( ARDUINO ) #include "../StandardCplusplus/iostream" #include <EthernetClient.h> #include <WiFiClient.h> #else #include <iostream> #endif namespace spt { namespace net { namespace data { bool networkTypeInitialised = false; spt::net::NetworkType networkType; } template <typename C> class HttpClientImpl : public HttpClient, C { public: HttpClientImpl() : HttpClient(), C(), server() {} int16_t connect( const std::string& srvr, uint16_t port ) { server = srvr; return C::connect( server.c_str(), port ); } uint8_t connected() { return C::connected(); } uint16_t get( const HttpRequest& request ) { uint16_t status = 0; C::print( F( "GET " ) ); C::print( request.getUri().c_str() ); const std::string& parameters = request.getParamters(); if ( parameters.size() > 0 ) { C::print( F( "?") ); C::print( parameters.c_str() ); } C::println( F( " HTTP/1.1" ) ); C::print( F( "Host: " ) ); C::println( server.c_str() ); writeHeaders( request ); if ( connected() ) { const std::string& line = readLine(); if ( line.length() > 14 ) status = atoi( line.substr( 9, 3 ).c_str() ); } return status; } uint16_t post( const HttpRequest& request ) { uint16_t status = 0; C::print( F( "POST " ) ); C::print( request.getUri().c_str() ); C::println( F( " HTTP/1.1" ) ); C::print( F( "Host: " ) ); C::println( server.c_str() ); if ( request.getBody().size() > 0 ) { C::print( F( "Content-Length: " ) ); C::println( request.getBody().length() ); } writeHeaders( request ); const std::string& parameters = request.getParamters(); if ( parameters.size() > 0 ) C::println( parameters.c_str() ); if ( request.getBody().size() > 0 ) C::println( request.getBody().c_str() ); C::println(); if ( connected() ) { const std::string& line = readLine(); if ( line.length() > 14 ) status = atoi( line.substr( 9, 3 ).c_str() ); } return status; } const std::string readLine() { std::string line; while ( connected() ) { int bytes = C::available(); int16_t c = ' '; if ( bytes ) { line.reserve( bytes ); c = C::timedRead(); while ( c >= 0 && c != '\n' ) { line += static_cast<char>( c ); c = C::timedRead(); } } if ( c == '\n' ) break; } return line; } HttpRequest::Map readHeaders() { HttpRequest::Map m; while ( connected() ) { const std::string& line = readLine(); std::size_t found = line.find( ":" ); if ( found != std::string::npos ) { const std::string& key = line.substr( 0, found ); const std::string& value = line.substr( found + 2 ); m.insert( std::pair<std::string,std::string>( key, value ) ); } if ( line.length() <= 1 ) break; } return m; } const std::string readBody() { std::string content; if ( connected() ) readHeaders(); while ( connected() ) { const std::string& line = readLine(); if ( line.length() == 0 ) break; content.append( line ); } return content; } protected: void writeHeaders( const HttpRequest& request, bool close = true ) { for ( HttpRequest::Iterator iter = request.beginHeaders(); iter != request.endHeaders(); ++iter ) { C::print( iter->first.c_str() ); C::print( ": " ); C::println( iter->second.c_str() ); } if ( close ) C::println( F( "Connection: close" ) ); C::println(); } private: std::string server; }; } } using std::string; using spt::net::HttpClient; HttpClient::Ptr HttpClient::create() { switch ( spt::net::data::networkType ) { case spt::net::Ethernet: return new spt::net::HttpClientImpl<EthernetClient>; break; case spt::net::WiFi: return new spt::net::HttpClientImpl<WiFiClient>; break; default: return Ptr(); } } void spt::net::initNetworkType( spt::net::NetworkType type ) { if ( ! spt::net::data::networkTypeInitialised ) { spt::net::data::networkType = type; spt::net::data::networkTypeInitialised = true; } }
HttpClientTest.cpp
Unit test suite for the HttpClient class.
#if defined( ARDUINO ) #include "tut.hpp" #include "SPT.h" #include "SHttpClient.h" #else #include <SPT.h> #include <tut/tut.hpp> #include <net/SHttpClient.h> #endif using spt::net::HttpClient; using spt::net::HttpRequest; namespace tut { struct HttpClientTestData { const std::string server = "sptci.com"; HttpClient::Ptr client = HttpClient::create(); }; typedef test_group<HttpClientTestData> HttpClientTestGroup; typedef HttpClientTestGroup::object HttpClientTest; HttpClientTestGroup httpClientTestGroup( "HttpClient test suite" ); template<> template<> void HttpClientTest::test<1>() { set_test_name( "connect" ); ensure( "[1] Unable to connect", client->connect( server ) ); client->disconnect(); std::cout << F( "Free SRAM: " ) << spt::freeRam() << std::endl; } template<> template<> void HttpClientTest::test<2>() { set_test_name( "get" ); if ( client->connect( server ) ) { HttpRequest request( "/" ); request.setHeader( "User-Agent", "SPT Library" ); ensure( "[2] response code not 200", client->get( request ) == uint16_t( 200 ) ); client->disconnect(); } else fail( "[2] Unable to connect" ); std::cout << F( "Free SRAM: " ) << spt::freeRam() << std::endl; } template<> template<> void HttpClientTest::test<3>() { set_test_name( "getWithParameters" ); if ( client->connect( "ixquick.com" ) ) { HttpRequest request( "/do/search" ); request.setParameter( "query", "arduino" ); request.setParameter( "cat", "web" ); request.setParameter( "language", "english" ); ensure( "[3] response code not 301", client->get( request ) == uint16_t( 301 ) ); client->readHeaders(); const HttpRequest::Map& headers = client->readHeaders(); ensure( "[3] No headers received", headers.size() > 0 ); ensure( "[3] no bytes available to read", client->available() > 0 ); uint64_t length = uint64_t( 0 ); while ( client->connected() ) { const std::string& line = client->readLine(); if ( line.size() == 0 ) break; length += uint64_t( line.length() ); } ensure( "[3] No body received", length > 0 ); client->disconnect(); } else fail( "[3] Unable to connect" ); std::cout << F( "Free SRAM: " ) << spt::freeRam() << std::endl; } template<> template<> void HttpClientTest::test<4>() { set_test_name( "post" ); if ( client->connect( server ) ) { HttpRequest request( "/service/json/search" ); request.setParameter( "term", "bson" ); request.setParameter( "offset", "0" ); request.setParameter( "pageSize", "10" ); ensure( "[4] response code not 200", client->post( request ) == uint16_t( 200 ) ); const HttpRequest::Map& headers = client->readHeaders(); ensure( "[4] No headers received", headers.size() > 0 ); ensure( "[4] No content type header", headers.find( "Content-Type" ) != headers.end() ); const std::string type( "application/json" ); const std::string& htype = headers.find( "Content-Type" )->second; ensure( "[4] Content type header invalid", type == htype ); int16_t contentLength = spt::fromString<int16_t>( headers.find( "Content-Length" )->second ); ensure( "[4] Content length invalid", contentLength > 0 ); int16_t length = 0; while ( client->connected() ) { const std::string& line = client->readLine(); if ( line.size() == 0 ) break; length += line.size() + 1; } ensure( "[4] No body received", length > 0 ); ensure( "[4] content length header not same as body length", contentLength == length ); client->disconnect(); } else fail( "[4] Unable to connect" ); std::cout << F( "Free SRAM: " ) << spt::freeRam() << std::endl; } }
Users must initialise the type of network used in the setup of the application. This is not elegant, however we have not been able to find a programmatic way of determining that information using the Arduino API.
// Invoke this in setup if you are using ethernet shield for networking void initEthernet() { if ( Ethernet.begin( mac ) == 0 ) { std::cout << F( "Failed to configure Ethernet using DHCP" ) << std::endl; Ethernet.begin( mac, ip ); } // give the Ethernet shield a second to initialize: std::cout << F( "Connecting to network..." ) << std::endl; delay( 1000 ); // Initialise network API to use Ethernet spt::net::initNetworkType( spt::net::Ethernet ); }