# 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 1. Implement efficient binary frame encoding over TCP 2. Support connection multiplexing for high throughput 3. Implement automatic reconnection with exponential backoff 4. Handle partial reads/writes correctly 5. 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 ```csharp namespace StellaOps.Router.Transport.Tcp; /// /// Encodes and decodes frames for TCP wire format. /// 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 _bufferPool; public TcpFrameCodec(ArrayPool? bufferPool = null) { _bufferPool = bufferPool ?? ArrayPool.Shared; } /// /// Encodes a frame to wire format. /// public int Encode(Frame frame, Span 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; } /// /// Decodes a frame from wire format. /// public Frame Decode(ReadOnlySpan 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 }; } /// /// Attempts to decode a frame from a buffer, returning bytes consumed. /// public bool TryDecode(ReadOnlySequence buffer, out Frame frame, out int bytesConsumed) { frame = default!; bytesConsumed = 0; if (buffer.Length < HeaderSize) return false; // Read header to get length Span 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 ```csharp namespace StellaOps.Router.Transport.Tcp; /// /// Represents a TCP connection with frame-based I/O. /// 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); } /// /// Sends a frame over the connection. /// 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(); } } /// /// Receives a frame from the connection. /// public async ValueTask ReceiveAsync(CancellationToken cancellationToken) { while (true) { // Try to decode from existing buffer if (_readBufferCount >= 24) // Minimum header size { var span = new ReadOnlySpan(_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; } } /// /// Receives frames as an async enumerable. /// public async IAsyncEnumerable 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 ```csharp namespace StellaOps.Router.Transport.Tcp; /// /// TCP server running on the gateway to accept microservice connections. /// public sealed class TcpTransportServer : IHostedService { private readonly TcpTransportConfig _config; private readonly TcpFrameCodec _codec; private readonly IGlobalRoutingState _routingState; private readonly IPayloadSerializer _serializer; private readonly ILogger _logger; private Socket? _listener; private CancellationTokenSource? _cts; private readonly ConcurrentDictionary _connections = new(); public TcpTransportServer( IOptions config, TcpFrameCodec codec, IGlobalRoutingState routingState, IPayloadSerializer serializer, ILogger 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(); } } /// /// Gets a connection for sending requests to a service instance. /// 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(); } } /// /// Represents an active microservice connection. /// public sealed class TcpMicroserviceConnection { private readonly TcpFrameConnection _connection; private readonly IPayloadSerializer _serializer; private readonly ILogger _logger; private readonly ConcurrentDictionary> _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 SendRequestAsync( RequestPayload request, TimeSpan timeout, CancellationToken cancellationToken) { var correlationId = Guid.NewGuid().ToString("N"); var tcs = new TaskCompletionSource(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() }; 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 ```csharp namespace StellaOps.Router.Transport.Tcp; /// /// TCP client for microservices to connect to the gateway. /// public sealed class TcpTransportClient : ITransportServer, IAsyncDisposable { private readonly TcpClientConfig _config; private readonly TcpFrameCodec _codec; private readonly IPayloadSerializer _serializer; private readonly ILogger _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>? OnRequest; public event Func? OnCancel; public TcpTransportClient( IOptions config, TcpFrameCodec codec, IPayloadSerializer serializer, ILogger 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 { ["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(), 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 ```csharp 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 ```yaml # 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 ```csharp namespace StellaOps.Router.Transport.Tcp; public static class TcpTransportExtensions { public static IServiceCollection AddTcpTransport( this IServiceCollection services, IConfiguration configuration) { services.Configure( configuration.GetSection("TcpTransport")); services.AddSingleton(); services.AddSingleton(); services.AddHostedService(sp => sp.GetRequiredService()); return services; } public static IServiceCollection AddTcpMicroserviceTransport( this IServiceCollection services, IConfiguration configuration) { services.Configure( configuration.GetSection("TcpClient")); services.AddSingleton(); services.AddSingleton(); return services; } } ``` --- ## Unit Tests ```csharp 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(() => codec.Decode(buffer)); } } ``` --- ## Deliverables 1. `StellaOps.Router.Transport.Tcp/TcpFrameCodec.cs` 2. `StellaOps.Router.Transport.Tcp/TcpFrameConnection.cs` 3. `StellaOps.Router.Transport.Tcp/TcpTransportServer.cs` 4. `StellaOps.Router.Transport.Tcp/TcpMicroserviceConnection.cs` 5. `StellaOps.Router.Transport.Tcp/TcpTransportClient.cs` 6. `StellaOps.Router.Transport.Tcp/TcpTransportConfig.cs` 7. `StellaOps.Router.Transport.Tcp/TcpTransportExtensions.cs` 8. Wire format encoding/decoding tests 9. Connection lifecycle tests 10. Reconnection tests --- ## Next Step Proceed to [Step 15: TLS Transport Implementation](15-Step.md) to add TLS encryption on top of TCP.