2009-09-07 14 views
10

Es fällt mir schwer herauszufinden, ob es eine Möglichkeit gibt, mit möglichen Konnektivitätsproblemen umzugehen, wenn .NETs HttpWebRequest-Klasse verwendet wird, um einen Remote-Server (speziell einen REST-Webdienst) aufzurufen. Aus meinen Untersuchungen ist das Verhalten der WebClient-Klasse das gleiche, was etwas erwartet wird, da es scheinbar nur eine einfachere Schnittstelle zu dem HttpWebRequest bietet.HttpWebRequest Wie behandelt (vorzeitige) Schließung der zugrunde liegenden TCP-Verbindung?

Zu Simulationszwecken habe ich einen sehr einfachen HTTP-Server geschrieben, der sich nicht nach dem HTTP 1.1-RFC verhält. Was es tut ist, dass es eine Client-Verbindung akzeptiert, dann sendet es entsprechende HTTP 1.1-Header und eine "Hallo Welt!" Nutzlast an den Client zurück und schließt die Buchse, der Thread-Client-Verbindungen auf der Serverseite zu akzeptieren sieht wie folgt aus:

private const string m_defaultResponse = "<html><body><h1>Hello World!</h1></body></html>"; 
    private void Listen() 
    { 
     while (true) 
     { 
      using (TcpClient clientConnection = m_listener.AcceptTcpClient()) 
      { 
       NetworkStream stream = clientConnection.GetStream(); 
       StringBuilder httpData = new StringBuilder("HTTP/1.1 200 OK\r\nServer: ivy\r\nContent-Type: text/html\r\n"); 
       httpData.AppendFormat("Content-Length: {0}\r\n\r\n", m_defaultResponse.Length); 
       httpData.AppendFormat(m_defaultResponse); 

       Thread.Sleep(3000); // Sleep to simulate latency 

       stream.Write(Encoding.ASCII.GetBytes(httpData.ToString()), 0, httpData.Length); 

       stream.Close(); 

       clientConnection.Close(); 
      } 
     } 
    } 

Da die HTTP 1.1 RFC Zustände, die HTTP 1.1 von Standardverbindungen alive und dass ein Server hält muss senden ein Antwortheader "Verbindung: Schließen", wenn er eine Verbindung schließen möchte, ist dies ein unerwartetes Verhalten für die Clientseite. Der Client verwendet HttpWebRequest in folgenden Weise:

private static void SendRequest(object _state) 
    { 
     WebResponse resp = null; 

     try 
     { 
      HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://192.168.0.32:7070/asdasd"); 
      request.Timeout = 50 * 1000; 

      DateTime requestStart = DateTime.Now; 
      resp = request.GetResponse(); 
      TimeSpan requestDuration = DateTime.Now - requestStart; 

      Console.WriteLine("OK. Request took: " + (int)requestDuration.TotalMilliseconds + " ms."); 
     } 
     catch (WebException ex) 
     { 
      if (ex.Status == WebExceptionStatus.Timeout) 
      { 
       Console.WriteLine("Timeout occurred"); 
      } 
      else 
      { 
       Console.WriteLine(ex); 
      } 
     } 
     finally 
     { 
      if (resp != null) 
      { 
       resp.Close(); 
      } 

      ((ManualResetEvent)_state).Set(); 
     } 
    } 

Das obige Verfahren über ThreadPool.QueueUserWorkItem (waitCallback, stateobject) ansteht. Das ManualResetEvent wird verwendet, um das Warteschlangenverhalten zu steuern, so dass nicht der gesamte Thread-Pool mit wartenden Tasks gefüllt wird (da die HttpWebRequest Worker-Threads implizit verwendet, da sie intern asynchron zur Implementierung der Timeout-Funktionalität funktioniert).

Das Problem bei all dem ist, dass, sobald alle Verbindungen der zugrunde liegenden Servicepoint des HttpWebRequest werden „verbraucht“ (das heißt von dem Remote-Server geschlossen), wird es keine neue erschlossen werden. Es spielt auch keine Rolle, ob ConnectionLeaseTimeout des ServicePoint auf einen niedrigen Wert (10 Sekunden) gesetzt ist. Sobald das System in diesen Zustand versetzt wird, funktioniert es nicht mehr ordnungsgemäß, da es sich nicht automatisch wieder verbindet und alle nachfolgenden HttpWebRequests eine Zeitüberschreitung aufweisen. Jetzt ist die Frage wirklich, ob es einen Weg gibt, dies zu lösen, indem man unter bestimmten Bedingungen einen ServicePoint irgendwie zerstört oder untergeordnete Verbindungen schließt (Ich hatte mit ServicePoint.CloseConnectionGroup() noch kein Glück, aber die Methode ist auch ziemlich undokumentiert um es richtig zu benutzen).

Hat jemand eine Idee, wie ich dieses Problem angehen könnte?

Antwort

2

Dies ist ein schrecklicher Hack, aber es funktioniert. Rufen Sie es regelmäßig auf, wenn Sie bemerken, dass Ihre Verbindungen hängen bleiben.

static public void SetIdle(object request) 
    { 
     MethodInfo getConnectionGroupLine = request.GetType().GetMethod("GetConnectionGroupLine", BindingFlags.Instance | BindingFlags.NonPublic); 
     string connectionName = (string)getConnectionGroupLine.Invoke(request, null); 

     ServicePoint servicePoint = ((HttpWebRequest)request).ServicePoint; 
     MethodInfo findConnectionGroup = servicePoint.GetType().GetMethod("FindConnectionGroup", BindingFlags.Instance | BindingFlags.NonPublic); 
     object connectionGroup; 
     lock (servicePoint) 
     { 
      connectionGroup = findConnectionGroup.Invoke(servicePoint, new object[] { connectionName, false }); 
     } 

     PropertyInfo currentConnections = connectionGroup.GetType().GetProperty("CurrentConnections", BindingFlags.Instance | BindingFlags.NonPublic); 
     PropertyInfo connectionLimit = connectionGroup.GetType().GetProperty("ConnectionLimit", BindingFlags.Instance | BindingFlags.NonPublic); 

     MethodInfo disableKeepAliveOnConnections = connectionGroup.GetType().GetMethod("DisableKeepAliveOnConnections", BindingFlags.Instance | BindingFlags.NonPublic); 

     if (((int)currentConnections.GetValue(connectionGroup, null)) == 
      ((int)connectionLimit.GetValue(connectionGroup, null))) 
     { 
      disableKeepAliveOnConnections.Invoke(connectionGroup, null); 
     } 

     MethodInfo connectionGoneIdle = connectionGroup.GetType().GetMethod("ConnectionGoneIdle", BindingFlags.Instance | BindingFlags.NonPublic); 
     connectionGoneIdle.Invoke(connectionGroup, null); 
    } 
1

Hier ist mein Vorschlag. Ich habe es nicht getestet. Alter Reference.cs

protected override WebResponse GetWebResponse(WebRequest request) 
    { 
     try 
     { 
      return base.GetWebResponse(request); 
     } 
     catch (WebException) 
     { 
      HttpWebRequest httpWebRequest = request as HttpWebRequest; 
      if (httpWebRequest != null && httpWebRequest.ServicePoint != null) 
       httpWebRequest.ServicePoint.CloseConnectionGroup(httpWebRequest.ConnectionGroupName); 

      throw; 
     } 
    } 
6

Die Lösung kam ich mit basierend auf einige der Ideen, die hier ist die Verbindungen selbst zu verwalten. Wenn einem WebRequest ein eindeutiger ConnectionGroupName zugewiesen wird (z. B. Guid.NewGuid(). ToString()), wird eine neue Verbindungsgruppe mit einer Verbindung im ServicePoint für die Anforderung erstellt. Beachten Sie, dass zu diesem Zeitpunkt keine Beschränkung der Verbindung mehr besteht, da .NET Limits pro Verbindungsgruppe und nicht pro ServicePoint gelten. Sie müssen also selbst damit umgehen. Sie sollten Verbindungsgruppen erneut verwenden, damit vorhandene Verbindungen mit KeepAlive wiederverwendet werden. Wenn jedoch eine WebException-Ausnahme auftritt, sollte die Verbindungsgruppe der Anforderung zerstört werden, da sie möglicherweise veraltet ist. In etwa so (Erstellen Sie für jeden Hostnamen eine neue Instanz):

public class ConnectionManager { 
    private const int _maxConnections = 4; 

    private Semaphore _semaphore = new Semaphore(_maxConnections, _maxConnections); 
    private Stack<string> _groupNames = new Stack<string>(); 

    public string ObtainConnectionGroupName() { 
     _semaphore.WaitOne(); 
     return GetConnectionGroupName(); 
    } 

    public void ReleaseConnectionGroupName(string name) { 
     lock (_groupNames) { 
      _groupNames.Push(name); 
     } 
     _semaphore.Release(); 
    } 

    public string SwapForFreshConnection(string name, Uri uri) { 
     ServicePoint servicePoint = ServicePointManager.FindServicePoint(uri); 
     servicePoint.CloseConnectionGroup(name); 
     return GetConnectionGroupName(); 
    } 

    private string GetConnectionGroupName() { 
     lock (_groupNames) { 
      return _groupNames.Count != 0 ? _groupNames.Pop() : Guid.NewGuid().ToString(); 
     } 
    } 
} 
Verwandte Themen