.NET Zone is brought to you in partnership with:

Den is a DZone Zone Leader and has posted 460 posts at DZone. You can read more from them at their website. View Full User Profile

Sending Large Files Through Sockets on Windows Phone 8 and Windows 8

05.31.2013
| 3361 views |
  • submit to reddit

Using sockets to send files is not that uncommon. As a matter of fact, if you ask any of the Windows Phone developers, this would probably be their first choice when it comes to sending binary content to another device. So what exactly is the problem with the standard approach?

It is much more subtle than you think. Think in the context of a standard socket harness based on StreamSocket (if you need help setting it up, I highly recommend using this sample). Try sending a 5KB file. No problem. How about 1MB? Also, no hiccups. Now try sending a 500MB+ file. Ooops. You got an exception informing you that you're out of memory.

Here's the problem - when you are sending a file with the help of StreamSocket, its contents are  stored in the memory in the form of cached allocations. That is, as you are sending the data byte-by-byte to the target device, you are also expecting a local cache from which the data is pulled. With that in mind, the cache is not being cleared automatically, so you will have a constant memory allocation increase until the data is sent completely. You can see now that given the sandbox application restrictions, your app will either be terminated by the OS, or you will get an exception before that happens on low-perf devices.

There is a solution to this, too, and thanks to the unified network stack, it applies both to Windows Phone 8 and Windows 8 (read: Windows Store) apps. What you need to do is send the data by reading only the necessary parts of the file stream. For example, let's assume that I want to send a StorageFile. I start by getting the size:

 

internal async void Send(object stFile, string fileName)
{
    DataWriter writer = new DataWriter(_socket.OutputStream);

    // Write the potential file name first.
    writer.WriteUInt16((ushort)fileName.Length);
    writer.WriteString(fileName);

    // Write the length of the binary data that is being
    // sent to the client.
    streamSize = (uint)(await (stFile as StorageFile).OpenStreamForReadAsync()).Length;

    writer.WriteUInt32((uint)streamSize);
    Store(writer, fileName, stFile, fileType, albumName);
}
Here, streamSize is a variable that simply holds the uint value of the current stream size - you can declare it either inside the function or outside of it for convenience purposes.

 

The storage mechanism can be implemented in a simple recursive pattern, where we have a fixed-length data buffer that is being populated from the file stream, exactly at the level that we need, that will not cause memory allocation problems. Since we already have the stream size, we can use the allocation to shift the position in the file stream and read the same amount of bytes continuously until everything reaches the destination.

private async void Store(DataWriter writer, string fileName, object stFile)
{
    if (StackDownloadViewModel.Instance.IsBusy)
    {
        Stream stream;

        stream = await (stFile as StorageFile).OpenStreamForReadAsync();

        using (stream)
        {
            int len = 0;
            stream.Position = streamPosition;

            long memAlloc = stream.Length - streamPosition < BUFFER_LENGTH ? stream.Length - streamPosition : BUFFER_LENGTH;
            byte[] buffer = new byte[memAlloc];

            while (writer.UnstoredBufferLength < memAlloc)
            {
                len = stream.Read(buffer, 0, buffer.Length);
                if (len > 0)
                {
                    writer.WriteBytes(buffer);
                    streamPosition += len;
                }
            }

            try
            {
                await writer.StoreAsync();
            }
            catch
            {
                Debug.WriteLine(string.Format("Failed to store {0} bytes.", writer.UnstoredBufferLength));
            }
        }

        // There is a leak somewhere that causes the stored stream
        // to be cached instead of being properly disposed.
        GC.Collect();

        if (streamPosition < streamSize)
        {
            Store(writer, fileName, stFile, fileType, albumName);
        }
        else
        {
            writer.Dispose();
        }
    }
}

And just like that, you can send large files between devices without having out of memory exceptions.