]> git.mxchange.org Git - simgear.git/commitdiff
HTTP Client improvements
authorJames Turner <zakalawe@mac.com>
Tue, 25 Jun 2013 06:55:20 +0000 (07:55 +0100)
committerJames Turner <zakalawe@mac.com>
Wed, 18 Sep 2013 11:17:38 +0000 (12:17 +0100)
- max connections limit, and parallel connections to a single host where possible.
  (Will permit updating terrain and Models / Airports / data in parallel)
- add LGPL headers
- give HTTP::Client a private impl class, to keep header simple.

simgear/io/HTTPClient.cxx
simgear/io/HTTPClient.hxx

index ba68a94a4b0f60947cee8a47f6ff3c949b8e54d7..787b4facc73cc003b4f63da900921deeb86cee88 100644 (file)
@@ -1,3 +1,26 @@
+/**
+ * \file HTTPClient.cxx - simple HTTP client engine for SimHear
+ */
+
+// Written by James Turner
+//
+// Copyright (C) 2013  James Turner  <zakalawe@mac.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Library General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Library General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//
+
 #include "HTTPClient.hxx"
 
 #include <sstream>
@@ -5,6 +28,7 @@
 #include <list>
 #include <iostream>
 #include <errno.h>
+#include <map>
 
 #include <boost/foreach.hpp>
 #include <boost/algorithm/string/case_conv.hpp>
@@ -17,6 +41,7 @@
 #include <simgear/compiler.h>
 #include <simgear/debug/logstream.hxx>
 #include <simgear/timing/timestamp.hxx>
+#include <simgear/structure/exception.hxx>
 
 #if defined( HAVE_VERSION_H ) && HAVE_VERSION_H
 #include "version.h"
@@ -52,8 +77,26 @@ const int GZIP_HEADER_FEXTRA = 1 << 2;
 const int GZIP_HEADER_FNAME = 1 << 3;
 const int GZIP_HEADER_COMMENT = 1 << 4;
 const int GZIP_HEADER_CRC = 1 << 1;
-  
+
+class Connection;
+typedef std::multimap<std::string, Connection*> ConnectionDict;
 typedef std::list<Request_ptr> RequestList;
+
+class Client::ClientPrivate
+{
+public:
+    std::string userAgent;
+    std::string proxy;
+    int proxyPort;
+    std::string proxyAuth;
+    NetChannelPoller poller;
+    unsigned int maxConnections;
+    
+    RequestList pendingRequests;
+    
+// connections by host (potentially more than one)
+    ConnectionDict connections;
+};
   
 class Connection : public NetChat
 {
@@ -690,56 +733,128 @@ private:
     RequestList sentRequests;
 };
 
-Client::Client()
+Client::Client() :
+    d(new ClientPrivate)
 {
+    d->proxyPort = 0;
+    d->maxConnections = 4;
     setUserAgent("SimGear-" SG_STRINGIZE(SIMGEAR_VERSION));
 }
 
+Client::~Client()
+{
+}
+
+void Client::setMaxConnections(unsigned int maxCon)
+{
+    if (maxCon < 1) {
+        throw sg_range_exception("illegal HTTP::Client::setMaxConnections value");
+    }
+    
+    d->maxConnections = maxCon;
+}
+
 void Client::update(int waitTimeout)
 {
-    _poller.poll(waitTimeout);
-        
-    ConnectionDict::iterator it = _connections.begin();
-    for (; it != _connections.end(); ) {
-        if (it->second->hasIdleTimeout() || it->second->hasError() ||
-            it->second->hasErrorTimeout())
+    d->poller.poll(waitTimeout);
+    bool waitingRequests = !d->pendingRequests.empty();
+    
+    ConnectionDict::iterator it = d->connections.begin();
+    for (; it != d->connections.end(); ) {
+        Connection* con = it->second;
+        if (con->hasIdleTimeout() || 
+            con->hasError() ||
+            con->hasErrorTimeout() ||
+            (!con->isActive() && waitingRequests))
         {
         // connection has been idle for a while, clean it up
-        // (or has an error condition, again clean it up)
+        // (or if we have requests waiting for a different host,
+        // or an error condition
             ConnectionDict::iterator del = it++;
             delete del->second;
-            _connections.erase(del);
+            d->connections.erase(del);
         } else {
             if (it->second->shouldStartNext()) {
                 it->second->tryStartNextRequest();
             }
-            
             ++it;
         }
-    } // of connecion iteration
+    } // of connection iteration
+    
+    if (waitingRequests && (d->connections.size() < d->maxConnections)) {
+        RequestList waiting(d->pendingRequests);
+        d->pendingRequests.clear();
+        
+        // re-submit all waiting requests in order; this takes care of
+        // finding multiple pending items targetted to the same (new)
+        // connection
+        BOOST_FOREACH(Request_ptr req, waiting) {
+            makeRequest(req);
+        }
+    }
 }
 
 void Client::makeRequest(const Request_ptr& r)
 {
     string host = r->host();
     int port = r->port();
-    if (!_proxy.empty()) {
-        host = _proxy;
-        port = _proxyPort;
+    if (!d->proxy.empty()) {
+        host = d->proxy;
+        port = d->proxyPort;
     }
     
+    Connection* con = NULL;
     stringstream ss;
     ss << host << "-" << port;
     string connectionId = ss.str();
+    bool havePending = !d->pendingRequests.empty();
     
-    if (_connections.find(connectionId) == _connections.end()) {
-        Connection* con = new Connection(this);
+    // assign request to an existing Connection.
+    // various options exist here, examined in order
+    if (d->connections.size() >= d->maxConnections) {
+        ConnectionDict::iterator it = d->connections.find(connectionId);
+        if (it == d->connections.end()) {
+            // maximum number of connections active, queue this request
+            // when a connection goes inactive, we'll start this one
+            d->pendingRequests.push_back(r);
+            return;
+        }
+        
+        // scan for an idle Connection to the same host (likely if we're
+        // retrieving multiple resources from the same host in quick succession)
+        // if we have pending requests (waiting for a free Connection), then
+        // force new requests on this id to always use the first Connection
+        // (instead of the random selection below). This ensures that when
+        // there's pressure on the number of connections to keep alive, one
+        // host can't DoS every other.
+        int count = 0;
+        for (;it->first == connectionId; ++it, ++count) {
+            if (havePending || !it->second->isActive()) {
+                con = it->second;
+                break;
+            }
+        }
+        
+        if (!con) {
+            // we have at least one connection to the host, but they are
+            // all active - we need to pick one to queue the request on.
+            // we use random but round-robin would also work.
+            int index = random() % count;
+            for (it = d->connections.find(connectionId); index > 0; --index) { ; }
+            con = it->second;
+        }
+    } // of at max connections limit
+    
+    // allocate a new connection object
+    if (!con) {
+        con = new Connection(this);
         con->setServer(host, port);
-        _poller.addChannel(con);
-        _connections[connectionId] = con;
+        d->poller.addChannel(con);
+        d->connections.insert(d->connections.end(), 
+            ConnectionDict::value_type(connectionId, con));
     }
     
-    _connections[connectionId]->queueRequest(r);
+    con->queueRequest(r);
 }
 
 void Client::requestFinished(Connection* con)
@@ -749,20 +864,35 @@ void Client::requestFinished(Connection* con)
 
 void Client::setUserAgent(const string& ua)
 {
-    _userAgent = ua;
+    d->userAgent = ua;
+}
+
+const std::string& Client::userAgent() const
+{
+    return d->userAgent;
+}
+    
+const std::string& Client::proxyHost() const
+{
+    return d->proxy;
+}
+    
+const std::string& Client::proxyAuth() const
+{
+    return d->proxyAuth;
 }
 
 void Client::setProxy(const string& proxy, int port, const string& auth)
 {
-    _proxy = proxy;
-    _proxyPort = port;
-    _proxyAuth = auth;
+    d->proxy = proxy;
+    d->proxyPort = port;
+    d->proxyAuth = auth;
 }
 
 bool Client::hasActiveRequests() const
 {
-    ConnectionDict::const_iterator it = _connections.begin();
-    for (; it != _connections.end(); ++it) {
+    ConnectionDict::const_iterator it = d->connections.begin();
+    for (; it != d->connections.end(); ++it) {
         if (it->second->isActive()) return true;
     }
     
index f1bdf0d63efd18997a827c2e30523b602acb6a9c..530552cdf4f0ee9549dae761e937b880abf4da9a 100644 (file)
@@ -1,10 +1,30 @@
+/**
+ * \file HTTPClient.hxx - simple HTTP client engine for SimHear
+ */
+
+// Written by James Turner
+//
+// Copyright (C) 2013  James Turner  <zakalawe@mac.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Library General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Library General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//
+
 #ifndef SG_HTTP_CLIENT_HXX
 #define SG_HTTP_CLIENT_HXX
 
-#include <map>
-
 #include <simgear/io/HTTPRequest.hxx>
-#include <simgear/io/sg_netChannel.hxx>
 
 namespace simgear
 {
@@ -12,12 +32,14 @@ namespace simgear
 namespace HTTP
 {
 
+// forward decls
 class Connection;
-
+    
 class Client
 {
 public:
     Client();
+    ~Client();
     
     void update(int waitTimeout = 0);
     
@@ -26,14 +48,17 @@ public:
     void setUserAgent(const std::string& ua);
     void setProxy(const std::string& proxy, int port, const std::string& auth = "");
     
-    const std::string& userAgent() const
-        { return _userAgent; }
+    /**
+     * Specify the maximum permitted simultaneous connections
+     * (default value is 1)
+     */
+    void setMaxConnections(unsigned int maxCons);
+    
+    const std::string& userAgent() const;
         
-    const std::string& proxyHost() const
-        { return _proxy; }
+    const std::string& proxyHost() const;
         
-    const std::string& proxyAuth() const
-        { return _proxyAuth; }
+    const std::string& proxyAuth() const;
     
     /**
      * predicate, check if at least one connection is active, with at
@@ -46,15 +71,8 @@ private:
     friend class Connection;
     friend class Request;
     
-    std::string _userAgent;
-    std::string _proxy;
-    int _proxyPort;
-    std::string _proxyAuth;
-    NetChannelPoller _poller;
-    
-// connections by host
-    typedef std::map<std::string, Connection*> ConnectionDict;
-    ConnectionDict _connections;
+    class ClientPrivate;
+    std::auto_ptr<ClientPrivate> d;
 };
 
 } // of namespace HTTP