Skip to content

Commit a119aed

Browse files
authored
Merge pull request #16 from smdn/transport-abstraction-ifcs
Introduce transport layer abstraction interfaces
2 parents af1b81b + 197af95 commit a119aed

21 files changed

+1524
-295
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// SPDX-FileCopyrightText: 2025 smdn <smdn@smdn.jp>
2+
// SPDX-License-Identifier: MIT
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Net;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
namespace Smdn.Net.MuninNode.Transport;
11+
12+
/// <summary>
13+
/// Provides an interface that abstracts the client implementation of
14+
/// the transport layer that connects to the <c>Munin-Node</c>.
15+
/// </summary>
16+
/// <seealso cref="IMuninNodeListener"/>
17+
public interface IMuninNodeClient : IDisposable, IAsyncDisposable {
18+
/// <summary>
19+
/// Gets the <see cref="EndPoint"/> that is bound with this instance.
20+
/// </summary>
21+
/// <exception cref="ObjectDisposedException">The client has been disposed.</exception>
22+
/// <value>
23+
/// <see langword="null"/> if this client does not have <see cref="EndPoint"/>.
24+
/// </value>
25+
/// <remarks>
26+
/// The value of this property will be used by the <see cref="IAccessRule"/> interface
27+
/// to determine if the client is accessible.
28+
/// </remarks>
29+
/// <seealso cref="IAccessRule.IsAcceptable(IPEndPoint)"/>
30+
EndPoint? EndPoint { get; }
31+
32+
/// <summary>
33+
/// Disconnects the active connection.
34+
/// </summary>
35+
/// <param name="cancellationToken">
36+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
37+
/// </param>
38+
/// <exception cref="ObjectDisposedException">The client has been disposed.</exception>
39+
/// <returns>
40+
/// The <see cref="ValueTask"/> that represents the asynchronous operation,
41+
/// disconnecting the active connection.
42+
/// </returns>
43+
ValueTask DisconnectAsync(CancellationToken cancellationToken);
44+
45+
/// <summary>
46+
/// Waits to receive a response from the server and writes the received data into a buffer.
47+
/// </summary>
48+
/// <param name="buffer">
49+
/// The <see cref="IBufferWriter{Byte}"/> for writing received data.
50+
/// </param>
51+
/// <param name="cancellationToken">
52+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
53+
/// </param>
54+
/// <exception cref="ObjectDisposedException">The client has been disposed.</exception>
55+
/// <returns>
56+
/// The <see cref="ValueTask"/> that represents the asynchronous operation,
57+
/// returning the number of bytes received.
58+
/// </returns>
59+
ValueTask<int> ReceiveAsync(IBufferWriter<byte> buffer, CancellationToken cancellationToken);
60+
61+
/// <summary>
62+
/// Sends a request to the server.
63+
/// </summary>
64+
/// <param name="buffer">
65+
/// The <see cref="ReadOnlyMemory{Byte}"/> that contains the data to be sent.
66+
/// </param>
67+
/// <param name="cancellationToken">
68+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
69+
/// </param>
70+
/// <exception cref="ObjectDisposedException">The client has been disposed.</exception>
71+
/// <returns>
72+
/// The <see cref="ValueTask"/> that represents the asynchronous operation,
73+
/// sending the requested data.
74+
/// </returns>
75+
ValueTask SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken);
76+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-FileCopyrightText: 2025 smdn <smdn@smdn.jp>
2+
// SPDX-License-Identifier: MIT
3+
4+
using System;
5+
using System.Net;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Smdn.Net.MuninNode.Transport;
10+
11+
/// <summary>
12+
/// Provides an interface that abstracts the listener implementation of
13+
/// the transport layer that accepts the connections to the <c>Munin-Node</c>.
14+
/// </summary>
15+
/// <seealso cref="IMuninNodeClient"/>
16+
/// <seealso cref="IMuninNodeListenerFactory"/>
17+
public interface IMuninNodeListener : IDisposable, IAsyncDisposable {
18+
/// <summary>
19+
/// Gets the <see cref="EndPoint"/> that is bound with this instance.
20+
/// </summary>
21+
/// <exception cref="ObjectDisposedException">The instance has been disposed.</exception>
22+
/// <exception cref="InvalidOperationException">The instance is not started yet.</exception>
23+
/// <value>
24+
/// <see langword="null"/> if this instance does not have <see cref="EndPoint"/>.
25+
/// </value>
26+
EndPoint? EndPoint { get; }
27+
28+
/// <summary>
29+
/// Start the listener and enable to accept connections from clients.
30+
/// </summary>
31+
/// <param name="cancellationToken">
32+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
33+
/// </param>
34+
/// <returns>
35+
/// The <see cref="ValueTask"/> that represents the asynchronous operation,
36+
/// starting the listener.
37+
/// </returns>
38+
ValueTask StartAsync(CancellationToken cancellationToken);
39+
40+
/// <summary>
41+
/// Waits for a single client and accepts an incoming connection.
42+
/// </summary>
43+
/// <param name="cancellationToken">
44+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
45+
/// </param>
46+
/// <returns>
47+
/// The <see cref="ValueTask{IMuninNodeClient}"/> that represents the asynchronous operation,
48+
/// creating the <see cref="IMuninNodeClient"/> representing the accepted client.
49+
/// </returns>
50+
ValueTask<IMuninNodeClient> AcceptAsync(CancellationToken cancellationToken);
51+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-FileCopyrightText: 2025 smdn <smdn@smdn.jp>
2+
// SPDX-License-Identifier: MIT
3+
4+
using System.Net;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace Smdn.Net.MuninNode.Transport;
9+
10+
/// <summary>
11+
/// Provides an interface that abstracts the factory for creating the client
12+
/// listener implementation of the transport layer used by <c>Munin-Node</c>.
13+
/// </summary>
14+
/// <seealso cref="IMuninNodeListener"/>
15+
public interface IMuninNodeListenerFactory {
16+
/// <summary>
17+
/// Creates and returns a <see cref="IMuninNodeListener"/> for the specific <c>Munin-Node</c>.
18+
/// </summary>
19+
/// <param name="endPoint">
20+
/// The <see cref="EndPoint"/> that will be bound with the <see cref="IMuninNodeListener"/> used by the <c>Munin-Node</c> to be created.
21+
/// </param>
22+
/// <param name="node">
23+
/// The <see cref="IMuninNode"/> representing the <c>Munin-Node</c> where the <see cref="IMuninNodeListener"/> to be created will be used.
24+
/// </param>
25+
/// <param name="cancellationToken">
26+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
27+
/// </param>
28+
/// <remarks>
29+
/// The <paramref name="node"/> parameter should not be used to determine an endpoint to bind to.
30+
/// It is intended to be used as a key for the management and identification of the
31+
/// <see cref="IMuninNodeListener"/> that has been created.
32+
/// </remarks>
33+
/// <returns>
34+
/// The <see cref="ValueTask{IMuninNodeListener}"/> that represents the asynchronous operation,
35+
/// creating the <see cref="IMuninNodeListener"/> bound to the <paramref name="endPoint"/>.
36+
/// </returns>
37+
ValueTask<IMuninNodeListener> CreateAsync(
38+
EndPoint endPoint,
39+
IMuninNode node,
40+
CancellationToken cancellationToken
41+
);
42+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// SPDX-FileCopyrightText: 2025 smdn <smdn@smdn.jp>
2+
// SPDX-License-Identifier: MIT
3+
4+
// TODO: use LoggerMessage.Define
5+
#pragma warning disable CA1848 // For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])'
6+
7+
using System;
8+
using System.Buffers;
9+
using System.Net;
10+
using System.Net.Sockets;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
14+
using Microsoft.Extensions.Logging;
15+
16+
namespace Smdn.Net.MuninNode.Transport;
17+
18+
internal sealed class MuninNodeClient : IMuninNodeClient {
19+
public EndPoint EndPoint => client?.RemoteEndPoint ?? throw new ObjectDisposedException(GetType().FullName);
20+
21+
private Socket client;
22+
private readonly ILogger? logger;
23+
24+
internal MuninNodeClient(
25+
Socket client,
26+
ILogger? logger
27+
)
28+
{
29+
this.client = client ?? throw new ArgumentNullException(nameof(client));
30+
this.logger = logger;
31+
}
32+
33+
public void Dispose()
34+
{
35+
Dispose(true);
36+
GC.SuppressFinalize(this);
37+
}
38+
39+
public async ValueTask DisposeAsync()
40+
{
41+
await DisposeAsyncCore().ConfigureAwait(false);
42+
43+
Dispose(disposing: false);
44+
GC.SuppressFinalize(this);
45+
}
46+
47+
private ValueTask DisposeAsyncCore()
48+
{
49+
client?.Close();
50+
client?.Dispose();
51+
client = null!;
52+
53+
return default;
54+
}
55+
56+
// protected virtual
57+
private void Dispose(bool disposing)
58+
{
59+
if (!disposing)
60+
return;
61+
62+
client?.Close();
63+
client?.Dispose();
64+
client = null!;
65+
}
66+
67+
public ValueTask DisconnectAsync(CancellationToken cancellationToken)
68+
{
69+
if (client is null)
70+
throw new ObjectDisposedException(GetType().FullName);
71+
72+
return DisposeAsync();
73+
}
74+
75+
public async ValueTask<int> ReceiveAsync(IBufferWriter<byte> buffer, CancellationToken cancellationToken)
76+
{
77+
if (client is null)
78+
throw new ObjectDisposedException(GetType().FullName);
79+
80+
// holds a reference to the endpoint before the client being disposed
81+
var remoteEndPoint = client.RemoteEndPoint;
82+
83+
const int ReceiveChunkSize = 256;
84+
var totalByteCount = 0;
85+
86+
for (; ; ) {
87+
try {
88+
cancellationToken.ThrowIfCancellationRequested();
89+
90+
if (!client.Connected)
91+
return 0;
92+
93+
var memory = buffer.GetMemory(ReceiveChunkSize);
94+
95+
var byteCount = await client.ReceiveAsync(
96+
buffer: memory,
97+
socketFlags: SocketFlags.None,
98+
cancellationToken: cancellationToken
99+
).ConfigureAwait(false);
100+
101+
buffer.Advance(byteCount);
102+
103+
totalByteCount += byteCount;
104+
105+
if (byteCount < memory.Length)
106+
return totalByteCount;
107+
}
108+
catch (SocketException ex) when (
109+
ex.SocketErrorCode is
110+
SocketError.OperationAborted or // ECANCELED (125)
111+
SocketError.ConnectionReset // ECONNRESET (104)
112+
) {
113+
logger?.LogDebug(
114+
"[{RemoteEndPoint}] expected socket exception ({NumericSocketErrorCode} {SocketErrorCode})",
115+
remoteEndPoint,
116+
(int)ex.SocketErrorCode,
117+
ex.SocketErrorCode
118+
);
119+
120+
break; // expected exception
121+
}
122+
catch (ObjectDisposedException) {
123+
logger?.LogDebug(
124+
"[{RemoteEndPoint}] socket has been disposed",
125+
remoteEndPoint
126+
);
127+
128+
break; // expected exception
129+
}
130+
}
131+
132+
return totalByteCount;
133+
}
134+
135+
public async ValueTask SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
136+
{
137+
if (client is null)
138+
throw new ObjectDisposedException(GetType().FullName);
139+
140+
try {
141+
_ = await client.SendAsync(
142+
buffer: buffer,
143+
socketFlags: SocketFlags.None,
144+
cancellationToken: cancellationToken
145+
).ConfigureAwait(false);
146+
}
147+
catch (SocketException ex) when (
148+
ex.SocketErrorCode is
149+
SocketError.Shutdown or // EPIPE (32)
150+
SocketError.ConnectionAborted or // WSAECONNABORTED (10053)
151+
SocketError.OperationAborted or // ECANCELED (125)
152+
SocketError.ConnectionReset // ECONNRESET (104)
153+
) {
154+
// expected exception in case of disconnection
155+
throw new MuninNodeClientDisconnectedException(
156+
message: "client disconnected",
157+
innerException: ex
158+
);
159+
}
160+
}
161+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-FileCopyrightText: 2025 smdn <smdn@smdn.jp>
2+
// SPDX-License-Identifier: MIT
3+
4+
using System;
5+
6+
namespace Smdn.Net.MuninNode.Transport;
7+
8+
/// <summary>
9+
/// The exception that is thrown when a connection is disconnected when responding to a <see cref="IMuninNodeClient"/>.
10+
/// </summary>
11+
public sealed class MuninNodeClientDisconnectedException : InvalidOperationException {
12+
public MuninNodeClientDisconnectedException()
13+
: base()
14+
{
15+
}
16+
17+
public MuninNodeClientDisconnectedException(string message)
18+
: base(message)
19+
{
20+
}
21+
22+
public MuninNodeClientDisconnectedException(string message, Exception innerException)
23+
: base(message, innerException)
24+
{
25+
}
26+
}

0 commit comments

Comments
 (0)