Skip to content

[API Proposal]: Simple, modern TCP APIs #63162

Open
@geoffkizer

Description

@geoffkizer

Background and motivation

When writing a TCP client or server, you have a choice of two APIs today: Socket or TcpClient/TcpListener.

You can use the Socket class directly. However, Socket is a low-level, general-purpose API that isn’t specific to TCP – it supports UDP, Unix Domain Sockets, raw sockets, and arbitrary socket types. Using it requires understanding concepts like AddressFamily, SocketType, ProtocolType, dual mode sockets, the EndPoint abstract base class and derived IPEndPoint class, how and when to use Bind, which Socket APIs work for TCP (Send/Receive) vs disconnected UDP (SendTo, ReceiveFrom), etc. If you are already familiar with Sockets, this is not a big deal – though note I’ve seen us mess some of this up in our own code, e.g. not enabling dual mode sockets properly because we used the wrong Socket constructor. If you are not already familiar with Sockets, and just want to write some basic TCP client or server code, then understanding Sockets is an unnecessary barrier to entry.

Alternatively, you can use TcpClient and TcpListener, which provide a higher-level, TCP-specific API. These simplify creating and accepting TCP connections and provide some convenience APIs for common TCP tasks like setting NoDelay, controlling LingerState, or specifying a local IP and port to use when connecting.

Unfortunately, TcpClient is an awful API.

TcpClient is an old-style “create-set-use” API. You create an instance, configure it, and then call Connect[Async] to actually establish the connection. You then retrieve the associated NetworkStream using GetStream().

The overall TCP connection functionality is split between NetworkStream and TcpClient, with some (but not all) functionality in both places. Want to read or write? Use NetworkStream. Want to set NoDelay, or configure the send and receive buffer sizes? Use TcpClient. Want to set a read or write timeout? You can use either.

Want to close the connection? You can use either. Both NetworkStream and TcpClient implement IDisposable and also have a Close method. Either one will dispose both the NetworkStream and the TcpClient, but this is not at all obvious – and in fact the docs for GetStream() get this wrong; see #63154.

Even worse, TcpClient is finalizable even though its finalizer simply calls Dispose(false), which, unless you override it, does nothing. Socket itself is finalizable and so classes that use it, like TcpClient and NetworkStream, don’t need to be finalizable themselves.

Even worse, some basic TCP functionality is missing from both TcpClient and NetworkStream. There is no way to shutdown the connection, no access to local or remote endpoints, no way to configure TCP keep-alive.

On top of that, some of the TcpClient APIs are just confusing. The property you use to access the underlying socket is called “Client”… why? Why not “Socket”?? One constructor takes a hostname and port and performs a (synchronous) Connect for you; another takes an IPEndPoint, but instead of performing the Connect, it uses this as the local endpoint for the connection.

Even more confusing, TcpClient is also used in TCP server scenarios. TcpListener has AcceptTcpClient[Async] methods that return a TcpClient instance to represent the accepted connection. This allows you to configure NoDelay and get the associated NetworkStream. Or even access the underlying socket using the Client property, even though you’re a server… gahhhhhhh.

We shouldn’t have two types that each incompletely represent a TCP connection. We should have a single type that represents a TCP connection and allows you to perform all common TCP connection operations. And we should have simple APIs that return this type for TCP client scenarios (Connect) and TCP server scenarios (Listen/Accept).

API Proposal

(Note this is a general sketch and is not intended to include all potential overloads, optional params, etc.)

namespace System.Net.Sockets
{
    // New class
    public sealed class TcpConnection : NetworkStream
    {
        // Create from existing Socket. Socket must be connected. TcpConnection takes ownership.
        public TcpConnection(Socket socket);

        public IPAddress LocalAddress { get; }
        public int LocalPort { get; }
        public IPAddress RemoteAddress { get; }
        public int RemotePort { get; }

        public bool NoDelay { get; set; }
        
        public int SendBufferSize { get; set; }
        public int ReceiveBufferSize { get; set; }

        public void Shutdown(SocketShutdown how);

        public Socket Socket { get; }

        // Read[Async], Write[Async], and Close are inherited from NetworkStream
        // Other possible additions, now or in the future: TCP keep-alive, LingerState
    }

    // New class
    public static class Tcp
    {
        public static TcpConnection Connect(IPAddress address, int port);
        public static ValueTask<TcpConnection> ConnectAsync(IPAddress address, int port, CancellationToken cancellationToken = default);
        public static TcpConnection Connect(string hostname, int port);
        public static ValueTask<TcpConnection> ConnectAsync(string hostname, int port, CancellationToken cancellationToken = default);

        // Note, the returned TcpListener is already started
        public static TcpListener Listen(IPAddress address, int port, int backlog = 100);
    }

    // Existing class
    public class TcpListener
    {
        public TcpConnection AcceptConnection();
        public ValueTask<TcpConnection> AcceptConnectionAsync(CancellationToken cancellationToken = default);
    }    
}

The following are obsoleted:
(1) TcpClient class
(2) TcpListener.AcceptTcpClient[Async], Start(), existing constructors, etc.

API Usage

Client example:

    TcpConnection connection = await Tcp.ConnectAsync("www.microsoft.com", 80);
    Console.WriteLine($"Established connection from {connection.LocalAddress}:{connection.LocalPort} to {connection.RemoteAddress}:{connection.RemotePort}");

    // Do something with the connection

Server example:

    TcpListener listener = Tcp.Listen(IPAddress.Any, 80);
    Console.WriteLine($"Server listening on {listener.ListenAddress()}:{listener.ListenPort()}");

    while (true)
    {
        TcpConnection connection = await listener.AcceptConnectionAsync();
        Console.WriteLine($"Accepted connection from {connection.RemoteAddress}:{connection.RemotePort} to {connection.LocalAddress}:{connection.RemotePort}");

        // Do something with the connection
    }

Example for both client and server:

    using (TcpConnection connection = ...)
    {
        // Configure TCP connection before we use it
        connection.NoDelay = true;
        connection.ReceiveBufferSize = MyReceiveBufferSize;
        connection.SendBufferSize = MySendBufferSize;

        // Perform reads and writes here

        // Half-close the connection
        connection.Shutdown(SocketShutdown.Send);

        // Read any remaining data from peer
    }

Alternative Designs

Some of the methods/properties defined on TcpConnection above may make more sense on NetworkStream, since they are not specific to TCP. E.g. Send/ReceiveBufferSize. Since TcpConnection derives from NetworkStream, these will be available to users of TcpConnection either way.

Another alternative is to just obsolete TcpClient and TcpListener entirely and tell users to always use Sockets. I don't think this is ideal; I think there's value in having simple APIs specifically for TCP. But if we don't think there is, then let's please obsolete the existing awful APIs and point people in the right direction.

Related

We should consider similar API updates for SSL, UDP, and Unix Domain Sockets.

See also these related TcpListener issues: #63114, #63115, #63117

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions