Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I'm trying to implement a TcpListener using the TPL. I'm not sure If I'm doing everything correctly though. I'm trying to keep the code as small as possible as well. It all works fine in a console application.

public async Task RunAsync(CancellationToken cancellationToken)
{
    var listener = new TcpListener(ip, port);
    listener.Start();

    while (!cancellationToken.IsCancellationRequested)
    {
        await AcceptClientAsync(listener, encoding, progress, cancellationToken);
    }
    listener.Stop();
}

Here is the AcceptClientAsync method. Sorry for the weird formatting. StyleCop likes to do that..

private async Task AcceptClientAsync(TcpListener tcpListener, Encoding encoding, IProgress<string> progress, CancellationToken cancellationToken)
    {
    var client = await tcpListener.AcceptTcpClientAsync();
    this.Connection = new ConnectionState { TcpClient = client };
    while (client.Connected && !cancellationToken.IsCancellationRequested)
    {
        await
            this.ReadStringAsync(this.Connection, encoding, progress)
                .ContinueWith(
                    task =>
                    progress.Report(
                        string.Format("Client {0} disconnected.", Connection.TcpClient.Client.RemoteEndPoint)),
                    TaskContinuationOptions.OnlyOnFaulted);
    }
}

And the ReadStringAsync method.

    private async Task ReadStringAsync(ConnectionState connection, Encoding encoding, IProgress<string> progress)
    {
        var stream = connection.TcpClient.GetStream();
        if (connection.Read > 0)
        {
            var encoded = encoding.GetString(connection.Buffer, 0, connection.Read);
            connection.StringBuilder.Append(encoded);
        }

        var decoded = connection.StringBuilder.ToString();
        if (decoded.Contains("<End>"))
        {
            progress.Report(decoded.Replace("<End>", string.Empty));
        }

        connection.Read = await stream.ReadAsync(connection.Buffer, 0, connection.Buffer.Length);
    }

EDIT:

Also, I'd like to keep using the IProgress<> interface and support cancellation via CancellationTokens

Update:

public class TcpServer
{
    public TcpServer(Encoding encoding, IProgress<string> progress)
    {
        this.Clients = new BindingList<ConnectionState>();
        this.Encoding = encoding;
        this.Progress = progress;
    }

    public BindingList<ConnectionState> Clients { get; private set; }

    private Encoding Encoding { get; set; }

    private IProgress<string> Progress { get; set; }

    public async Task WriteStringAsync(ConnectionState connection, string text, Encoding encoding)
    {
        var stream = connection.TcpClient.GetStream();
        var data = encoding.GetBytes(text);
        await stream.WriteAsync(data, 0, data.Length);
    }

    public async Task ConnectAsync(IPAddress ip, int port, Encoding encoding, IProgress<string> progress, CancellationToken token)
    {
        var tcpClient = new TcpClient();
        await tcpClient.ConnectAsync(ip, port);
        this.Progress.Report(string.Format("Successfully connected to {0}.", tcpClient.Client.RemoteEndPoint));
        var connection = new ConnectionState { TcpClient = tcpClient };
        this.Clients.Add(connection);
        this.HandleClientAsync(tcpClient, encoding, progress, token);
    }

    public async Task RunAsync(IPAddress ip, int port, CancellationToken cancellationToken)
    {
        if (ip == null)
        {
            throw new ArgumentNullException("ip");
        }

        if (port > 65535)
        {
            throw new ArgumentOutOfRangeException("port");
        }

        var listener = new TcpListener(ip, port);
        listener.Start();
        this.Progress.Report(string.Format("Server now running on {0}.", listener.LocalEndpoint));
        while (true)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                listener.Stop();
                cancellationToken.ThrowIfCancellationRequested();
                return;
            }

            var client = await listener.AcceptTcpClientAsync();
            this.Progress.Report(string.Format("Client {0} connected.", client.Client.RemoteEndPoint));
            this.HandleClientAsync(client, this.Encoding, this.Progress, cancellationToken);
        }
    }

    private async Task ReadStringAsync(ConnectionState connection, Encoding encoding)
    {
            var stream = connection.TcpClient.GetStream();
            if (connection.Read > 0)
            {
                connection.StringBuilder.Append(encoding.GetString(connection.Buffer, 0, connection.Read));
            }

            var decoded = connection.StringBuilder.ToString();
            if (decoded.Contains("<End>"))
            {
                this.Progress.Report(
                    string.Format("Client {0}: {1}", connection.IpAddress, decoded.Replace("<End>", string.Empty)));
                connection.StringBuilder.Clear();
            }

            connection.Read = await stream.ReadAsync(connection.Buffer, 0, connection.Buffer.Length);
    }

    private async Task HandleClientAsync(
        TcpClient client,
        Encoding encoding,
        IProgress<string> progress,
        CancellationToken cancellationToken)
    {
        var connection = new ConnectionState { TcpClient = client };
        this.Clients.Add(connection);
        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                await this.ReadStringAsync(connection, encoding);
            }
            catch (SocketException)
            {
                progress.Report(
                    string.Format("Client {0} disconnected.", connection.TcpClient.Client.RemoteEndPoint));
            }
        }
    }
}
share|improve this question
add comment

1 Answer

  1. If you immediately await everything, you're not going to get any parallelism. This means that at any moment, there can be only one connection to this listener. Is this intentional?

  2. Because of the way TCP works, the Connected property may return true even if the other side already disconnected. This means that you should send keep-alive packets, even if all you logically want to do is reading.

  3. ContinueWith() is useful only rarely when you can use async. OnlyOnFaulted can be easily rewritten using try-catch. Though you should be catching only the specific exception that you need, not all of them. So your code could look something like:

    try
    {
        await this.ReadStringAsync(this.Connection, encoding, progress);
    }
    catch (NotSureWhichException ex)
    {
        progress.Report(
            string.Format("Client {0} disconnected.", Connection.TcpClient.Client.RemoteEndPoint));
    }
    
  4. I think the way you're dealing with <End> is wrong. If there is always going to be only one message per connection, then you should somehow indicate that after reading <End>. If there can be multiple messages, then an end of one message and start of the following one could be read in the same ReadAsync(), which means you're not going to report partial message.

share|improve this answer
    
So about #1, perhaps I should use a ManualResetEvent to signal to the main thread to listen for another connection if one is active? Also, how often should I send Keep-Alive packets? –  shredder8910 Feb 22 at 2:13
    
I don't think MRE is a good solution here, especially since it's not async. What I would do is to move the while loop from AcceptClientAsync() to a separate method and not await that. But if you do that, be very careful about exceptions (i.e. you should probably add a catch around that while). –  svick Feb 22 at 12:57
    
Regarding the timing of keep alive packets, that depends on you, specifically, on how soon do you want to learn about the other side disappearing. –  svick Feb 22 at 12:58
    
I've updated the main post with full code and edits. Tell me what you think. I've not implemented the keep-alive packets yet. –  shredder8910 Feb 22 at 19:37
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.