Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
- Implemented MigrationCategoryTests to validate migration categorization for startup, release, seed, and data migrations. - Added tests for edge cases, including null, empty, and whitespace migration names. - Created StartupMigrationHostTests to verify the behavior of the migration host with real PostgreSQL instances using Testcontainers. - Included tests for migration execution, schema creation, and handling of pending release migrations. - Added SQL migration files for testing: creating a test table, adding a column, a release migration, and seeding data.
32 KiB
32 KiB
Step 14: TCP Transport Implementation
Phase 3: Transport Layer Estimated Complexity: High Dependencies: Step 13 (InMemory Transport)
Overview
The TCP transport is the primary production transport for connecting microservices to the gateway. It provides reliable, ordered frame delivery over persistent connections with efficient binary framing, connection multiplexing, and automatic reconnection.
Goals
- Implement efficient binary frame encoding over TCP
- Support connection multiplexing for high throughput
- Implement automatic reconnection with exponential backoff
- Handle partial reads/writes correctly
- Integrate with .NET's socket pooling and buffer management
Wire Protocol
Frame Layout
┌────────────────────────────────────────────────────────────────┐
│ TCP Frame Format │
├──────────┬──────────┬──────────┬──────────┬───────────────────┤
│ Magic │ Flags │ Type │ Length │ Correlation │
│ (2B) │ (1B) │ (1B) │ (4B) │ ID (16B) │
├──────────┴──────────┴──────────┴──────────┴───────────────────┤
│ Payload │
│ (Length bytes) │
└────────────────────────────────────────────────────────────────┘
Total Header: 24 bytes
Magic: 0x53 0x52 ("SR" - Stella Router)
Flags: Compression, Final, Error
Type: Hello=1, Heartbeat=2, Request=3, Response=4, Cancel=5
Length: uint32, big-endian (max 16MB)
Correlation ID: 16 bytes (GUID)
Payload: Variable length
Core Types
TCP Frame Codec
namespace StellaOps.Router.Transport.Tcp;
/// <summary>
/// Encodes and decodes frames for TCP wire format.
/// </summary>
public sealed class TcpFrameCodec
{
private const ushort Magic = 0x5352; // "SR"
private const int HeaderSize = 24;
private const int MaxPayloadSize = 16 * 1024 * 1024; // 16MB
private readonly ArrayPool<byte> _bufferPool;
public TcpFrameCodec(ArrayPool<byte>? bufferPool = null)
{
_bufferPool = bufferPool ?? ArrayPool<byte>.Shared;
}
/// <summary>
/// Encodes a frame to wire format.
/// </summary>
public int Encode(Frame frame, Span<byte> destination)
{
if (destination.Length < HeaderSize + frame.Payload.Length)
throw new ArgumentException("Destination buffer too small");
var offset = 0;
// Magic (2 bytes)
BinaryPrimitives.WriteUInt16BigEndian(destination[offset..], Magic);
offset += 2;
// Flags (1 byte)
destination[offset++] = (byte)frame.Flags;
// Type (1 byte)
destination[offset++] = (byte)frame.Type;
// Length (4 bytes)
BinaryPrimitives.WriteUInt32BigEndian(destination[offset..], (uint)frame.Payload.Length);
offset += 4;
// Correlation ID (16 bytes)
if (Guid.TryParse(frame.CorrelationId, out var guid))
{
guid.TryWriteBytes(destination[offset..]);
}
offset += 16;
// Payload
frame.Payload.AsSpan().CopyTo(destination[offset..]);
offset += frame.Payload.Length;
return offset;
}
/// <summary>
/// Decodes a frame from wire format.
/// </summary>
public Frame Decode(ReadOnlySpan<byte> source)
{
if (source.Length < HeaderSize)
throw new ProtocolException("Incomplete frame header");
var offset = 0;
// Magic
var magic = BinaryPrimitives.ReadUInt16BigEndian(source[offset..]);
if (magic != Magic)
throw new ProtocolException($"Invalid magic: 0x{magic:X4}");
offset += 2;
// Flags
var flags = (FrameFlags)source[offset++];
// Type
var type = (FrameType)source[offset++];
// Length
var length = BinaryPrimitives.ReadUInt32BigEndian(source[offset..]);
if (length > MaxPayloadSize)
throw new ProtocolException($"Payload too large: {length}");
offset += 4;
// Correlation ID
var correlationId = new Guid(source.Slice(offset, 16)).ToString("N");
offset += 16;
// Verify we have full payload
if (source.Length < HeaderSize + length)
throw new ProtocolException("Incomplete payload");
// Payload
var payload = source.Slice(offset, (int)length).ToArray();
return new Frame
{
Type = type,
Flags = flags,
CorrelationId = correlationId,
Payload = payload
};
}
/// <summary>
/// Attempts to decode a frame from a buffer, returning bytes consumed.
/// </summary>
public bool TryDecode(ReadOnlySequence<byte> buffer, out Frame frame, out int bytesConsumed)
{
frame = default!;
bytesConsumed = 0;
if (buffer.Length < HeaderSize)
return false;
// Read header to get length
Span<byte> header = stackalloc byte[HeaderSize];
buffer.Slice(0, HeaderSize).CopyTo(header);
var length = BinaryPrimitives.ReadUInt32BigEndian(header[4..]);
var totalLength = HeaderSize + (int)length;
if (buffer.Length < totalLength)
return false;
// Decode full frame
var frameBytes = new byte[totalLength];
buffer.Slice(0, totalLength).CopyTo(frameBytes);
frame = Decode(frameBytes);
bytesConsumed = totalLength;
return true;
}
}
TCP Connection
namespace StellaOps.Router.Transport.Tcp;
/// <summary>
/// Represents a TCP connection with frame-based I/O.
/// </summary>
public sealed class TcpFrameConnection : IAsyncDisposable
{
private readonly Socket _socket;
private readonly NetworkStream _stream;
private readonly TcpFrameCodec _codec;
private readonly ILogger _logger;
private readonly SemaphoreSlim _writeLock = new(1, 1);
private readonly byte[] _readBuffer;
private readonly byte[] _writeBuffer;
private int _readBufferOffset;
private int _readBufferCount;
public string ConnectionId { get; }
public EndPoint? RemoteEndPoint => _socket.RemoteEndPoint;
public bool IsConnected => _socket.Connected;
public TcpFrameConnection(
Socket socket,
TcpFrameCodec codec,
ILogger logger)
{
_socket = socket;
_stream = new NetworkStream(socket, ownsSocket: false);
_codec = codec;
_logger = logger;
_readBuffer = new byte[64 * 1024]; // 64KB read buffer
_writeBuffer = new byte[64 * 1024]; // 64KB write buffer
ConnectionId = Guid.NewGuid().ToString("N");
// Configure socket options
_socket.NoDelay = true; // Disable Nagle's algorithm
_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
}
/// <summary>
/// Sends a frame over the connection.
/// </summary>
public async ValueTask SendAsync(Frame frame, CancellationToken cancellationToken)
{
await _writeLock.WaitAsync(cancellationToken);
try
{
var size = _codec.Encode(frame, _writeBuffer);
await _stream.WriteAsync(_writeBuffer.AsMemory(0, size), cancellationToken);
await _stream.FlushAsync(cancellationToken);
}
finally
{
_writeLock.Release();
}
}
/// <summary>
/// Receives a frame from the connection.
/// </summary>
public async ValueTask<Frame> ReceiveAsync(CancellationToken cancellationToken)
{
while (true)
{
// Try to decode from existing buffer
if (_readBufferCount >= 24) // Minimum header size
{
var span = new ReadOnlySpan<byte>(_readBuffer, _readBufferOffset, _readBufferCount);
// Check if we have a complete frame
if (span.Length >= 8)
{
var payloadLength = BinaryPrimitives.ReadUInt32BigEndian(span[4..]);
var totalLength = 24 + (int)payloadLength;
if (span.Length >= totalLength)
{
var frame = _codec.Decode(span[..totalLength]);
_readBufferOffset += totalLength;
_readBufferCount -= totalLength;
// Compact buffer if needed
if (_readBufferOffset > _readBuffer.Length / 2)
{
Buffer.BlockCopy(_readBuffer, _readBufferOffset, _readBuffer, 0, _readBufferCount);
_readBufferOffset = 0;
}
return frame;
}
}
}
// Need more data
if (_readBufferOffset + _readBufferCount >= _readBuffer.Length)
{
// Compact buffer
Buffer.BlockCopy(_readBuffer, _readBufferOffset, _readBuffer, 0, _readBufferCount);
_readBufferOffset = 0;
}
var bytesRead = await _stream.ReadAsync(
_readBuffer.AsMemory(_readBufferOffset + _readBufferCount),
cancellationToken);
if (bytesRead == 0)
{
throw new EndOfStreamException("Connection closed by remote");
}
_readBufferCount += bytesRead;
}
}
/// <summary>
/// Receives frames as an async enumerable.
/// </summary>
public async IAsyncEnumerable<Frame> ReceiveAllAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Frame frame;
try
{
frame = await ReceiveAsync(cancellationToken);
}
catch (EndOfStreamException)
{
yield break;
}
catch (OperationCanceledException)
{
yield break;
}
yield return frame;
}
}
public async ValueTask DisposeAsync()
{
_writeLock.Dispose();
await _stream.DisposeAsync();
_socket.Dispose();
}
}
Gateway-Side TCP Server
namespace StellaOps.Router.Transport.Tcp;
/// <summary>
/// TCP server running on the gateway to accept microservice connections.
/// </summary>
public sealed class TcpTransportServer : IHostedService
{
private readonly TcpTransportConfig _config;
private readonly TcpFrameCodec _codec;
private readonly IGlobalRoutingState _routingState;
private readonly IPayloadSerializer _serializer;
private readonly ILogger<TcpTransportServer> _logger;
private Socket? _listener;
private CancellationTokenSource? _cts;
private readonly ConcurrentDictionary<string, TcpMicroserviceConnection> _connections = new();
public TcpTransportServer(
IOptions<TcpTransportConfig> config,
TcpFrameCodec codec,
IGlobalRoutingState routingState,
IPayloadSerializer serializer,
ILogger<TcpTransportServer> logger)
{
_config = config.Value;
_codec = codec;
_routingState = routingState;
_serializer = serializer;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_cts = new CancellationTokenSource();
_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_listener.Bind(new IPEndPoint(IPAddress.Parse(_config.ListenAddress), _config.Port));
_listener.Listen(_config.Backlog);
_logger.LogInformation(
"TCP transport server listening on {Address}:{Port}",
_config.ListenAddress, _config.Port);
_ = AcceptConnectionsAsync(_cts.Token);
}
private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
var socket = await _listener!.AcceptAsync(cancellationToken);
_logger.LogDebug("Accepted connection from {RemoteEndPoint}", socket.RemoteEndPoint);
_ = HandleConnectionAsync(socket, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error accepting connection");
}
}
}
private async Task HandleConnectionAsync(Socket socket, CancellationToken cancellationToken)
{
var connection = new TcpFrameConnection(socket, _codec, _logger);
try
{
// Wait for HELLO frame
var helloFrame = await connection.ReceiveAsync(cancellationToken)
.AsTask()
.WaitAsync(TimeSpan.FromSeconds(_config.HandshakeTimeoutSeconds), cancellationToken);
if (helloFrame.Type != FrameType.Hello)
{
_logger.LogWarning("Expected HELLO frame, got {Type}", helloFrame.Type);
return;
}
var hello = _serializer.DeserializeHello(helloFrame.Payload);
_logger.LogInformation(
"Microservice connected: {ServiceName}/{InstanceId}",
hello.ServiceName, hello.InstanceId);
// Send HELLO response
var helloResponse = new HelloResponse
{
Accepted = true,
HeartbeatIntervalMs = _config.HeartbeatIntervalMs,
MaxPayloadSize = _config.MaxPayloadSize
};
var responseFrame = new Frame
{
Type = FrameType.Hello,
CorrelationId = helloFrame.CorrelationId,
Payload = _serializer.SerializeHelloResponse(helloResponse)
};
await connection.SendAsync(responseFrame, cancellationToken);
// Create connection wrapper
var msConnection = new TcpMicroserviceConnection(
connection,
hello.ServiceName,
hello.InstanceId,
hello.Endpoints,
_serializer,
_logger);
_connections[connection.ConnectionId] = msConnection;
// Register with routing state
_routingState.RegisterConnection(new EndpointConnection
{
ConnectionId = connection.ConnectionId,
ServiceName = hello.ServiceName,
InstanceId = hello.InstanceId,
Transport = "TCP",
State = ConnectionState.Connected,
Endpoints = hello.Endpoints,
Region = hello.Metadata?.GetValueOrDefault("region"),
LastHeartbeat = DateTimeOffset.UtcNow
});
// Process frames
await msConnection.ProcessAsync(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling connection {ConnectionId}", connection.ConnectionId);
}
finally
{
_connections.TryRemove(connection.ConnectionId, out _);
_routingState.RemoveConnection(connection.ConnectionId);
await connection.DisposeAsync();
}
}
/// <summary>
/// Gets a connection for sending requests to a service instance.
/// </summary>
public TcpMicroserviceConnection? GetConnection(string connectionId)
{
return _connections.TryGetValue(connectionId, out var conn) ? conn : null;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_cts?.Cancel();
_listener?.Close();
foreach (var connection in _connections.Values)
{
await connection.DisconnectAsync();
}
_cts?.Dispose();
}
}
/// <summary>
/// Represents an active microservice connection.
/// </summary>
public sealed class TcpMicroserviceConnection
{
private readonly TcpFrameConnection _connection;
private readonly IPayloadSerializer _serializer;
private readonly ILogger _logger;
private readonly ConcurrentDictionary<string, TaskCompletionSource<Frame>> _pendingRequests = new();
private DateTimeOffset _lastActivity;
public string ServiceName { get; }
public string InstanceId { get; }
public EndpointDescriptor[] Endpoints { get; }
public DateTimeOffset LastActivity => _lastActivity;
public TcpMicroserviceConnection(
TcpFrameConnection connection,
string serviceName,
string instanceId,
EndpointDescriptor[] endpoints,
IPayloadSerializer serializer,
ILogger logger)
{
_connection = connection;
ServiceName = serviceName;
InstanceId = instanceId;
Endpoints = endpoints;
_serializer = serializer;
_logger = logger;
_lastActivity = DateTimeOffset.UtcNow;
}
public async Task ProcessAsync(CancellationToken cancellationToken)
{
await foreach (var frame in _connection.ReceiveAllAsync(cancellationToken))
{
_lastActivity = DateTimeOffset.UtcNow;
switch (frame.Type)
{
case FrameType.Response:
if (_pendingRequests.TryRemove(frame.CorrelationId, out var tcs))
{
tcs.TrySetResult(frame);
}
break;
case FrameType.Heartbeat:
// Microservice sent heartbeat response
break;
}
}
}
public async Task<ResponsePayload> SendRequestAsync(
RequestPayload request,
TimeSpan timeout,
CancellationToken cancellationToken)
{
var correlationId = Guid.NewGuid().ToString("N");
var tcs = new TaskCompletionSource<Frame>(TaskCreationOptions.RunContinuationsAsynchronously);
_pendingRequests[correlationId] = tcs;
try
{
var frame = new Frame
{
Type = FrameType.Request,
CorrelationId = correlationId,
Payload = _serializer.SerializeRequest(request)
};
await _connection.SendAsync(frame, cancellationToken);
using var timeoutCts = new CancellationTokenSource(timeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
try
{
var responseFrame = await tcs.Task.WaitAsync(linkedCts.Token);
return _serializer.DeserializeResponse(responseFrame.Payload);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
// Send cancel
await SendCancelAsync(correlationId, CancellationToken.None);
throw new TimeoutException($"Request timed out after {timeout}");
}
}
finally
{
_pendingRequests.TryRemove(correlationId, out _);
}
}
private async Task SendCancelAsync(string correlationId, CancellationToken cancellationToken)
{
try
{
var cancelFrame = new Frame
{
Type = FrameType.Cancel,
CorrelationId = correlationId,
Payload = Array.Empty<byte>()
};
await _connection.SendAsync(cancelFrame, cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to send cancel frame");
}
}
public async Task DisconnectAsync()
{
foreach (var pending in _pendingRequests.Values)
{
pending.TrySetCanceled();
}
_pendingRequests.Clear();
await _connection.DisposeAsync();
}
}
Microservice-Side TCP Client
namespace StellaOps.Router.Transport.Tcp;
/// <summary>
/// TCP client for microservices to connect to the gateway.
/// </summary>
public sealed class TcpTransportClient : ITransportServer, IAsyncDisposable
{
private readonly TcpClientConfig _config;
private readonly TcpFrameCodec _codec;
private readonly IPayloadSerializer _serializer;
private readonly ILogger<TcpTransportClient> _logger;
private TcpFrameConnection? _connection;
private CancellationTokenSource? _cts;
private Task? _processingTask;
private int _reconnectAttempts;
public string TransportType => "TCP";
public bool IsConnected => _connection?.IsConnected ?? false;
public event Func<RequestPayload, CancellationToken, Task<ResponsePayload>>? OnRequest;
public event Func<string, CancellationToken, Task>? OnCancel;
public TcpTransportClient(
IOptions<TcpClientConfig> config,
TcpFrameCodec codec,
IPayloadSerializer serializer,
ILogger<TcpTransportClient> logger)
{
_config = config.Value;
_codec = codec;
_serializer = serializer;
_logger = logger;
}
public async Task ConnectAsync(
string serviceName,
string instanceId,
EndpointDescriptor[] endpoints,
CancellationToken cancellationToken)
{
_cts = new CancellationTokenSource();
await ConnectWithRetryAsync(serviceName, instanceId, endpoints, cancellationToken);
// Start processing loop
_processingTask = ProcessFramesAsync(serviceName, instanceId, endpoints, _cts.Token);
}
private async Task ConnectWithRetryAsync(
string serviceName,
string instanceId,
EndpointDescriptor[] endpoints,
CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(_config.GatewayHost, _config.GatewayPort, cancellationToken);
_connection = new TcpFrameConnection(socket, _codec, _logger);
// Send HELLO
var hello = new HelloPayload
{
ServiceName = serviceName,
InstanceId = instanceId,
Endpoints = endpoints,
Metadata = new Dictionary<string, string>
{
["region"] = _config.Region ?? "default",
["version"] = _config.ServiceVersion ?? "1.0.0"
}
};
var helloFrame = new Frame
{
Type = FrameType.Hello,
CorrelationId = Guid.NewGuid().ToString("N"),
Payload = _serializer.SerializeHello(hello)
};
await _connection.SendAsync(helloFrame, cancellationToken);
// Wait for response
var response = await _connection.ReceiveAsync(cancellationToken);
if (response.Type != FrameType.Hello)
{
throw new ProtocolException($"Expected HELLO response, got {response.Type}");
}
_reconnectAttempts = 0;
_logger.LogInformation(
"Connected to gateway at {Host}:{Port}",
_config.GatewayHost, _config.GatewayPort);
return;
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
{
_reconnectAttempts++;
var delay = Math.Min(
_config.InitialReconnectDelayMs * Math.Pow(2, _reconnectAttempts - 1),
_config.MaxReconnectDelayMs);
_logger.LogWarning(
ex,
"Connection attempt {Attempt} failed, retrying in {Delay}ms",
_reconnectAttempts, delay);
await Task.Delay((int)delay, cancellationToken);
}
}
}
private async Task ProcessFramesAsync(
string serviceName,
string instanceId,
EndpointDescriptor[] endpoints,
CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (_connection == null || !_connection.IsConnected)
{
await ConnectWithRetryAsync(serviceName, instanceId, endpoints, cancellationToken);
}
await foreach (var frame in _connection!.ReceiveAllAsync(cancellationToken))
{
switch (frame.Type)
{
case FrameType.Request:
_ = HandleRequestAsync(frame, cancellationToken);
break;
case FrameType.Cancel:
if (OnCancel != null)
{
await OnCancel(frame.CorrelationId, cancellationToken);
}
break;
case FrameType.Heartbeat:
await HandleHeartbeatAsync(frame);
break;
}
}
}
catch (EndOfStreamException)
{
_logger.LogWarning("Connection closed, attempting reconnect");
_connection = null;
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing frames");
_connection = null;
}
}
}
private async Task HandleRequestAsync(Frame frame, CancellationToken cancellationToken)
{
if (_connection == null || OnRequest == null) return;
try
{
var request = _serializer.DeserializeRequest(frame.Payload);
var response = await OnRequest(request, cancellationToken);
var responseFrame = new Frame
{
Type = FrameType.Response,
CorrelationId = frame.CorrelationId,
Payload = _serializer.SerializeResponse(response),
Flags = FrameFlags.Final
};
await _connection.SendAsync(responseFrame, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling request");
var errorResponse = new ResponsePayload
{
StatusCode = 500,
Headers = new Dictionary<string, string>(),
ErrorMessage = ex.Message,
IsFinalChunk = true
};
var errorFrame = new Frame
{
Type = FrameType.Response,
CorrelationId = frame.CorrelationId,
Payload = _serializer.SerializeResponse(errorResponse),
Flags = FrameFlags.Final | FrameFlags.Error
};
await _connection.SendAsync(errorFrame, cancellationToken);
}
}
private async Task HandleHeartbeatAsync(Frame frame)
{
if (_connection == null) return;
var pongFrame = new Frame
{
Type = FrameType.Heartbeat,
CorrelationId = frame.CorrelationId,
Payload = frame.Payload
};
await _connection.SendAsync(pongFrame, CancellationToken.None);
}
public async Task DisconnectAsync()
{
_cts?.Cancel();
if (_processingTask != null)
{
try
{
await _processingTask.WaitAsync(TimeSpan.FromSeconds(5));
}
catch { }
}
if (_connection != null)
{
await _connection.DisposeAsync();
}
_cts?.Dispose();
}
public async ValueTask DisposeAsync()
{
await DisconnectAsync();
}
}
Configuration
namespace StellaOps.Router.Transport.Tcp;
public class TcpTransportConfig
{
public string ListenAddress { get; set; } = "0.0.0.0";
public int Port { get; set; } = 9500;
public int Backlog { get; set; } = 100;
public int HandshakeTimeoutSeconds { get; set; } = 30;
public int HeartbeatIntervalMs { get; set; } = 10000;
public int MaxPayloadSize { get; set; } = 16 * 1024 * 1024;
}
public class TcpClientConfig
{
public string GatewayHost { get; set; } = "localhost";
public int GatewayPort { get; set; } = 9500;
public string? Region { get; set; }
public string? ServiceVersion { get; set; }
public int InitialReconnectDelayMs { get; set; } = 1000;
public int MaxReconnectDelayMs { get; set; } = 30000;
public int ConnectionTimeoutMs { get; set; } = 10000;
}
YAML Configuration
# Gateway config
TcpTransport:
ListenAddress: "0.0.0.0"
Port: 9500
Backlog: 100
HandshakeTimeoutSeconds: 30
HeartbeatIntervalMs: 10000
MaxPayloadSize: 16777216 # 16MB
# Microservice config
TcpClient:
GatewayHost: "gateway.internal"
GatewayPort: 9500
Region: "us-east-1"
ServiceVersion: "1.0.0"
InitialReconnectDelayMs: 1000
MaxReconnectDelayMs: 30000
Service Registration
namespace StellaOps.Router.Transport.Tcp;
public static class TcpTransportExtensions
{
public static IServiceCollection AddTcpTransport(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<TcpTransportConfig>(
configuration.GetSection("TcpTransport"));
services.AddSingleton<TcpFrameCodec>();
services.AddSingleton<TcpTransportServer>();
services.AddHostedService(sp => sp.GetRequiredService<TcpTransportServer>());
return services;
}
public static IServiceCollection AddTcpMicroserviceTransport(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<TcpClientConfig>(
configuration.GetSection("TcpClient"));
services.AddSingleton<TcpFrameCodec>();
services.AddSingleton<ITransportServer, TcpTransportClient>();
return services;
}
}
Unit Tests
public class TcpFrameCodecTests
{
[Fact]
public void Encode_Decode_RoundTrips()
{
var codec = new TcpFrameCodec();
var original = new Frame
{
Type = FrameType.Request,
CorrelationId = Guid.NewGuid().ToString("N"),
Payload = Encoding.UTF8.GetBytes("test payload"),
Flags = FrameFlags.Compressed
};
var buffer = new byte[1024];
var length = codec.Encode(original, buffer);
var decoded = codec.Decode(buffer.AsSpan(0, length));
Assert.Equal(original.Type, decoded.Type);
Assert.Equal(original.CorrelationId, decoded.CorrelationId);
Assert.Equal(original.Payload, decoded.Payload);
Assert.Equal(original.Flags, decoded.Flags);
}
[Fact]
public void Decode_ThrowsOnInvalidMagic()
{
var codec = new TcpFrameCodec();
var buffer = new byte[24];
buffer[0] = 0xFF;
buffer[1] = 0xFF;
Assert.Throws<ProtocolException>(() => codec.Decode(buffer));
}
}
Deliverables
StellaOps.Router.Transport.Tcp/TcpFrameCodec.csStellaOps.Router.Transport.Tcp/TcpFrameConnection.csStellaOps.Router.Transport.Tcp/TcpTransportServer.csStellaOps.Router.Transport.Tcp/TcpMicroserviceConnection.csStellaOps.Router.Transport.Tcp/TcpTransportClient.csStellaOps.Router.Transport.Tcp/TcpTransportConfig.csStellaOps.Router.Transport.Tcp/TcpTransportExtensions.cs- Wire format encoding/decoding tests
- Connection lifecycle tests
- Reconnection tests
Next Step
Proceed to Step 15: TLS Transport Implementation to add TLS encryption on top of TCP.