Received data buffer always empty after calling so

2019-08-10 06:40发布

I have a Windows Phone 8 app that talks to server over a socket. The server is quite simple. It accepts a string, returns a string, and immediately closes the connection. I have talked to the server using Windows Forms apps and never had a problem.

I'm using the code below which I adapted from this MSDN page that shows how to use a socket in a Windows Phone 8 app:

http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202858(v=vs.105).aspx

I modified the code to be non-blocking using async/await. All of the methods are awaitable now and the WaitOne() call after each socket async operation is spun off to a new task. I am not getting any socket errors or Exceptions. However, when the anonymous Completed event handler for the ReceiveAsync() call fires, the bytes transferred value is always 0.

Strange note. If I set a breakpoint on certain lines in completed event handler for ReceiveAsync() the call times out. This doesn't happen if I don't set a breakpoint and it only happens on certain lines. I have no idea why. No time-outs occur if I don't set breakpoints

What am I doing wrong? Here's the code I'm using. The calling code (not shown) simply creates an instance of the SocketDetails class, and then calls in order, await ConnectAsync(), await SendAsync("somestring"), and finally await ReceiveAsync() on the SocketDetails instance.:

/// <summary>
/// Details object that holds a socket.
/// </summary>
public class SocketDetails
{
    /// <summary>
    /// Creates a new socket details object.
    /// </summary>
    /// <param name="hostName">The host name for the connection.</param>
    /// <param name="portNumber">The port name for the connection.</param>
    /// <param name="timeOutMS">The maximum number of milliseconds to wait for a connection before <br />
    ///  timing out.</param>
    /// <param name="defaultBufferSize">The maximum number of bytes for the buffer that receives the <br />
    ///  connection result string.</param>
    public SocketDetails(
        string hostName,
        int portNumber,
        int timeOutMS,
        int defaultBufferSize)
    {
        if (String.IsNullOrWhiteSpace(hostName))
            throw new ArgumentNullException("The host name is empty.");

        if (portNumber <= 0)
            throw new ArgumentOutOfRangeException("The port number is less than or equal to 0.");

        if (timeOutMS < 0)
            throw new ArgumentOutOfRangeException("The time-out value is negative.");

        this.HostName = hostName;
        this.PortNumber = portNumber;
        this.TimeOutMS = timeOutMS;

        // Create DnsEndPoint. The hostName and port are passed in to this method.
        this.HostEntry = new DnsEndPoint(this.HostName, this.PortNumber);

        // Create a stream-based, TCP socket using the InterNetwork Address Family. 
        this.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        // Create the manual reset event.
        this.ClientDone = new ManualResetEvent(false);
    }

    /// <summary>
    /// The string returned by the last connection attempt.
    /// </summary>
    public string ConnectionResult { get; private set; }

    public DnsEndPoint HostEntry
    { get; private set; }

    /// <summary>
    /// The host name to open the socket on.
    /// </summary>
    public string HostName { get; private set; }

    /// <summary>
    /// The port number to use when opening the socket.
    /// </summary>
    public int PortNumber { get; private set; }

    /// <summary>
    /// Cached Socket object that will be used by each call for the lifetime of this class
    /// </summary>
    public Socket Socket { get; private set; }

    /// <summary>
    /// Signaling object used to notify when an asynchronous operation is completed.  Exposing it <br />
    ///  so other threads/code can reset it if necessary, unblocking any threads waiting on this socket.
    /// </summary>
    public ManualResetEvent ClientDone { get; private set; }

    /// <summary>
    /// Define a timeout in milliseconds for each asynchronous call. If a response is not received within this <br />
    //   timeout period, the call is aborted.
    /// </summary>
    public int TimeOutMS { get; set; }

    // The maximum size of the data buffer to use with the asynchronous socket methods
    public int BufferSize { get; set; }

    /// <summary>
    /// Waits until a socket operation completes or the time-out period is reached.
    /// </summary>
    /// <returns>TRUE if the semaphore wait did not TIME-OUT, FALSE if a time-out did occur.</returns>
    private bool BlockUntilSocketOperationCompletes(string caller)
    {

        // Sets the state of the event to nonsignaled, causing this task's thread to block.
        // The completed handler of the socket method that called this method should unblock it.
        this.ClientDone.Reset();

        bool bRet = this.ClientDone.WaitOne(this.TimeOutMS);

        if (bRet)
            Debug.WriteLine("WaitOne() completed successfully for caller: " + caller);
        else
            Debug.WriteLine("WaitOne() timed-out for caller: " + caller);

        return bRet;
    }

    /// <summary>
    /// (awaitable) Connects to the socket using the details given in the constructor.
    /// </summary>
    /// <returns>Returns the banner or error message returned from the sockete during the <br />
    ///  connection attempt.</returns>
    ///  <remarks>This call BLOCKS until the connection succeeds or the time-out limit is reached!</remarks>
    async public Task<string> ConnectAsync()
    {
        // Create a SocketAsyncEventArgs object to be used in the connection request
        SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();
        socketEventArg.RemoteEndPoint = this.HostEntry;

        // Inline event handler for the Completed event.
        // Note: This event handler was implemented inline in order to make this method self-contained.
        socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(delegate(object s, SocketAsyncEventArgs e)
        {
            // Retrieve the result of this request
            this.ConnectionResult = e.SocketError.ToString();

            Debug.WriteLine("CONNECT completed, Connection result string received: " + this.ConnectionResult);

            // Signal that the request is complete, unblocking the UI thread
            this.ClientDone.Set();
        });

        // Make an asynchronous Connect request over the socket
        this.Socket.ConnectAsync(socketEventArg);

        // Wait for the return operation to complete or until it times out.
        bool bIsTimeOut = !(await Task.Run(() => BlockUntilSocketOperationCompletes("ConnectAsync")));

        return this.ConnectionResult;
    }

    /// <summary>
    /// (awaitable) Send the given data to the server using the established connection
    /// </summary>
    /// <param name="data">The data to send to the server</param>
    /// <returns>The result of the Send request</returns>
    /// <remarks>This call BLOCKS until the data is received or the attempt times out!</remarks>
    async public Task<string> SendAsync(string data)
    {
        string response = "Operation Timeout";

        // We are re-using the _socket object initialized in the Connect method
        if (this.Socket != null)
        {
            // Create SocketAsyncEventArgs context object
            SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();

            // Set properties on context object
            socketEventArg.RemoteEndPoint = this.Socket.RemoteEndPoint;
            socketEventArg.UserToken = null;

            // Inline event handler for the Completed event.
            // Note: This event handler was implemented inline in order 
            // to make this method self-contained.
            socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(delegate(object s, SocketAsyncEventArgs e)
            {
                response = e.SocketError.ToString();

                Debug.WriteLine("SEND completed, Response received: " + response);

                // Unblock the UI thread
                this.ClientDone.Set();
            });

            // Add the data to be sent into the buffer
            byte[] payload = Encoding.UTF8.GetBytes(data);
            socketEventArg.SetBuffer(payload, 0, payload.Length);

            // Make an asynchronous Send request over the socket
            this.Socket.SendAsync(socketEventArg);

            // Wait for the return operation to complete or until it times out.
            bool bIsTimeOut = !(await Task.Run(() => BlockUntilSocketOperationCompletes("SendAsync")));
        }
        else
        {
            response = "Socket is not initialized";
        }

        return response;
    }
    /// <summary>
    /// (awaitable) Receive data from the server using the established socket connection
    /// </summary>
    /// <returns>The data received from the server</returns>
    async public Task<string> ReceiveAsync()
    {
        string response = "Operation Timeout";

        // We are receiving over an established socket connection
        if (this.Socket != null)
        {
            // Create SocketAsyncEventArgs context object
            SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();
            socketEventArg.RemoteEndPoint = this.Socket.RemoteEndPoint;

            // Setup the buffer to receive the data
            socketEventArg.SetBuffer(new Byte[this.BufferSize], 0, this.BufferSize);

            // Inline event handler for the Completed event.
            // Note: This even handler was implemented inline in order to make 
            // this method self-contained.
            socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(delegate(object s, SocketAsyncEventArgs e)
            {
                Debug.WriteLine("RECEIVE completed.");

                if (e.SocketError == SocketError.Success)
                {
                    // Retrieve the data from the buffer
                    response = Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred);
                    response = response.Trim('\0');

                    Debug.WriteLine("RECEIVE completed, response received: " + response);
                }
                else
                {
                    response = e.SocketError.ToString();

                    Debug.WriteLine("RECEIVE failed: socket error: " + response);
                }

                this.ClientDone.Set();
            });

            // Make an asynchronous Receive request over the socket
            this.Socket.ReceiveAsync(socketEventArg);

            bool bIsTimeOut = await Task.Run(() => BlockUntilSocketOperationCompletes("ReceiveAsync"));
        }
        else
        {
            response = "Socket is not initialized";
        }

        return response;
    }

    /// <summary>
    /// Closes the Socket connection and releases all associated resources
    /// </summary>
    public void Close()
    {
        if (this.Socket != null)
        {
            this.Socket.Close();
        }
    }
} // public class SocketDetails

1条回答
▲ chillily
2楼-- · 2019-08-10 07:26

First off, I strongly recommend you not use TCP/IP. If at all possible, use WebAPI + HttpClient instead. This is particularly true if you are still learning async.

That said, my comments follow.

Receiving an empty array is a normal situation for a socket. It indicates that the other side has closed down its sending channel.

I don't recommend using the SocketAsyncEventArgs-based API. It's possible, but awkward to make it work with async. Instead, write TAP-over-APM wrappers (I prefer to write these as extension methods).

The *Async wrappers in your code sample all follow a pattern where they start an asynchronous operation and then queue up a thread pool operation to wait for it to complete. This seems quite unnecessary to me; Task<T>.Factory.FromAsync or TaskCompletionSource<T> would be more efficient and less convoluted.

Finally, you should ensure you are not using a read-then-write loop, a common mistake with TCP/IP applications. There are a couple of problems with a read-then-write loop; one of them is that it cannot recover from the half-open problem, which I describe on my blog.

查看更多
登录 后发表回答