views:

29

answers:

1

I'm working on a client/server application where the connections from the client to the server stay open until the client application is closed.

If the server application goes down unexpectedly, while the client is reading data, I want the client to treat this as an exception, but then to catch the exception and raise an event with the exception as the argument.

I've written a test that I think should test that this system works, but the object I'm testing doesn't seem to register that the socket is closed unless I put in a break point and then continue.

The important part of the test looks like this:

StreamingMonitor sm = new StreamingMonitor();
bool errored = false;
string msg = "";
sm.ErrorOccurred += (s, a) =>
{
    errored = true;
    msg = a.Exception.Message;
};
sm.Enabled = true;
client = listener.AcceptTcpClient();
client.GetStream().Write(BitConverter.GetBytes(10000), 0, 4);
client.Close();
while(!errored)
{}
Assert.AreEqual("A request to send or receive data was disallowed because the socket had already been shut down in that direction with a previous shutdown call", msg);

The TcpListener object listener is listening to the loopback address.

The StreamingMonitor begins listening for the length of the data to retrieve when it is enabled. The length of data is always assumed to fit into a signed 32 bit integer.

When the message length is received then this methods is called.

    private void GotMessageLength(IAsyncResult asyncResult)
    {
        try
        {
            client.Client.EndReceive(asyncResult);
            if(firstMessage)
            {
                firstMessage = false;
                if (Connected != null)
                {
                    Connected(this, new EventArgs());
                }
            }
            int msgLen = BitConverter.ToInt32(messageLength, 0);
            byte[] message = new byte[msgLen];
            List<byte> lbMessage = new List<byte>();
            int bytesReturned = client.Client.Receive(message);
            int remaining = (msgLen < bytesReturned) ? bytesReturned - msgLen : msgLen - bytesReturned;
            if(remaining > 0)
            {
                if (bytesReturned > 0)
                {
                    for (int i = 0; i < bytesReturned; i++)
                    {
                        lbMessage.Add(message[i]);
                    }
                }
                while(remaining > 0)
                {
                    if(!client.Connected)
                    {
                        throw new SocketException((int)SocketError.Shutdown);
                    }
                    bytesReturned = client.Client.Receive(message);
                    remaining = (remaining < bytesReturned) ? bytesReturned - remaining : remaining - bytesReturned;
                    if (bytesReturned > 0)
                    {
                        for (int i = 0; i < bytesReturned; i++)
                        {
                            lbMessage.Add(message[i]);
                        }
                    }
                }
                message = lbMessage.ToArray();
            }
            MessageReceived(this, new MessageReceivedEventArgs(message));
            if (Enabled)
            {
                client.Client.BeginReceive(messageLength, 0, 4, SocketFlags.None, GotMessageLength, null);
            }
        }
        catch (SocketException ex)
        {
            if(ErrorOccurred != null)
            {
                ErrorOccurred(this, new ErrorEventArgs(ex));
            }
        }
        catch (ObjectDisposedException)
        {

        }
    }

The method reads data from the network stream until it has read the specified number of bytes. If the remote connection closes then it should raise a socket exception.

However, the unit test gets caught in a infinite loop, waiting for the error to occur, because the socket in the StreamingMonitor never realises that the other end has closed.

How can I make the SteamingMonitor realise that the server has gone?

Is this possible on the loopback address?

Sorry for all the code, I couldn't think how to cut the method down.

+1  A: 

I can give some general pointers on the area that might help.

Loopback (or just using localhost) in general does not act the same way as a real network. Scenarios like how much data is send/received in each call to the socket api. So always test or real network connections.

The socket api will only find out if the other side is disconnected upon trying to send to it ( i think that is correct). So some sort of heartbeat functionality comes in handy =)

Edit: You can also get the SocketException to determine if the the other side is disconnected by trying to receive (did some basic test on some old code of mine).

protected void ReceiveCallback(IAsyncResult ar)
{
     var so = (StateObject)ar.AsyncState;
     if (!so.Socket.Connected) return;

     try
     {
         int read = so.Socket.EndReceive(ar);
         if (read > 0)
         ProcessBuffer(so, so.Buffer, read);

         so.Socket.BeginReceive(so.Buffer, 0, so.Buffer.Length, SocketFlags.None, ReceiveCallback, so);
      }
      catch (SocketException e)
      {
          Trace.WriteLine("[Networking]::NetBase.ReceiveCallback: SocketException");
          Output.WriteLine(e.Message);
          RaiseDisconnected();
      }
      catch (ObjectDisposedException e)
      {
          Trace.WriteLine("[Networking]::NetBase.ReceiveCallback: ObjectDisposedException");
          Output.WriteLine(e.Message);
          RaiseDisconnected();
       }
}

This will call my disconnect function if the other side crashes for some reason. Hope it helps

Markus Andersson
Thanks for the pointers. I'll try using a different address, other than loopback.
Matt Ellen
Markus is correct; [you must send data to detect whether the connection is still viable](http://nitoprograms.blogspot.com/2009/05/detection-of-half-open-dropped.html).
Stephen Cleary
Excellent link. Thanks, Stephen. I've added in a heartbeat and the problem has gone away.
Matt Ellen