Research completion date: 2007-10-14

HttpWebRequest and unreliable connections

The problems

By using the HttpWebRequest object on an unreliable GPRS connection, the request may block unless the GPRS connection is terminated. This problem is more frequent if the request spawns a GPRS connection and is also more frequent on some PocketPC Phone models with a weak reception.

The reason

Due to development reasons, the blocking behaviour has not been investigated but is reasonably caused by the high packet loss. The HttpWebRequest object expects a defined quantity of data and the reception of just a portion of such data, causes this blocking behaviour.

The solution

Luckily, the request is not OS-blocking, so it is possible to bypass the problem by using threads. Obviously it's not possible to retrieve the lost data, but is possible to bypass the blocking behaviour and instead return null.

Example

The following C# sourcecode illustrate how to use threads to force a file download inside a defined timespan.

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