tags:

views:

610

answers:

4

What is the pattern and/or how would you change the following code so that a connection can be closed gracefully from either server or client, without needing exception handling.

  1. TcpClient vs. Socket: I'm not tied to using the TcpClient client class. I wrote the following code to try to demonstrate the simplest case possible. I had been using a Socket & SocketAsyncEventArgs but things were getting too complicated dealing with this issue.

  2. Blocking vs. Asynchronous: Because of the blocking calls this may be difficult or impossible. If so, that's fine, but how do you solve it in the asynchronous case?

  3. Quit Token?: I've experimented with sending some kind of "Quit" token to other side so that it knows to shutdown, but haven't gotten it working and I wanted to present a minimal example here.

  4. Exception Handling Needed Anyways: I know exception handling will be necessary in the real application as network connections etc will fail. But can not the expected case of a graceful shutdown be handled without exceptions??

Edit: Please note the following code will throw an exception when either side closes the connection. I am looking for a (likely application level) pattern to avoid throwing an exception entirely if either side "quits".


Channel

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;

public class Channel
{
    private readonly TcpClient client;
    private readonly NetworkStream stream;
    private readonly TextReader reader;
    private readonly TextWriter writer;
    private readonly string name;

    public Channel(string name, TcpClient client)
    {
        SocketAsyncEventArgs
        this.name = name;
        this.client = client;
        stream = client.GetStream();
        stream.ReadTimeout = 1000;
        reader = new StreamReader(stream, Encoding.ASCII);
        writer = new StreamWriter(stream, Encoding.ASCII);
    }

    public void Run()
    {
        Console.WriteLine(name + ": connected");
        Thread readThread = new Thread(Read);
        readThread.Start();

        while (true)
        {
            var line = Console.ReadLine();
            if (String.IsNullOrEmpty(line)) break;

            writer.WriteLine(line);
            writer.Flush();

        }
        Console.WriteLine(name + " quitting");

        stream.Flush();
        client.Close();
    }

    private void Read()
    {
        while (true)
        {
            var line = reader.ReadLine();
            Console.WriteLine(name + " recieved: " + line);
        }
    }
}


Server

class Server
{
    static void Main(string[] args)
    {
        TcpListener listener = new TcpListener(IPAddress.Loopback, 5000);
        listener.Start();

        Console.WriteLine("server: listener started");

        var channel = new Channel("server", listener.AcceptTcpClient());

        channel.Run();

        listener.Stop();
    }
}


Client

class Client
{
    static void Main(string[] args)
    {
        TcpClient client = new TcpClient(AddressFamily.InterNetwork);

        Console.WriteLine("client: created, press key to connect");
        Console.ReadKey();

        client.Connect(IPAddress.Loopback, 5000);

        var channel = new Channel("client", client);
        channel.Run();
    }
}
A: 
Joel Coehoorn
The current example above will throw an exception if one side closes the connection. The using will not trap the exception. Regardless, I am looking for a way to avoid throwing an exception entirely during a graceful shutdown.
Joseph Kingry
+1  A: 

Use Socket.Shutdown() before calling Socket.Close(). Wait for shutdown processing to complete (e.g. ReceiveAsync() will return 0 bytes).

  1. Abstraction choice is (mostly) a non-issue.
  2. Async I/O means never having to see your sorry.
  3. From MS documentation for Socket.ReceiveAsync() method: For byte streams, zero bytes having been read indicates graceful closure and that no more bytes will ever be read.
Sky
A: 

I haven't done sockets for a while, but I remember that you had to call shutdown() for them.

The .NET equivalent, I guess, would be socket.Shutdown(SocketShutdown.Both)

erikkallen
A: 

I modified my code for Channel to use Socket.Shutdown and to use asynchronous reads. This seems to be the minimum required. Basically graceful shutdown looks like this:

Originator - the side of the connection requesting the "quit"

Receiver - the side of the connection receiving the "quit"

  1. Originator-Send calls Socket.Shutdown(SocketShutdown.Send) and waits for Originator-Read to signal.
  2. This will cause Receiver-Read NetworkStream.EndRead(result) to return 0
  3. Receive-Readr closes the connection.
  4. This will cause Originator-Read NetworkStream.EndRead(result) to return 0
  5. Originator-Read closes the connection and signals Originator-Send.
  6. Originator-Send quits
  7. Next request to write on Receiver-Send recognizes connection is closed and exits.
public class Channel
{
    private readonly TcpClient client;
    private readonly NetworkStream stream;
    private readonly TextReader reader;
    private readonly TextWriter writer;
    private readonly string name;
    private readonly ManualResetEvent quit = new ManualResetEvent(false);

    public Channel(string name, TcpClient client)
    {
     this.name = name;
     this.client = client;
     stream = client.GetStream();
     writer = new StreamWriter(stream, Encoding.ASCII);
    }

    public void Run()
    {
     Console.WriteLine(name + ": connected");

     byte[] buffer = new byte[client.Client.ReceiveBufferSize];
     stream.BeginRead(buffer, 0, buffer.Length, this.Read, buffer);

     while (true)
     {
      var line = Console.ReadLine();
      if (String.IsNullOrEmpty(line) || !this.client.Connected) break;

      writer.WriteLine(line);
      writer.Flush();
     }
     Console.WriteLine(name + " quitting");

     if (client.Connected)
     {
      client.Client.Shutdown(SocketShutdown.Send);

      Console.WriteLine(name + " send shutdown, waiting for read to shutdown.");

      quit.WaitOne();
     }

     Console.WriteLine(name + " quit, press key to continue.");

     Console.ReadKey();
    }

    private void Read(IAsyncResult result)
    {
     var read = this.stream.EndRead(result);

     if (read == 0)
     {
      Console.WriteLine(name + " read stopped");
      this.client.Close();
      this.quit.Set();
      return;
     }

     var buffer = (byte[])result.AsyncState;
     Console.WriteLine(name + " recieved:" + Encoding.ASCII.GetString(buffer, 0, read));

     stream.BeginRead(buffer, 0, buffer.Length, this.Read, buffer);
    }
}
Joseph Kingry