初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
|
||||
public interface IRequest<out TResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the request type string, e.g., "c:ping".
|
||||
/// </summary>
|
||||
string RequestType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Writes the request body to the provided buffer writer.
|
||||
/// </summary>
|
||||
/// <param name="writer">The buffer to write to.</param>
|
||||
void WriteRequestBody(IBufferWriter<byte> writer);
|
||||
|
||||
/// <summary>
|
||||
/// Parses the response body from the given memory span.
|
||||
/// </summary>
|
||||
/// <param name="responseBody">The raw response body.</param>
|
||||
/// <returns>The parsed response object.</returns>
|
||||
TResponse ParseResponseBody(ReadOnlyMemory<byte> responseBody);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
|
||||
public record ScaffoldingResponse(byte Status, ReadOnlyMemory<byte> Body);
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
using PCL.Core.Link.Scaffolding.Exceptions;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Framing;
|
||||
|
||||
internal static class ProtocolReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Read a response from the pipe.
|
||||
/// </summary>
|
||||
/// <param name="reader">The PipeReader to read from.</param>
|
||||
/// <param name="ct">The CancellationToken.</param>
|
||||
/// <returns>A task representing the asynchronous operation, with a ScaffoldingResponse as the result.</returns>
|
||||
/// <exception cref="ScaffoldingRequestException">Thrown if the request fails due to an invalid status.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the connection is closed unexpectedly.</exception>
|
||||
public static async ValueTask<ScaffoldingResponse> ReadResponseAsync(
|
||||
PipeReader reader,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var result = await reader.ReadAsync(ct).ConfigureAwait(false);
|
||||
var buffer = result.Buffer;
|
||||
|
||||
if (_TryParseResponse(ref buffer, out var response))
|
||||
{
|
||||
reader.AdvanceTo(buffer.Start);
|
||||
return response;
|
||||
}
|
||||
|
||||
reader.AdvanceTo(result.Buffer.Start, result.Buffer.End);
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
throw new InvalidOperationException("Connection closed unexpectedly.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a response from the given buffer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer containing the response data.</param>
|
||||
/// <param name="response">The parsed ScaffoldingResponse.</param>
|
||||
/// <returns>True if the response was successfully parsed; otherwise, false.</returns>
|
||||
/// <exception cref="ScaffoldingRequestException">Thrown if the request fails due to an invalid status.</exception>
|
||||
private static bool _TryParseResponse(ref ReadOnlySequence<byte> buffer, out ScaffoldingResponse response)
|
||||
{
|
||||
response = null!;
|
||||
if (buffer.Length < 5)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> header = stackalloc byte[5];
|
||||
buffer.Slice(0, 5).CopyTo(header);
|
||||
|
||||
var status = header[0];
|
||||
var bodyLength = BinaryPrimitives.ReadUInt32BigEndian(header[1..]);
|
||||
|
||||
var fullPacketLength = 5 + bodyLength;
|
||||
if (buffer.Length < fullPacketLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bodyBuffer = buffer.Slice(5, bodyLength);
|
||||
var body = bodyBuffer.ToArray();
|
||||
|
||||
buffer = buffer.Slice(fullPacketLength);
|
||||
|
||||
response = new ScaffoldingResponse(status, body);
|
||||
|
||||
if (response.Status != 0)
|
||||
{
|
||||
var serverMessage = response.Status == 255 ? Encoding.UTF8.GetString(response.Body.Span) : null;
|
||||
throw new ScaffoldingRequestException(response.Status, serverMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Framing;
|
||||
|
||||
internal static class ProtocolWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Write message to server.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Request body object type.</typeparam>
|
||||
/// <exception cref="InvalidOperationException">Thrown if request type is too long.</exception>
|
||||
/// <exception cref="OperationCanceledException">Thrown if operation is canceled.</exception>
|
||||
public static async ValueTask WriteRequestAsync<T>(
|
||||
PipeWriter writer,
|
||||
IRequest<T> request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var bodyWriter = new ArrayBufferWriter<byte>();
|
||||
request.WriteRequestBody(bodyWriter);
|
||||
var requestBody = bodyWriter.WrittenMemory;
|
||||
|
||||
var requestTypeBytes = Encoding.ASCII.GetBytes(request.RequestType);
|
||||
if (requestTypeBytes.Length > byte.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException("Request type is too long.");
|
||||
}
|
||||
|
||||
writer.GetSpan(1)[0] = (byte)requestTypeBytes.Length;
|
||||
writer.Advance(1);
|
||||
|
||||
writer.Write(requestTypeBytes);
|
||||
|
||||
var lengthSpan = writer.GetSpan(4);
|
||||
BinaryPrimitives.WriteUInt32BigEndian(lengthSpan, (uint)requestBody.Length);
|
||||
writer.Advance(4);
|
||||
|
||||
if (!requestBody.IsEmpty)
|
||||
{
|
||||
writer.Write(requestBody.Span);
|
||||
}
|
||||
|
||||
var result = await writer.FlushAsync(ct).ConfigureAwait(false);
|
||||
if (result.IsCanceled)
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Models;
|
||||
|
||||
public record LobbyInfo(string FullCode, string NetworkName, string NetworkSecret);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Models;
|
||||
|
||||
public enum LobbyType
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
PCLCE,
|
||||
Terracotta,
|
||||
Scaffolding
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Models;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum PlayerKind
|
||||
{
|
||||
HOST,
|
||||
GUEST
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Models;
|
||||
|
||||
public record PlayerProfile
|
||||
{
|
||||
[JsonPropertyName("name")] public required string Name { get; init; }
|
||||
[JsonPropertyName("machine_id")] public required string MachineId { get; init; }
|
||||
[JsonPropertyName("vendor")] public required string Vendor { get; init; }
|
||||
|
||||
[JsonPropertyName("kind")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public PlayerKind? Kind { get; init; }
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Requests;
|
||||
|
||||
public sealed class GetPlayerProfileListRequest : IRequest<IReadOnlyList<PlayerProfile>>
|
||||
{
|
||||
private static readonly JsonSerializerOptions _JsonOptions = new(JsonCompat.SerializerOptions)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:player_profiles_list";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteRequestBody(IBufferWriter<byte> writer)
|
||||
{
|
||||
// empty request body
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<PlayerProfile> ParseResponseBody(ReadOnlyMemory<byte> responseBody)
|
||||
{
|
||||
var profiles = JsonSerializer.Deserialize<IReadOnlyList<PlayerProfile>>(responseBody.Span, _JsonOptions);
|
||||
return profiles ?? ArraySegment<PlayerProfile>.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Requests;
|
||||
|
||||
public sealed class GetProtocolsRequest(IEnumerable<string> supportProtocols) : IRequest<IReadOnlyList<string>>
|
||||
{
|
||||
private static readonly byte[] _Separator = [(byte)'\0'];
|
||||
private readonly IEnumerable<string> _supportProtocols = supportProtocols;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:protocols";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteRequestBody(IBufferWriter<byte> writer)
|
||||
{
|
||||
var protocolStr = string.Join('\0', _supportProtocols);
|
||||
var bytes = Encoding.ASCII.GetBytes(protocolStr);
|
||||
|
||||
writer.Write(bytes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> ParseResponseBody(ReadOnlyMemory<byte> responseBody)
|
||||
{
|
||||
if (responseBody.IsEmpty)
|
||||
{
|
||||
return ArraySegment<string>.Empty;
|
||||
}
|
||||
|
||||
var responseStr = Encoding.ASCII.GetString(responseBody.Span);
|
||||
return responseStr.Split('\0', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Requests;
|
||||
|
||||
public sealed class GetServerPortRequest : IRequest<ushort>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:server_port";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteRequestBody(IBufferWriter<byte> writer)
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ushort ParseResponseBody(ReadOnlyMemory<byte> responseBody)
|
||||
{
|
||||
if (responseBody.Length != 2)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid response body for server port.");
|
||||
}
|
||||
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(responseBody.Span);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Requests;
|
||||
|
||||
public sealed class PingRequest : IRequest<ReadOnlyMemory<byte>>
|
||||
{
|
||||
private readonly ReadOnlyMemory<byte> _payload;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:ping";
|
||||
|
||||
public PingRequest(ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
if (payload.Length >= 32)
|
||||
{
|
||||
throw new ArgumentException("Payload must be less than 32 bytes.", nameof(payload));
|
||||
}
|
||||
|
||||
_payload = payload;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteRequestBody(IBufferWriter<byte> writer)
|
||||
{
|
||||
writer.Write(_payload.Span);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyMemory<byte> ParseResponseBody(ReadOnlyMemory<byte> responseBody)
|
||||
{
|
||||
return responseBody;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client.Requests;
|
||||
|
||||
public sealed class PlayerPingRequest(string name, string machineId, string vendor) : IRequest<bool>
|
||||
{
|
||||
private readonly PlayerProfile _profile = new()
|
||||
{
|
||||
Name = name,
|
||||
MachineId = machineId,
|
||||
Vendor = vendor
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _JsonOptions = new(JsonCompat.SerializerOptions)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:player_ping";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteRequestBody(IBufferWriter<byte> writer)
|
||||
{
|
||||
using var jsonWriter = new Utf8JsonWriter(writer);
|
||||
JsonSerializer.Serialize(jsonWriter, _profile, _JsonOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ParseResponseBody(ReadOnlyMemory<byte> responseBody)
|
||||
{
|
||||
// An empty response body indicates success.
|
||||
return responseBody.IsEmpty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using PCL.Core.Link.Scaffolding.Client.Abstractions;
|
||||
using PCL.Core.Link.Scaffolding.Client.Framing;
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using PCL.Core.Link.Scaffolding.Client.Requests;
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Client;
|
||||
|
||||
internal enum ClientState
|
||||
{
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Handshaking, // 正在进行握手
|
||||
Connected, // 握手成功,准备就绪
|
||||
Disposing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A client for the Scaffolding data exchange protocol.
|
||||
/// </summary>
|
||||
public sealed class ScaffoldingClient(string host, int scfPort, string playerName, string machineId, string vendor)
|
||||
: IAsyncDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _srLock = new(1, 1);
|
||||
private TcpClient? _tcpClient;
|
||||
private PipeReader? _pipeReader;
|
||||
private PipeWriter? _pipeWriter;
|
||||
|
||||
// Heart Beat
|
||||
private Task? _heartbeatTask;
|
||||
private readonly PlayerPingRequest _playerPingRequest = new(playerName, machineId, vendor);
|
||||
private CancellationTokenSource? _heartbeatCts;
|
||||
private readonly Stopwatch _heartbeatTimer = new();
|
||||
|
||||
private ClientState _state = ClientState.Disconnected;
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a heartbeat signal is received, providing the current list of player profiles and the elapsed time
|
||||
/// since the last heartbeat.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Subscribers can use this event to monitor player activity or synchronize state at regular
|
||||
/// intervals. The event provides a read-only list of player profiles and an integer representing the elapsed time,
|
||||
/// typically in milliseconds or seconds, depending on the implementation.
|
||||
/// </remarks>
|
||||
public event Action<IReadOnlyList<PlayerProfile>, long>? Heartbeat;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the server has been shut down or is unreachable.
|
||||
/// </summary>
|
||||
public event Action? ServerShuttedDown;
|
||||
|
||||
#endregion
|
||||
|
||||
public IReadOnlyList<PlayerProfile>? PlayerList;
|
||||
|
||||
public bool IsConnected => _state == ClientState.Connected;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to a Scaffolding server.
|
||||
/// </summary>
|
||||
/// <exception cref="Exception">Throws if fialed to connect to server.</exception>
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_state is not ClientState.Disconnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_tcpClient = new TcpClient();
|
||||
try
|
||||
{
|
||||
_state = ClientState.Connecting;
|
||||
|
||||
LogWrapper.Info("Scaffolding", $"Trying to connect to server: {host}:{scfPort}");
|
||||
|
||||
await _tcpClient.ConnectAsync(host, scfPort, ct).ConfigureAwait(false);
|
||||
|
||||
var stream = _tcpClient.GetStream();
|
||||
_pipeReader = PipeReader.Create(stream);
|
||||
_pipeWriter = PipeWriter.Create(stream);
|
||||
|
||||
_state = ClientState.Handshaking;
|
||||
LogWrapper.Info("Scaffolding", "Connecting established. Performing handshake...");
|
||||
|
||||
await SendRequestAsync(_playerPingRequest, ct).ConfigureAwait(false);
|
||||
|
||||
_state = ClientState.Connected;
|
||||
|
||||
_StartHeartbeats();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "ScaffoldingClient", "Failed to connect to server.");
|
||||
|
||||
await DisposeAsync().ConfigureAwait(false);
|
||||
ServerShuttedDown?.Invoke();
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void _StartHeartbeats()
|
||||
{
|
||||
_heartbeatCts = new CancellationTokenSource();
|
||||
_heartbeatTask = _HeartbeatLoopAsync(_heartbeatCts.Token);
|
||||
}
|
||||
|
||||
private async Task _HeartbeatLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false);
|
||||
|
||||
_heartbeatTimer.Start();
|
||||
await SendRequestAsync(_playerPingRequest, ct).ConfigureAwait(false);
|
||||
_heartbeatTimer.Stop();
|
||||
|
||||
var letancy = _heartbeatTimer.ElapsedMilliseconds;
|
||||
_heartbeatTimer.Reset();
|
||||
|
||||
PlayerList = await SendRequestAsync(new GetPlayerProfileListRequest(), ct).ConfigureAwait(false);
|
||||
|
||||
Heartbeat?.Invoke(PlayerList, letancy);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "[ScaffoldingClient]",
|
||||
"Failed when sending heartbeat message. Maybe the server has been shut down.");
|
||||
|
||||
ServerShuttedDown?.Invoke();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send TCP request to the Scaffolding server.
|
||||
/// </summary>
|
||||
/// <param name="request">Thr request type.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <typeparam name="TResponse">Response type.</typeparam>
|
||||
/// <exception cref="InvalidOperationException">Throws when server is not ready.</exception>
|
||||
public async Task<TResponse> SendRequestAsync<TResponse>(
|
||||
IRequest<TResponse> request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (_state < ClientState.Handshaking)
|
||||
{
|
||||
throw new InvalidOperationException("Client is not connected.");
|
||||
}
|
||||
|
||||
if (_pipeWriter is null || _pipeReader is null)
|
||||
{
|
||||
throw new InvalidOperationException("Client is not connected.");
|
||||
}
|
||||
|
||||
await _srLock.WaitAsync(ct).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await ProtocolWriter.WriteRequestAsync(_pipeWriter, request, ct).ConfigureAwait(false);
|
||||
var response = await ProtocolReader.ReadResponseAsync(_pipeReader, ct).ConfigureAwait(false);
|
||||
|
||||
return request.ParseResponseBody(response.Body);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_srLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_state is ClientState.Disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_state = ClientState.Disposing;
|
||||
|
||||
|
||||
await CastAndDispose(_srLock).ConfigureAwait(false);
|
||||
if (_tcpClient is not null) await CastAndDispose(_tcpClient).ConfigureAwait(false);
|
||||
if (_heartbeatCts is not null)
|
||||
{
|
||||
await _heartbeatCts.CancelAsync().ConfigureAwait(false);
|
||||
await CastAndDispose(_heartbeatCts).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_heartbeatTask is not null)
|
||||
{
|
||||
await _heartbeatTask.ConfigureAwait(false);
|
||||
await CastAndDispose(_heartbeatTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
static async ValueTask CastAndDispose(IDisposable resource)
|
||||
{
|
||||
if (resource is IAsyncDisposable resourceAsyncDisposable)
|
||||
await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false);
|
||||
else
|
||||
resource.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user