11 votes

HttpWebRequest Comment gérer la fermeture (prématurée) de la connexion TCP sous-jacente?

Je rencontre des difficultés à déterminer s'il existe un moyen de gérer les problèmes de connectivité potentiels lors de l'utilisation de la classe HttpWebRequest de .NET pour appeler un serveur distant (en particulier un service web REST). D'après mes investigations, le comportement de la classe WebClient est le même, ce qui est quelque peu attendu puisqu'elle semble offrir simplement une interface plus simple à HttpWebRequest.

À des fins de simulation, j'ai écrit un serveur HTTP très simple qui ne se comporte pas conformément au RFC HTTP 1.1. Il accepte une connexion client, envoie ensuite des en-têtes HTTP 1.1 appropriés et une charge utile "Hello World!" au client, puis ferme le socket. Le thread acceptant les connexions client du côté serveur ressemble à ceci :

    private const string m_defaultResponse = "Hello World!";
    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();
            }
        }
    }

Étant donné que le RFC HTTP 1.1 indique que HTTP 1.1 conserve par défaut les connexions actives et qu'un serveur doit envoyer un en-tête de réponse "Connection: Close" s'il souhaite fermer une connexion, ce comportement est inattendu du côté client. Le client utilise HttpWebRequest de la manière suivante :

    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. La requête a pris : " + (int)requestDuration.TotalMilliseconds + " ms.");
        }
        catch (WebException ex)
        {
            if (ex.Status == WebExceptionStatus.Timeout)
            {
                Console.WriteLine("Un dépassement de délai s'est produit");
            }
            else
            {
                Console.WriteLine(ex);
            }
        }
        finally
        {
            if (resp != null)
            {
                resp.Close();
            }

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

La méthode ci-dessus est mise en file d'attente via ThreadPool.QueueUserWorkItem(waitCallback, stateObject). Le ManualResetEvent est utilisé pour contrôler le comportement de mise en file d'attente afin que l'ensemble du pool de threads ne soit pas saturé de tâches en attente (puisque HttpWebRequest utilise implicitement des threads de travail car il fonctionne de manière asynchrone en interne pour mettre en œuvre la fonctionnalité de délai d'attente).

Le problème avec tout cela est qu'une fois que toutes les connexions du ServicePoint sous-jacent de HttpWebRequest sont "utilisées" (c'est-à-dire fermées par le serveur distant), aucune nouvelle connexion n'est ouverte. Il n'est pas non plus important que le ConnectionLeaseTimeout du ServicePoint soit réglé sur une valeur faible (10 secondes). Une fois que le système se retrouve dans cet état, il ne fonctionnera plus correctement car il ne se reconnecte pas automatiquement et toutes les HttpWebRequests ultérieures expireront. Maintenant, la vraie question est de savoir s'il existe un moyen de résoudre ce problème en détruisant d'une certaine manière un ServicePoint dans certaines conditions ou en fermant les connexions sous-jacentes (je n'ai pas eu de chance avec ServicePoint.CloseConnectionGroup() pour l'instant, la méthode est également assez peu documentée en termes d'utilisation correcte).

Quelqu'un a-t-il une idée de comment je pourrais aborder ce problème ?

6voto

J.D. Points 1026

La solution que j'ai trouvée sur la base de certaines des idées ici est de gérer moi-même les connexions. Si un ConnectionGroupName unique est assigné à une WebRequest (par exemple Guid.NewGuid().ToString()), un nouveau groupe de connexion avec une seule connexion sera créé dans le ServicePoint pour la requête. Notez qu'il n'y a plus de limitation de connexion à ce stade, car .NET limite par groupe de connexion plutôt que par ServicePoint, vous devrez donc le gérer vous-même. Vous voudrez réutiliser des groupes de connexion afin de réutiliser les connexions existantes avec KeepAlive, mais si une exception de type WebException se produit, le groupe de connexion de la demande devrait être détruit car il pourrait être obsolète. Quelque chose comme ceci (créez une nouvelle instance pour chaque nom d'hôte):

public class ConnectionManager {
    private const int _maxConnections = 4;

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

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

2voto

MB. Points 21

C'est une horrible astuce, mais ça marche. Appelez-la périodiquement si vous remarquez que vos connexions restent bloquées.

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

1voto

Ryan Williams Points 359

Voici ma suggestion. Je ne l'ai pas testée. Modifier 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;
        }
    }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X