Forum Xamarin.Forms

Xamarin Forms - Sending a large file over a TCP connection (using Socket.BeginSendFile) fails

I encountered a strange behavior when using the Socket.BeginSendFile method in a Xamarin Forms app on android.

When I run my app using the Visual Studio debugger (with an android phone connected over USB) and want to pass a large file (7 MB) over a TCP connection, only the first 2 MB of data are being sent. Transmission then stops.
The app functions as a TCP client. I checked with Wireshark and the TCP packets all look OK: The last 2 packets or a TCP ACK from my app (sending some data), followed by a TCP ACK from the server. The socket remains active.

When I run the app without the debugger (directly from a smartphone) the error doesn't seem to occur. The complete 7 MB of data is being transmitted.

I use Socket.BeginSendFile because that gives the highest throughput. When I do the exchange with 'normal' Socket.BeginSend calls (and my own file stream and reader code) the error never pops up, but throughput is bad. Transfer speed is about 40 times less than with Socket.BeginSendFile.

Is there anyone who can shed some light on this? The issue is probably in the Mono runtime since only System.Net.Sockets is being involved.

Answers

  • jezhjezh Member, Xamarin Team Xamurai

    Could you please post the main code that you send a large file or a basic demo so that we can tested with it?

  • JosHuybrighsJosHuybrighs USMember ✭✭

    Thanks for the prompt feedback!
    I continued testing in the mean time and observed 2 other things that might help in explaining the issue.

    1. Long living TCP connections
    The code I was referring to in my initial question was using the concept of a long living TCP connection; i.e. the socket to the server is only closed when the app stops. There is only 1 socket which is being reused with each file transfer.
    In debug mode, this code results in a large file not always being completely sent over the socket, or sometimes sending hangs for a relatively long time (10..30 seconds) and then resumes.
    When running straight on the device (without the debugger) files are correctly transferred, but I now noticed that when repeating large transfers the processor load on a Samsung S8 increases to 30% for the app and remains at that high level, even long after file transfer has stopped.
    I have no idea what the reason could be for this but I suspect that it might have something to do with socket resources hanging around. I therefore changed the code to always close the socket after the files have been transferred and connect again when a new file transfer comes up. That seems to have solved the processor load issue. The hanging file transfer issue however is still there in debug mode.

    This is my code now (which is almost the same as my original code, except for the creation and the disposal of the socket):

    async Task UploadSelectedFiles(IPEndPoint serverIPAddress)
    {
        // Setup connection
        Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        var result = socket.BeginConnect(serverIPAddress,
            (IAsyncResult ar) =>
            {
                socket.EndConnect(ar);
            },
            null);
        result.AsyncWaitHandle.WaitOne();
        if (!result.IsCompleted)
        {
            Debug.WriteLine("ExportingViewModel - Can't establish connection");
        }
        else
        {
            bool isError = false;
            bool isCancelled = false;
            _fileNumberUploading = 0;
            long totalSize = 0;
            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();
            foreach (var itemVM in Items)
            {
                if (_sendTaskCancelTokenSource.Token.IsCancellationRequested)
                {
                    break;
                }
                if (itemVM.IsSelected)
                {
                    await RunOnMainThreadAsync(() =>
                    {
                        FileUploadProgress = (double)FileNumberUploading / _nrOfFilesToUpload;
                        UploadedFileName = Path.GetFileName(itemVM.Item.PathName);
                        FileNumberUploading++;
                    });
                    totalSize += (int)itemVM.Item.Size;
                    Debug.WriteLine("ExportingViewModel - Send file");
                    bool success = SendFile(itemVM.Item.PathName, (int)itemVM.Item.Size, socket);
                    if (!success)
                    {
                        // Error
                        isError = true;
                        break;
                    }
                    // Wait for host receipt confirmation
                    byte[] data = new byte[4];
                    Debug.WriteLine("ExportingViewModel - Wait for file receipt message");
                    var rcvAsyncResult = socket.BeginReceive(data, 0, 4, SocketFlags.None,
                        (IAsyncResult ar) =>
                        {
                            int count = socket.EndReceive(ar);
                        },
                        null);
                    rcvAsyncResult.AsyncWaitHandle.WaitOne();
                    if (!rcvAsyncResult.IsCompleted)
                    {
                        // Error
                        Debug.WriteLine("ExportingViewModel - Detected socket close");
                        isError = true;
                        break;
                    }
                    Debug.WriteLine("ExportingViewModel - Got file receipt message");
                    if (data[2] == (byte)HostMessageId.FileImportResponse)
                    {
                        if (data[3] == 0x00)
                        {
                            // Host could not save the file
                        }
                    }
                }
            }
            stopWatch.Stop();
            long elapsedMs = stopWatch.ElapsedMilliseconds;
            if (!isCancelled &&
                _nrOfFilesToUpload != 0)
            {
                // Update UI
                await RunOnMainThreadAsync(() =>
                {
                    UploadedFileName = "";
                    FileUploadProgress = FileNumberUploading / _nrOfFilesToUpload;
                    IsUploading = false;
                    ErrorOccurred = isError;
                    TransferSpeedKbps = (int)(totalSize * 10 / elapsedMs); // approximate upload speed
                });
            }
            string errorStr = isError ? "error" : "no error";
            Debug.WriteLine($"ExportingViewModel - Finished: {errorStr}");
        }
        socket.Close();
        socket.Dispose();
    }
    
    bool SendFile(string filePathName, int fileSize, Socket socket)
    {
        // Message:
        // | headerLength(2) | msgId(1) | pathNameLength(1) | pathName(1..n | fileLength(4) | file(0..n |
        // Max file size = signed int => 2,147,483,647 bytes
    
        // Construct pathName bytes
        var pathNameBytes = Encoding.UTF8.GetBytes(filePathName);
    
        Int16 headerLength = (Int16)(1 + 1 + pathNameBytes.Length + 4);
        byte[] data = new byte[2 + headerLength];
    
        var lengthBytes = BitConverter.GetBytes(headerLength);
        // Will use big-endian format for the 8 bytes size field
        if (BitConverter.IsLittleEndian)
        {
            Array.Reverse(lengthBytes);
        }
        lengthBytes.CopyTo(data, 0);
    
        data[2] = (byte)HostMessageId.ImportFileRequest;
        data[3] = (byte)pathNameBytes.Length;
        pathNameBytes.CopyTo(data, 4);
    
        lengthBytes = BitConverter.GetBytes(fileSize);
        // Will use big-endian format for the 4 bytes size field
        if (BitConverter.IsLittleEndian)
        {
            Array.Reverse(lengthBytes);
        }
        lengthBytes.CopyTo(data, 4 + pathNameBytes.Length);
    
        Debug.WriteLine("ExportingViewModel - Calling BeginSend");
        var result = socket.BeginSend(data, 0, 2 + headerLength, System.Net.Sockets.SocketFlags.None,
            (IAsyncResult ar) =>
            {
                socket.EndSend(ar);
            },
            null);
        result.AsyncWaitHandle.WaitOne();
        if (!result.IsCompleted)
        {
            Debug.WriteLine("ExportingViewModel - Detected socket close");
            return false;
        }
        Debug.WriteLine("ExportingViewModel - Calling BeginSendFile");
        result = socket.BeginSendFile(filePathName,
            (IAsyncResult ar) =>
            {
                Debug.WriteLine("ExportingViewModel - EndSendFile called");
                try
                {
                    socket.EndSendFile(ar);
                }
                catch (Exception e)
                {
                    Debug.WriteLine($"ExportingViewModel - EndSendFile exception: {e.Message}");
                }
            },
            null);
        // return true;
        result.AsyncWaitHandle.WaitOne();
        if (!result.IsCompleted)
        {
            Debug.WriteLine("ExportingViewModel - BeginSendFile/EndSendFile failed");
        }
        return result.IsCompleted;
    }
    
    public Task RunOnMainThreadAsync(Action action)
    {
        var tcs = new TaskCompletionSource<object>();
        Device.BeginInvokeOnMainThread(
            () =>
            {
                try
                {
                    action();
                    tcs.SetResult(null);
                }
                catch (Exception e)
                {
                    tcs.SetException(e);
                }
            });
    
        return tcs.Task;
    }
    

    (UploadSelectedFiles runs in a seperate thread).

    2. Strange callback sequence for Socket.EndSendFile
    I have 2 test devices: one running android 5.1, another running 8.0.
    The code shown above only runs successfully on my android 5.1 device. On android 8.0 (samsung s8) the code hangs forever on the wait handle that has to synchronize the callback to Socket.EndSendFile. In other words: EndSendFile is not called for the first file.
    When I change the code to not wait on the handle (I can do that because I anyhow wait for a receipt message returned by the server) everything runs OK, but there is still a strange behaviour on android 8.0 with respect to how the EndSendFile callback should work: When I transfer 10 files the callback is never called during the transfer. It is only after all files have been sent and I close the socket that I get 10 consecutive calls to the EndSendFile callback.

    I know this is quite a long explanation of what I have experienced, but hopefully it is of use. I can proceed with the new short lived TCP concept, but the issue of the wait handle of course bothers me.

Sign In or Register to comment.