using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Capnp.Rpc { /// /// TCP-based RPC implementation which will establish a connection to a TCP server implementing /// the Cap'n Proto RPC protocol. /// public class TcpRpcClient: IDisposable { ILogger Logger { get; } = Logging.CreateLogger(); class OutboundTcpEndpoint : IEndpoint { readonly TcpRpcClient _client; readonly FramePump _pump; public OutboundTcpEndpoint(TcpRpcClient client, FramePump pump) { _client = client; _pump = pump; } public void Dismiss() { _pump.Dispose(); } public void Forward(WireFrame frame) { _pump.Send(frame); } } readonly RpcEngine _rpcEngine; readonly TcpClient _client; RpcEngine.RpcEndpoint _inboundEndpoint; OutboundTcpEndpoint _outboundEndpoint; FramePump _pump; Thread _pumpThread; /// /// Gets a Task which completes when TCP is connected. /// public Task WhenConnected { get; } async Task ConnectAsync(string host, int port) { try { await _client.ConnectAsync(host, port); } catch (SocketException exception) { throw new RpcException("TcpRpcClient is unable to connect", exception); } } async Task Connect(string host, int port) { await ConnectAsync(host, port); _pump = new FramePump(_client.GetStream()); _outboundEndpoint = new OutboundTcpEndpoint(this, _pump); _inboundEndpoint = _rpcEngine.AddEndpoint(_outboundEndpoint); _pumpThread = new Thread(() => { try { Thread.CurrentThread.Name = $"TCP RPC Client Thread {Thread.CurrentThread.ManagedThreadId}"; _pump.Run(); } finally { _outboundEndpoint.Dismiss(); _inboundEndpoint.Dismiss(); _pump.Dispose(); } }); _pump.FrameReceived += _inboundEndpoint.Forward; _pumpThread.Start(); } /// /// Constructs an instance and attempts to connect it to given host. /// /// The DNS name of the remote RPC host /// The port number of the remote RPC host /// is null. /// is not between System.Net.IPEndPoint.MinPort and System.Net.IPEndPoint.MaxPort. /// An error occurred when accessing the socket. public TcpRpcClient(string host, int port) { _rpcEngine = new RpcEngine(); _client = new TcpClient(); WhenConnected = Connect(host, port); } /// /// Returns the remote bootstrap capability. /// /// Bootstrap capability interface /// A proxy for the bootstrap capability public TProxy GetMain() where TProxy: class { if (!WhenConnected.IsCompleted) { throw new InvalidOperationException("Connection not yet established"); } if (!WhenConnected.IsCompletedSuccessfully) { throw new InvalidOperationException("Connection not successfully established"); } Debug.Assert(_inboundEndpoint != null); return CapabilityReflection.CreateProxy(_inboundEndpoint.QueryMain()) as TProxy; } /// /// Dispose pattern implementation /// public void Dispose() { _client.Dispose(); try { if (!WhenConnected.Wait(500)) { Logger.LogError("Unable to join connection task within timeout"); } } catch (System.Exception) { } if (_pumpThread != null && !_pumpThread.Join(500)) { Logger.LogError("Unable to join pump thread within timeout"); } GC.SuppressFinalize(this); } /// /// Gets the number of RPC protocol messages sent by this client so far. /// public long SendCount => _inboundEndpoint.SendCount; /// /// Gets the number of RPC protocol messages received by this client so far. /// public long RecvCount => _inboundEndpoint.RecvCount; /// /// Gets the remote port number which this client is connected to, /// or null if the connection is not yet established. /// public int? RemotePort => ((IPEndPoint)_client.Client?.RemoteEndPoint)?.Port; /// /// Whether the I/O thread is currently running /// public bool IsComputing => _pumpThread.ThreadState == System.Threading.ThreadState.Running; /// /// Whether the I/O thread is waiting for data to receive /// public bool IsWaitingForData => _pump.IsWaitingForData; } }