-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[API Proposal]: Simple, modern TCP APIs #63162
Comments
Tagging subscribers to this area: @dotnet/ncl Issue DetailsBackground and motivationWhen 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 making progress. 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: API UsageClient example: TcpConnection connection = await Tcp.ConnectAsync("www.microsoft.com", 80);
Console.WriteLine($"Established connection from {connection.LocalIPAddress}:{connection.LocalPort} to {connection.RemoteIPAddress}:{connection.RemotePort}");
// Do something with the connection Server example: TcpListener listener = Tcp.Listen(IPAddress.Any, 80);
Console.WriteLine($"Server listening on {listener.ListenIPAddress()}:{listener.ListenPort()}");
while (true)
{
TcpConnection connection = await listener.AcceptTcpConnectionAsync();
Console.WriteLine($"Accepted connection from {connection.RemoteIPAddress}:{connection.RemotePort} to {connection.LocalIPAddress}:{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 DesignsSome 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. RisksNo response
|
|
Side note: I think you should look into what Apple did with the Network.framework APIs. They aimed to simplify similar scenarios but also have a concept of layering to implement things like TLS or application-level protocols. The main reason I am suggesting that though is that on newer Apple platforms the Network.framework is the only supported API [bundled with the system] for establishing TLS (over UDP/TCP) connections. The current |
Instead of a static class should this be something like |
Note that this violates Framework Design Guidelines, which say:
|
This comment has been minimized.
This comment has been minimized.
(Though I tend to agree with @filipnavara that extending |
IMHO if a "modern" TCP API was added, it should operate similarly to the types in the |
@geoffkizer doesn't #1793 already cover this? |
At first sight #1793 is much more like the Apple API model. |
Though I mostly agree with the proposal, I think we should investigate synergies with #1793, and potentially merge the two. If - for some reason - we decide to implement Connection Abstractions for let's say .NET 8, we'll end up having two sets of very similar API's for TCP connection creation, which will generate some confusion. Alternatively we may decide that we don't want #1793 to be part of the BCL and close it. |
Can we see some side-by-side examples where this will have the best impact? |
I find your rant very refreshing 😆. So I'm not the only one who finds
|
The overall Socket API is bad for out of the box use. These are the kinds of contraptions one has to create on top of .net sockets currently to normalize the design: https://github.com/tactical-drone/zero/tree/net-standard/zero.core/network/ip The amount of work seems excessive. Then there are still other problems. Having to |
That is not necessarily true. Since we use SafeHandle under the cover One that note, we tend to provide synchronous versions for compatibility. If anything, I would question that for the new API. There is significant complication (at least on Unix) for mixing the modes. |
I did look at the link @tactical-drone but lot of the code does not make sense to me - like I did more searching within runtime and found TlsStream that wraps That other proposal also brings concept of connect policy. People trying to prefer given address family, or some way how to impact interface or address used. [Flags]
public enum ConnectOptions
{
Default = 0,
IPv4Only,
IPv6Only,
Parallel, // Happy-Eyeball
FastOpen,
...
} I'm wondering if anybody here would find it interesting. For example, the |
Lastly, the #1793 is dead as right now. I think it may be because it tried to solve everything. While related, I see this proposal as way how to make writing simple networking endpoints easier. In order to do that, I think we should focus on simplicity and consistency. If combined with TLS it may go beyond the |
public static ValueTask<TcpConnection> ConnectAsync(string hostname, int port, SslClientAuthenticationOptions? sslOptions = null, CancellationToken cancellationToken = default);
public ValueTask<TcpConnection> AcceptConnectionAsync(SslServerAuthenticationOptions? sslOptions = null, CancellationToken cancellationToken = default); that would optionally negotiate TLS if |
A few thoughts:
public sealed class TcpConnectionOptions
{
public IPEndPoint RemoteEndPoint { get; set; }
public bool NoDelay { get; set; }
public int SendBufferSize { get; set; }
public int ReceiveBufferSize { get; set; }
// APIs for TCP Keepalive etc.
}
public static class Tcp
{
ValueTask<TcpStream> ConnectAsync(TcpConnectionOptions options, CancellationToken cancellationToken = default);
} This would require merging namespace System.Net.Security;
public static class NetworkStreamExtensions
{
public static async ValueTask<SslStream> AuthenticateAsClientSslStream(this NetworkStream networkStream, SslClientAuthenticationOptions options);
} |
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.)
The following are obsoleted:
(1) TcpClient class
(2) TcpListener.AcceptTcpClient[Async], Start(), existing constructors, etc.
API Usage
Client example:
Server example:
Example for both client and server:
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
The text was updated successfully, but these errors were encountered: