2009-09-07 14 views
10

Mam trudności z ustaleniem, czy istnieje sposób na rozwiązanie potencjalnych problemów z łącznością podczas korzystania z klasy HttpWebRequest .NET do wywoływania zdalnego serwera (w szczególności usługi sieciowej REST). Z moich badań zachowanie klasy WebClient jest takie samo, co jest nieco oczekiwane, ponieważ wydaje się, że oferuje tylko prostszy interfejs do HttpWebRequest.HttpWebRequest Jak obsłużyć (przedwczesne) zamknięcie podstawowego połączenia TCP?

Dla celów symulacji napisałem bardzo prosty serwer HTTP, który nie zachowuje się zgodnie ze specyfikacją HTTP 1.1 RFC. To, co robi, to akceptuje połączenie klienta, a następnie wysyła odpowiednie nagłówki HTTP 1.1 i "Hello World!" ładunek z powrotem do klienta i zamyka gniazdo, wątek przyjmowania połączeń klienckich po stronie serwera wygląda następująco:

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(); 
      } 
     } 
    } 

Ponieważ HTTP 1.1 RFC państw, które HTTP 1.1 domyślnie przechowuje połączenia żyje i że serwer musi wysłać nagłówek odpowiedzi "Połączenie: Zamknij", jeśli chce zamknąć połączenie, jest to nieoczekiwane zachowanie po stronie klienta. Klient używa HttpWebRequest w następujący sposób:

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(); 
     } 
    } 

Powyższa metoda jest w kolejce przez ThreadPool.QueueUserWorkItem (waitCallback, stateObject). ManualResetEvent służy do kontrolowania zachowania w kolejce, tak aby cała pula wątków nie została wypełniona zadaniami oczekującymi (ponieważ HttpWebRequest niejawnie używa wątków roboczych, ponieważ wewnętrznie funkcjonuje asynchronicznie w celu implementacji funkcji limitu czasu).

Problem z tym wszystkim polega na tym, że gdy wszystkie połączenia podstawowego ServicePointa HttpWebRequest zostaną "zużyte" (tj. Zamknięte przez serwer zdalny), nie zostaną otwarte żadne nowe. Nie ma również znaczenia, czy wartość ConnectionLeaseTimeout ServicePoint jest ustawiona na niską wartość (10 sekund). Gdy system wejdzie w ten stan, nie będzie działał poprawnie, ponieważ nie łączy się ponownie automatycznie i wszystkie kolejne HttpWebRequests upłyną. Teraz naprawdę chodzi o to, czy istnieje sposób na rozwiązanie tego problemu, niszcząc ServicePoint pod pewnymi warunkami lub zamykając połączenia underylingowe (nie miałem jeszcze szczęścia w ServicePoint.CloseConnectionGroup(), ale metoda ta jest również nieudokumentowana pod względem sposobu właściwie go używać).

Czy ktoś ma pojęcie, jak mogę podejść do tego problemu?

Odpowiedz

2

To straszne włamanie, ale działa. Dzwoń do niego okresowo, jeśli zauważysz, że twoje połączenia się blokują.

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

Oto moja propozycja. Nie testowałem tego. 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

Rozwiązanie wpadłem na podstawie niektórych tutejszych pomysłów jest zarządzanie połączeniami siebie. Jeśli do obiektu WebRequest zostanie przypisana unikalna nazwa grupy połączeń (na przykład Guid.NewGuid(). ToString()), nowa usługa połączenia z jednym połączeniem zostanie utworzona w ServicePoint dla żądania. Zauważ, że nie ma więcej ograniczeń połączeń w tym momencie, ponieważ ograniczenia .NET na grupę połączeń, a nie na ServicePoint, więc będziesz musiał sobie z tym poradzić. Będziesz chciał ponownie użyć grup połączeń, aby istniejące połączenia z KeepAlive były ponownie wykorzystywane, ale jeśli wystąpi wyjątek WebException, grupa połączeń żądania powinna zostać zniszczona, ponieważ może być nieaktualna. Coś takiego (utwórz nowe wystąpienie dla każdej nazwy hosta):

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(); 
     } 
    } 
} 
Powiązane problemy