Aetheria/Assets/Scripts/ServerShared/NIH/RethinkDb.Driver/Net/SocketWrapper.cs
hyperion51 e96146b7bb Out with MagicOnion, in with LiteNetLib
Removed MagicOnion
Added LiteNetLib
Created client and server abstractions
Fixed serialization bugs
2020-03-24 14:36:22 +00:00

396 lines
No EOL
14 KiB
C#

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RethinkDb.Driver.Utils;
namespace RethinkDb.Driver.Net
{
internal class SocketWrapper
{
private Socket socket;
private readonly TimeSpan timeout;
private readonly string hostname;
private readonly int port;
private readonly Action<Exception> errorCallback;
private NetworkStream ns = null;
private BinaryWriter bw = null;
private BinaryReader br = null;
private SslContext sslContext;
private SslStream ssl = null;
public SocketWrapper(string hostname, int port, TimeSpan? timeout, SslContext sslContext, Action<Exception> errorCallback)
{
this.hostname = hostname;
this.port = port;
this.errorCallback = errorCallback;
this.sslContext = sslContext;
this.timeout = timeout ?? TimeSpan.FromSeconds(60);
}
private Exception currentException;
public virtual async Task ConnectAsync(Handshake handshake)
{
try
{
handshake.Reset();
var addresses = await Dns.GetHostAddressesAsync(this.hostname)
.ConfigureAwait(false);
this.socket = await ConnectInternalAsync(addresses, this.port)
.ConfigureAwait(false);
this.ns = new NetworkStream(this.socket);
if (this.sslContext != null)
{
this.ssl = new SslStream(this.ns,
leaveInnerStreamOpen: false,
userCertificateValidationCallback: this.sslContext.ServerCertificateValidationCallback,
userCertificateSelectionCallback: this.sslContext.LocalCertificateSelectionCallback);
await this.ssl.AuthenticateAsClientAsync(this.sslContext.TargetHostOverride ?? this.hostname,
this.sslContext.ClientCertificateCollection,
this.sslContext.EnabledProtocols,
this.sslContext.CheckCertificateRevocation)
.ConfigureAwait(false);
}
this.bw = new BinaryWriter(this.ssl as Stream ?? this.ns);
this.br = new BinaryReader(this.ssl as Stream ?? this.ns);
// execute RethinkDB handshake
ExecuteHandshake(handshake);
//http://blog.i3arnon.com/2015/07/02/task-run-long-running/
//LongRunning creates a new thread and marks it as a background thread
//(ie: does not block application shutdown, when all foreground threads finish)
pump = new CancellationTokenSource();
// GitHub Issue #24 - Set PUMP token first before starting thread.
// If the pump token is set in ResponsePump, and the thread is scheduled
// late, and we return immediately to the caller indicating the connection
// is ready, the caller will immediately SendQuery. However, SendQuery
// is dependent on pump, and will encounter a null reference exception
// because the ResponsePump thread is late setting the token.
//
// So, set the token so that we're *really* ready to begin sending
// queries.
#pragma warning disable 4014 // We know what' we're doing. It's intentional.
Task.Factory.StartNew(ResponsePump, TaskCreationOptions.LongRunning);
#pragma warning restore 4014
}
catch
{
try
{
this.Close();
}
catch
{
// attempt to close, ignored,
// and re-throw the original exception
}
throw;
}
}
private static async Task<Socket> ConnectInternalAsync(IPAddress[] addresses, int port)
{
Exception lastExc = null;
foreach( var address in addresses )
{
var s = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
{
NoDelay = true
};
s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
try
{
#if STANDARD
await s.ConnectAsync(address, port).ConfigureAwait(false);
#else
await Task.Factory.FromAsync(
(targetHost, targetPort, callback, state) =>
((Socket)state).BeginConnect(targetHost, targetPort, callback, state),
asyncResult =>
((Socket)asyncResult.AsyncState).EndConnect(asyncResult),
address, port, state: s)
.ConfigureAwait(false);
#endif
return s;
}
catch( Exception exc )
{
lastExc = exc;
s.Dispose();
}
}
if( lastExc != null ) throw lastExc;
throw new ArgumentException("No addresses provided", nameof(addresses));
}
private void ExecuteHandshake(Handshake handshake)
{
// initialize handshake
var toWrite = handshake.NextMessage(null);
// Sit in the handshake until it's completed. Exceptions will be thrown if
// anything goes wrong.
while (!handshake.IsFinished)
{
if (toWrite != null)
{
bw.Write(toWrite);
}
var serverMsg = ReadNullTerminatedString(this.timeout);
toWrite = handshake.NextMessage(serverMsg);
}
}
public virtual void Connect(Handshake handshake)
{
if( !ConnectAsync(handshake).Wait(this.timeout) )
{
try
{
this.Close();
}
catch
{
}
throw new ReqlDriverError("Connection timed out.");
}
}
private string ReadNullTerminatedString(TimeSpan? deadline)
{
var deadlineInstant = deadline.HasValue ? DateTime.Now.Add(deadline.Value) : (DateTime?)null;
var sb = new StringBuilder();
char c;
while( (c = this.br.ReadChar()) != '\0' )
{
if( deadlineInstant.HasValue )
{
if( deadlineInstant <= DateTime.Now )
{
throw new ReqlDriverError("Connection timed out.");
}
}
sb.Append(c);
}
return sb.ToString();
}
private CancellationTokenSource pump = null;
/// <summary>
/// Started just after connect. Do not use or async/await code in this pump
/// because it is a long-running task.
/// http://blog.i3arnon.com/2015/07/02/task-run-long-running/
/// </summary>
private void ResponsePump()
{
while( true )
{
if( pump.IsCancellationRequested )
{
Log.Trace("Response Pump: shutting down. Cancel is requested.");
break;
}
if( this.Closed )
{
Log.Trace("Response Pump: The connected socket is not open. Response Loop exiting.");
break;
}
try
{
//WARN: Do not use or async/await code in this pump; because it is a
//long-running task.
//http://blog.i3arnon.com/2015/07/02/task-run-long-running/
var response = this.Read();
Awaiter awaitingTask;
if( awaiters.TryRemove(response.Token, out awaitingTask) )
{
Task.Run(() =>
{
//regardless of the outcome, clean up any registered
//cancellation tokens with using statement.
using ( awaitingTask )
{
//try setting the result, because it's possible
//the awaiting task was canceled.
if( !awaitingTask.TrySetResult(response) )
{
Log.Debug(
$"Response Pump: The awaiter waiting for response token {response.Token} could not be set. The task was probably canceled.");
}
}
});
}
else
{
//Wow, there's nobody waiting for this response.
Log.Debug(
$"Response Pump: There are no awaiters waiting for {response.Token} token. A cursor was probably closed and this might be a response to a QUERY:STOP. The response will be ignored.");
//I guess we'll ignore for now, perhaps a cursor was killed
}
}
catch( Exception e )
{
currentException = e;
this.errorCallback?.Invoke(currentException);
Log.Trace($"Response Pump: {e.GetType().Name} - {e.Message} The connection can no longer be used. {nameof(ResponsePump)} is preparing to shutdown.");
//shutdown all.
try
{
this.Close();
}
catch
{
}
break;
}
}
Log.Trace($"Cleaning up Response Pump awaiters for {hostname}:{port}");
//clean up.
foreach( var a in awaiters.Values )
{
if( currentException != null )
{
a.TrySetException(currentException);
}
else
{
a.TrySetCanceled();
}
}
awaiters.Clear();
}
/// <summary>
/// Blocking Read by the ResponsePump
/// </summary>
private Response Read()
{
var token = this.br.ReadInt64();
var responseLength = this.br.ReadInt32();
var response = this.br.ReadBytes(responseLength);
return Response.ParseFrom(token, Encoding.UTF8.GetString(response));
}
private ConcurrentDictionary<long, Awaiter> awaiters = new ConcurrentDictionary<long, Awaiter>();
private object writeLock = new object();
public virtual Task<Response> SendQuery(long token, string json, bool awaitResponse, CancellationToken cancelToken)
{
cancelToken.ThrowIfCancellationRequested();
if( this.Closed )
{
throw new ReqlDriverError($"Threads may not {nameof(SendQuery)} because the connection is closed.");
}
if( pump.IsCancellationRequested )
{
throw new ReqlDriverError($"Threads may not {nameof(SendQuery)} because the connection is shutting down.");
}
Awaiter awaiter = null;
if( awaitResponse )
{
//Assign a new awaiter for this token,
//The caller is expecting a response.
awaiter = new Awaiter(cancelToken);
awaiters[token] = awaiter;
}
lock( writeLock )
{
// Everyone can write their query as fast as they can; block if needed.
cancelToken.ThrowIfCancellationRequested();
//We could probably use a semaphore slim as a lock
//and Wait(cancelToken), but using semaphore slim
//is slower and requires more overhead. So, instead
//we just cancel at earliest possible time when the lock
//is free.
try
{
//using bw is fast and convenient
this.bw.Write(token);
var jsonBytes = Encoding.UTF8.GetBytes(json);
this.bw.Write(jsonBytes.Length);
this.bw.Write(jsonBytes);
//we check here because it's possibly very expensive to ship this json around in the call stack
if (Log.IsTraceEnabled) Log.Trace($"JSON Send: Token: {token}, JSON: {json}");
}
catch( Exception e )
{
currentException = e;
this.errorCallback?.Invoke(currentException);
Log.Trace($"Write Query failed for Token {token}. Exception: {e.Message}");
throw;
}
}
return awaiter?.Task ?? TaskHelper.CompletedResponse;
}
public virtual bool Closed => !this.Open;
public virtual bool Open => socket?.Connected ?? false;
public virtual IPEndPoint ClientEndPoint => this.socket.LocalEndPoint as IPEndPoint;
public virtual IPEndPoint RemoteEndPoint => this.socket.RemoteEndPoint as IPEndPoint;
public virtual bool HasError => currentException != null;
public virtual void Close()
{
this.pump?.Cancel();
try
{
this.ssl?.Dispose();
this.ns.Dispose();
}
catch
{
}
try
{
socket.Shutdown(SocketShutdown.Both);
socket.Dispose();
}
catch
{
}
currentException = currentException ?? new EndOfStreamException("The driver connection is closed.");
}
}
}