Out with MagicOnion, in with LiteNetLib
Removed MagicOnion Added LiteNetLib Created client and server abstractions Fixed serialization bugs
This commit is contained in:
parent
433a7b4ae4
commit
e96146b7bb
869 changed files with 59401 additions and 16812 deletions
|
|
@ -0,0 +1,396 @@
|
|||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue