다음을 통해 공유


[**From Alberto**] Socket-based application loses data when finishing communication

Subtitle -- How to implement graceful disconnection in a .NET (and .NET Compact Framework) application

Alberto is one of my direct colleagues and works on the same kind of issues and with the same developers, that I work. We'll possibly share this blog since it's all about "Mobile Development - Support Side Story", in the meantime I'm lending this space... Smile

 

1. Introduction

Graceful disconnection is always an important topic in a client - server application, because when one writes such an application, the first questions is: “When I’m done and I close the connection, how can I assure that the whole data will be transferred from client to server and vice-versa?”

In order to answer this question, we have to clarify some important things: first of all, we have to distinguish between connection-oriented and connectionless transport. In connection-oriented based application, we usually make use of TCP protocol as transport protocol, while in connectionless based application we usually make use of UDP protocol. This means that with TCP we have a message exchange to setup, control and teardown the connection, while with UDP there’s no need to setup a connection, because each block of data is self-contained in the so-called DATAGRAM, and this single “message” (block of data) is sent in a single step, without any warranties about the correct reception of the message from the counterpart.

In managed world, these two connection ways are mapped into two enumerative constants used by the Socket class during Socket object construction (Socket type is the basic building block of network managed application and is part of System.Net.Socket namespace). Documentation reference about Socket class is here.

 

2. TCP Internals

I pointed out this introduction because it’s important to understand that the graceful disconnection pattern applies only to connection-oriented based application, because in connectionless transport data are unreliably forwarded to the counterpart, so we have to implement some other mechanism in order to check integrity of data received.

So, let’s switch back to connection oriented (TCP based) application and look what happens under the hood: the lifecycle of a typical TCP connection is SETUP – SEND/RECEIVE data (with ACKnowledge packets) – TEARDOWN.

The SETUP phase consist in a “three way handshake”, that allows client and server to move through a FSM (Finite State Machine) in order to cover the TCP setup algorithm and managed errors; from network perspective, the handshake is completed when SYN, SYN-ACK, ACK packets are exchanged correctly between client and server (this is done when eventually the connect() unmanaged API is called from the client towards the server).

The SEND/RECEIVE phase consist in data exchange (two ways) between client and server, with a windowed-base flow control mechanism that ensures reliable transfer of blocks of data between two endpoints; the important thing to notice is that such a protocol allows usage of prioritized message by means of a parallel channel, called OOB (Out Of Band). This way, each part can send an urgent message that can overcome currently ongoing packets over connection and it will be processed by the counterpart before other data packets. Such messages can be normal flow control messages (window resizing, counter updating) or also RST packets, that will broke up connection immediately: they are very useful in order to recover error scenarios, but RST packets can also be sent during normal operations when one side decide to forcibly close the connection calling closesocket() unmanaged API (For example, this API is called by OS when an application crashes or its process exits).

The TEARDOWN phase is symmetric to the SETUP phase, except for the handshake that should involve four messages: FIN [ACK], ACK , FIN-ACK, ACK. This is due to the fact that this is a two-way connection, so we have to ensure that both the client and the server correctly receive all data and stop their TCP sender (this avoid that some zombie packets are sent over network). If we recognize such a pattern in a network trace, we are confident that the connection is correctly (gracefully) closed. So, let’s have a look how this pattern can be achieved through native and managed programming.

 

3. Graceful disconnection: native (unmanaged) applications

For native applications, there is a good article that shows the details of the graceful disconnection: " Graceful Shutdown, Linger Options, and Socket Closure ". Basically, this is the code snippet for server and/or client to ensure graceful disconnection by their own side; notice that the implementation is independent from entity.

 // SOCKET CHANNEL:
// Tx --->\ /---> Rx
// ============
// Rx <---/ \<--- Tx
//

// NOTE: I assume here we are not willing to accept nor parse more data from counterpart, 
// but only to close the socket gracefully
void socket_disc(SOCKET s)
{
    // shutdown SEND channel 
    // (cause FD_CLOSE to be generated on the counterpart)
    if(shutdown(s, SD_SEND) == SOCKET_ERROR)
    {
    printf("Error while shutdown socket: GetSocketLastError=0x%x", WSAGetLastError());
    
    // check the error .. possibly act according to error code
    closesocket(s);
    
    return;
    }
    
    // wait for FD_CLOSE message – any data discarded
    int rec_res;
    char c;
    do
    {
        rec_res = recv(s, &c, sizeof(char), 0);
    } while (rec_res != 0 && rec_res != SOCKET_ERROR)
    
    // check the code – recv() can exit for an error, due to a RST packet from other side
    if(WSAGetLastError() != 0) // errors -> maybe notify ...
    {
        ...
    }
    
    // else: FD_CLOSE received, socket gracefully closed from remote side
    closesocket(s);
    
    return;
}

 

With the code above, you should get a network trace pattern like this (Network Monitor trace):

FINACK

Note the pattern:

  •        Client --> Server: [FIN, ACK]
  •        Server --> Client: [ACK]
  •        Server --> Client: [FIN, ACK]
  •        Client --> Server: [ACK]

If everything works fine, you should not see any RST packet related to lost segments.

 

4. Graceful disconnection: managed (.NET) applications

As I said in the first paragraph, in managed .NET world the basic building block of connection-aware applications is the Socket class. Socket class is a thin wrapper onto the socket native handle, so it provides all the operations available for sockets: hence, the above code snippet can be easily mapped into managed application:

 public void GracefulDisconnection(Socket s)
{
    byte[] buf = new byte[1];
    try
    {
        s.Shutdown(SocketShutdown.Send);
        while (s.Receive(buf, 1, SocketFlags.None) != 0)
            ;
    }
    catch (SocketException s_ex)
    {
        Debug.Write("Socket Exception: ");
        Debug.Write(s_ex.ErrorCode + s_ex.Message);
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex.Message);
    }
    finally
    {
        s.Close();
    }
}

 

Usually you won’t deal directly with Socket object, but with higher-level protocol classes that inherit from Socket class or embed a Socket object instance: anyway, you should always have access to socket-related operations, either directly or indirectly. For example, you can have a NetworkStream object that involves a TCP connection info and objects, then you can associate a StreamReader and/or StreamWriter objects in order to have an handy way to manage data; in this case, you have to call the proper method, which in turn eventually cause Socket object methods to be called.

Here follows an example of a case study, using TCPClient and NetworkStream classes:

 try
{
    using ( NetworkStream stream = tcpClient.GetStream() )
    {
        // Write some data onto the stream
        StreamWriter w = new StreamWriter( stream );
        w.Write( s );
        w.Flush();
        
        // force shutdown 
        tcpClient.Client.Shutdown(SocketShutdown.Send);
        
        StreamReader r = new StreamReader(stream);
        try
        {
            r.Read();
        }
        catch (System.IO.IOException ex)
        {
            Debug.WriteLine("End of connection (forced)");
        }
        
        w.Close();
        r.Close();
    }
}
catch ( Exception e )
{
    Debug.WriteLine( e.ToString() );
}

Notice that Close() method has always to be called in order to dispose of the Socket object and to close native socket handle (this in turns raise a call to closesocket() API).

 

Cheers,

ALBERTO!