-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Closed
Labels
api-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementationarea-System.Net.Sockets
Milestone
Description
This proposal eliminates allocations for connectionless use of Socket
. It augments the SocketAddress
class to allow reuse across operations, becoming a high-perf alternative to EndPoint
.
Rationale and Usage
APIs which need to translate between IPEndPoint
and native sockaddr
structures are performing a large amount of defensive copying and layering workarounds.
This affects UDP performance and contributes to excessive GC. A simple example is:
Socket socket = ...;
byte[] buffer = ...;
var remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref remoteEndPoint);
socket.SendTo(buffer, 0, buffer.Length, SocketFlags.None, remoteEndPoint);
These two calls allocate 12 times:
- 3x
IPEndPoint
- 3x
IPAddress
- 3x
SocketAddress
- 3x
byte[]
...6x if IPv6
See also: #30196
New usage has 0 allocations:
Socket socket = ...;
byte[] buffer = ...;
var remoteAddress = new SocketAddress(AddressFamily.InterNetwork);
socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, remoteAddress);
socket.SendTo(buffer, 0, buffer.Length, SocketFlags.None, remoteAddress);
Proposed API
class Socket
{
public int ReceiveFrom(Span<byte> buffer, SocketFlags socketFlags, SocketAddress socketAddress);
public ValueTask<int> ReceiveFromAsync(Memory<byte> buffer, SocketFlags socketFlags, SocketAddress socketAddress, CancellationToken cancellationToken = default);
public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags socketFlags, SocketAddress socketAddress);
public ValueTask<int> SendToAsync(ReadOnlyMemory<byte> buffer, SocketFlags socketFlags, SocketAddress socketAddress, CancellationToken cancellationToken = default);
}
class SocketAsyncEventArgs
{
// Only one of RemoteEndPoint or RemoteAddress must be specified.
public SocketAddress RemoteAddress { get; set; }
}
class SocketAddress
{
// If we can merge System.Net.Primitives and System.Net.Sockets, these two methods are unnecessary. That would be ideal.
public static void GetBuffer(SocketAddress address, out byte[] buffer);
public static void SetSockaddrSize(SocketAddress address, int size);
}
class EndPoint
{
public virtual void SerializeTo(SocketAddress socketAddress); // Already has "SocketAddress Serialize()"; default would be to call that and copy.
}
Details
- It is intended that UDP servers will use
SocketAddress
as a dictionary key to lookup client state, to avoid first converting toEndPoint
.- It is assumed that users will only rarely care to actually get the IP/port/etc. from the
SocketAddress
. This duty continues to be delegated toEndPoint
. - If users ever want to deserialize a
SocketAddress
into anEndPoint
, they can already useEndPoint.Create
. - Need to ensure only the actual sockaddr structure is compared/hashed, not the entire byte buffer.
- It is assumed that users will only rarely care to actually get the IP/port/etc. from the
SocketAddress
is currently duplicated in System.Net.Primitives and System.Net.Sockets to avoid exposing its internal buffer. This change will allow avoiding duplication.- This relies on users not using a
SocketAddress
until the I/O is finished.- This is a bit safer with
EndPoint
as we can take defensive copies before methods return.
- This is a bit safer with
- We can currently do some optimizations to avoid all allocations for
SendTo
andSendToAsync
IPv4/IPv6 with some special casing, so this API would primarily be to optimizeReceiveFrom
variants as well as (less important) allowing non-IPv4/IPv6 protocols to benefit. Still, if we were to add an API forReceiveFrom
we would probably want an API onSendTo
for symmetry.
Open Questions
- We've put some effort into not doing something like this before. It would be great to understand why. Currently:
- We duplicate the
SocketAddress
class in multiple assemblies to avoid exposing its buffer, and have a step to marshal (byte-by-byte) between the two implementations. - Tons of APIs take
EndPoint
, it's a nice abstraction that we wanted here despite performance implications.
- We duplicate the
- It isn't immediately obvious from the API surface that
socketAddress
is written to byReceiveFrom
. Is there a better way we can indicate this? - The two new methods on
SocketAddress
exist purely because System.Net.Sockets needs access to internals in System.Net.Primitives. Any thoughts on how to avoid exposing these "pubternal" bits?- One option is to merge System.Net.Primitives and System.Net.Sockets; I don't see harm in this but that is a much larger discussion :)
- If we merge the Primitives and Sockets assemblies, we can get rid of some of the allocations for
ReceiveFrom
without making any API changes. It's not a perfect solution but might be good enough.
Related Issues
There are two additional issues to update our APIs with ValueTask
/Span
/Memory
that this will need to be consistent with:
- Update
Socket
class add Span overloads for Socket datagram functions #938 - Update
UdpClient
class UdpClient - add Span support #864
karaziox, patricksuo, ZOXEXIVO, billknye, am11 and 60 moresgfTwoTenPvP, macaba, rzikm, ryanthedev, sandersaares and 24 moresgf
Metadata
Metadata
Assignees
Labels
api-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementationarea-System.Net.Sockets