2009-09-07 138 views
10

我很難弄清楚在使用.NET的HttpWebRequest類調用遠程服務器(特別是REST Web服務)時是否有辦法處理潛在的連接問題。從我的調查中,WebClient類的行爲是相同的,這是有點期待的,因爲它似乎只提供了一個更簡單的HttpWebRequest接口。HttpWebRequest如何處理(過早)關閉底層TCP連接?

爲了模擬目的,我編寫了一個非常簡單的HTTP服務器,該服務器的行爲不符合HTTP 1.1 RFC。它所做的是接受客戶端連接,然後發送適當的HTTP 1.1頭文件和「Hello World!」。有效載荷返回給客戶端,並關閉套接字,線程接受服務器端的客戶端連接,如下所示:

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

由於該HTTP 1.1默認情況下保持連接活着的HTTP 1.1 RFC狀態和服務器必須發送一個「Connection:Close」響應頭,如果它想關閉一個連接,這是客戶端的意外行爲。客戶端使用的HttpWebRequest以下列方式:

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

上述方法是通過ThreadPool.QueueUserWorkItem(waitCallback,stateObject)排隊。 ManualResetEvent用於控制排隊行爲,以免整個線程池被等待任務填滿(因爲HttpWebRequest隱式使用工作線程,因爲它在內部異步執行以實現超時功能)。

所有這一切的問題是,一旦HttpWebRequest的底層ServicePoint的所有連接都被「用完」(即由遠程服務器關閉),就不會有新的連接打開。如果ServicePoint的ConnectionLeaseTimeout設置爲較低值(10秒),也沒有關係。一旦系統進入此狀態,它將不再正常工作,因爲它不會自動重新連接,並且所有後續的HttpWebRequests都會超時。現在的問題是,如果有辦法通過在某些條件下銷燬ServicePoint或關閉解除綁定連接來解決這個問題(我沒有任何運用ServicePoint.CloseConnectionGroup()的方法,那麼該方法也沒有相應的文檔記錄正確使用它)。

有沒有人有任何想法我可以如何解決這個問題?

回答

2

這是一個可怕的黑客,但它的作品。如果您發現連接受阻,請定期撥打電話。

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

這是我的建議。我沒有測試過它。 阿爾特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

我想出了基於這裏的一些想法的解決方案是管理連接自己。如果將唯一的ConnectionGroupName分配給WebRequest(例如Guid.NewGuid()。ToString()),則將在ServicePoint中爲請求創建一個具有一個連接的新連接組。請注意,此時不再有連接限制,因爲.NET限制每個連接組而不是每個ServicePoint,所以您必須自己處理。您需要重用連接組,以便重新使用與KeepAlive的現有連接,但如果發生WebException異常,請求的連接組應該被銷燬,因爲它可能已過時。類似這樣(爲每個主機名創建一個新實例):

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(); 
     } 
    } 
} 
相關問題