HttpWebRequest Как обрабатывать (преждевременное) закрытие базового TCP-соединения?

Мне сложно определить, есть ли способ справиться с потенциальными проблемами подключения при использовании класса .NET HttpWebRequest для вызова удаленного сервера (в частности, веб-службы REST). Из моих исследований поведение класса WebClient является одним и тем же, что несколько ожидалось, поскольку он, кажется, предлагает более простой интерфейс для HttpWebRequest.

В целях моделирования я написал очень простой 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 RFC заявляет, что HTTP 1.1 по умолчанию сохраняет соединения живыми и сервер должен отправить заголовок ответа "Соединение: Закрыть", если он хочет закрыть соединение, это непредвиденное поведение для клиентской стороны. Клиент использует 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, "израсходованы" (т.е. закрыты удаленным сервером), новых открытий не будет. Также не имеет значения, установлено ли ConnectionLeaseTimeout из ServicePoint на низкое значение (10 секунд). Как только система попадет в это состояние, она перестанет функционировать должным образом, потому что она не будет автоматически подключаться автоматически, и все последующие HttpWebRequests будут отключены. Теперь на самом деле вопрос заключается в том, есть ли способ решить эту проблему, как-то уничтожить ServicePoint при определенных условиях или закрыть подключающиеся соединения (мне еще не повезло с ServicePoint.CloseConnectionGroup(), этот метод также довольно не документирован с точки зрения того, как для правильного использования).

Кто-нибудь знает, как я мог бы подойти к этой проблеме?

Ответ 1

Решение, которое я придумал, основывается на некоторых идеях здесь, чтобы управлять своими соединениями. Если уникальному 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();
        }
    }
}

Ответ 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);
    }

Ответ 3

Вот мое предложение. Я его не тестировал. 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;
        }
    }