Data completamento ricerca: 2007-10-14

HttpWebRequest e connessioni inaffidabili

Il problema

Usando l'oggetto HttpWebRequest con una connessione GPRS, se questa non è particolarmente robusta la richiesta a volte si blocca finchè la connessione GPRS non viene terminata. Il problema è più evidente se la richiesta innesca una connessione GPRS e su alcuni modelli di PocketPC Phone con una ricezione non particolarmente brillante.

Le cause

Per esigenze di sviluppo le cause non sono state approfondite ma sono probabilmente da ricercare nel packet loss. L'oggetto HttpWebRequest si aspetta una determinata quantità di dati e la ricezione di solo una porzione di dati ne provoca il blocco.

La soluzione

La richiesta fortunatamente non è bloccante a livello di sistema operativo, per cui è possibile ovviare in modo abbastanza semplice al problema ricorrendo all'uso dei thread. Non è possibile fare in modo che i dati vengano recuperati tutti, ma è possibile fare in modo che la richiesta non causi un blocco a tempo indefinito e che ritorni invece null.

Esempio

Il codice C# che segue illustra come utilizzare i thread per vincolare il download di un file a un tempo ben definito.

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Threading;
using System.IO;

namespace Updater
{
    /// <summary>
    /// This class enable you to perform request that can be
    /// interrupted after a specified number of milliseconds,
    /// even in case of packet loss (which usually prevents
    /// the request to terminate).
    /// It's especially useful for unstable connections, like
    /// GPRS connections.
    /// If a request cannot be completed, the Request or
    /// RequestBinary method returns null.
    /// </summary>
    public class TimedWebRequest
    {
        #region Constants
        public const int DefaultBufferSize = 1024;
        public const int DefaultMaxSize = 1048576;
        #endregion

        #region Private members
        private int m_BufferSize = DefaultBufferSize;
        private int m_MaxSize = DefaultMaxSize;
        private object locker = new object();

        #region Private properties. Used to perform locking
        private byte[] bRetval = null;
        private byte[] ByteRetval
        {
            get
            {
                lock (locker)
                    return bRetval;
            }
            set
            {
                lock (locker)
                    bRetval = value;
            }
        }

        private string sAddress = null;
        private string Address
        {
            get
            {
                lock (locker)
                    return sAddress;
            }
            set
            {
                lock (locker)
                    sAddress = value;
            }
        }

        private int iCur = 0;
        private int Current
        {
            get
            {
                lock (locker)
                    return iCur;
            }
            set
            {
                lock (locker)
                    iCur = value;
            }
        }

        private int iTot = 0;
        private int Total
        {
            get
            {
                lock (locker)
                    return iTot;
            }
            set
            {
                lock (locker)
                    iTot = value;
            }
        }

        private bool bComplete = true;
        private bool IsComplete()
        {
            lock (locker)
                return bComplete;
        }
        #endregion

        private void SetComplete(bool bValue)
        {
            lock (locker)
                bComplete = bValue;
        }

        private void threadFuncB()
        {
            // Signal that the request is not complete.
            SetComplete(false);
            // Performs the request.
            HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(Address);
            myRequest.KeepAlive = false;
            try
            {
                // Read the response.
                byte[] buffer = new byte[BufferSize];
                HttpWebResponse myResponse = (HttpWebResponse)myRequest.GetResponse();
                Stream stream = myResponse.GetResponseStream();
                long len = myResponse.ContentLength;
                if (len <= 0)
                    len = MaxSize;
                long total = 0;
                List<byte> data = new List<byte>(65536);
                Current = 0;
                Total = (int)len;
                while (total < len)
                {
                    System.Threading.Thread.Sleep(20);
                    int rbytes = stream.Read(buffer, 0, BufferSize);
                    for (int i = 0; i < rbytes; i++)
                        data.Add(buffer[i]);
                    total += rbytes;
                    Current = (int)total;
                    Total = (int)len;
                    if (total > MaxSize && rbytes == 0)
                        break;
                }
                ByteRetval = data.ToArray();
            }
            catch
            {
                try
                {
                    myRequest.Abort();
                }
                catch
                {
                }
                ByteRetval = null;
            }
            finally
            {
                SetComplete(true);
            }
        }
        #endregion

        #region public members
        public delegate void DgtDownloadProgress(int iCurrent, int iTotal);

        /// <summary>
        /// You can use this event to keep track of the download progress.
        /// </summary>
        public event DgtDownloadProgress DownloadProgress;

        /// <summary>
        /// Specify the buffer size for downloading.
        /// Smaller buffer means more frequent progress updates.
        /// Greater buffer means better download speed on fast connections.
        /// </summary>
        public int BufferSize
        {
            get { lock (locker) return m_BufferSize; }
            set { lock (locker) m_BufferSize = value; }
        }

        /// <summary>
        /// Specify the Maximum download size if the HTML header itself does not specify the content-length.
        /// </summary>
        public int MaxSize
        {
            get { lock (locker) return m_MaxSize; }
            set { lock (locker) m_MaxSize = value; }
        }

        /// <summary>
        /// Request a text file from the specified url.
        /// </summary>
        /// <param name="sURL">The url from which download data.</param>
        /// <param name="timeout">The number of milliseconds before the request must be terminated.</param>
        /// <returns>A string containing the page data.</returns>
        public string Request(string sURL, int timeout)
        {
            byte[] result = RequestBinary(sURL, timeout);
            if (result != null)
            {
                MemoryStream ms = new MemoryStream(result);
                StreamReader rdr = new StreamReader(ms);
                return rdr.ReadToEnd();
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Request binary data from the specified url.
        /// </summary>
        /// <param name="sURL">The url from which download data.</param>
        /// <param name="timeout">The number of milliseconds before the request must be terminated.</param>
        /// <returns>A byte array containing the requested data.</returns>
        public byte[] RequestBinary(string sURL, int timeout)
        {
            int sleeptime = 150;
            ByteRetval = null;
            Address = sURL;
            Thread th = new Thread(new ThreadStart(threadFuncB));
            // Wait until previous request completes
            while (IsComplete() == false)
                System.Threading.Thread.Sleep(150);
            SetComplete(false);
            // Start reading thread
            th.Start();
            int time = 0;
            // Read until completion or until timeout is reached.
            while (IsComplete() == false)
            {
                th.Join(sleeptime);
                lock (locker)
                {
                    if (Current > Total)
                        Current = Total;
                    if (DownloadProgress != null)
                        DownloadProgress(Current, Total);
                }
                time += sleeptime;
                if (time > timeout)
                    th.Abort();
            }
            th = null;
            return ByteRetval;
        }

        /// <summary>
        /// Request a text file from the specified url, using a timeout of 90 seconds.
        /// </summary>
        /// <param name="sURL">The url from which download data.</param>
        /// <returns>A string containing the page data.</returns>
        public string Request(string sURL)
        {
            return Request(sURL, 90000);
        }

        /// <summary>
        /// Request binary data from the specified url using a timeout of 90 seconds.
        /// </summary>
        /// <param name="sURL">The url from which download data.</param>
        /// <returns>A byte array containing the requested data.</returns>
        public byte[] RequestBinary(string sURL)
        {
            return RequestBinary(sURL, 90000);
        }
        #endregion
    }
}