Converting pipelines to channels
This commit is contained in:
parent
1119bcf1b3
commit
c997ea8165
|
|
@ -28,11 +28,11 @@ namespace ClientSample
|
||||||
var logger = loggerFactory.CreateLogger<Program>();
|
var logger = loggerFactory.CreateLogger<Program>();
|
||||||
|
|
||||||
using (var httpClient = new HttpClient(new LoggingMessageHandler(loggerFactory, new HttpClientHandler())))
|
using (var httpClient = new HttpClient(new LoggingMessageHandler(loggerFactory, new HttpClientHandler())))
|
||||||
using (var pipelineFactory = new PipelineFactory())
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Connecting to {0}", baseUrl);
|
logger.LogInformation("Connecting to {0}", baseUrl);
|
||||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||||
using (var connection = await HubConnection.ConnectAsync(new Uri(baseUrl), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
using (var connection = await HubConnection.ConnectAsync(new Uri(baseUrl),
|
||||||
|
new JsonNetInvocationAdapter(), transport, httpClient, loggerFactory))
|
||||||
{
|
{
|
||||||
logger.LogInformation("Connected to {0}", baseUrl);
|
logger.LogInformation("Connected to {0}", baseUrl);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Sockets;
|
||||||
using Microsoft.AspNetCore.Sockets.Client;
|
using Microsoft.AspNetCore.Sockets.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -27,11 +28,10 @@ namespace ClientSample
|
||||||
var logger = loggerFactory.CreateLogger<Program>();
|
var logger = loggerFactory.CreateLogger<Program>();
|
||||||
|
|
||||||
using (var httpClient = new HttpClient(new LoggingMessageHandler(loggerFactory, new HttpClientHandler())))
|
using (var httpClient = new HttpClient(new LoggingMessageHandler(loggerFactory, new HttpClientHandler())))
|
||||||
using (var pipelineFactory = new PipelineFactory())
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Connecting to {0}", baseUrl);
|
logger.LogInformation("Connecting to {0}", baseUrl);
|
||||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||||
using (var connection = await Connection.ConnectAsync(new Uri(baseUrl), transport, httpClient, pipelineFactory, loggerFactory))
|
using (var connection = await Connection.ConnectAsync(new Uri(baseUrl), transport, httpClient, loggerFactory))
|
||||||
{
|
{
|
||||||
logger.LogInformation("Connected to {0}", baseUrl);
|
logger.LogInformation("Connected to {0}", baseUrl);
|
||||||
|
|
||||||
|
|
@ -44,8 +44,10 @@ namespace ClientSample
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ready to start the loops
|
// Ready to start the loops
|
||||||
var receive = StartReceiving(loggerFactory.CreateLogger("ReceiveLoop"), connection, cts.Token);
|
var receive =
|
||||||
var send = StartSending(loggerFactory.CreateLogger("SendLoop"), connection, cts.Token);
|
StartReceiving(loggerFactory.CreateLogger("ReceiveLoop"), connection, cts.Token).ContinueWith(_ => cts.Cancel());
|
||||||
|
var send =
|
||||||
|
StartSending(loggerFactory.CreateLogger("SendLoop"), connection, cts.Token).ContinueWith(_ => cts.Cancel());
|
||||||
|
|
||||||
await Task.WhenAll(receive, send);
|
await Task.WhenAll(receive, send);
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +62,9 @@ namespace ClientSample
|
||||||
var line = Console.ReadLine();
|
var line = Console.ReadLine();
|
||||||
logger.LogInformation("Sending: {0}", line);
|
logger.LogInformation("Sending: {0}", line);
|
||||||
|
|
||||||
await connection.Output.WriteAsync(Encoding.UTF8.GetBytes(line));
|
await connection.Output.WriteAsync(new Message(
|
||||||
|
ReadableBuffer.Create(Encoding.UTF8.GetBytes("Hello World")).Preserve(),
|
||||||
|
Format.Text));
|
||||||
}
|
}
|
||||||
logger.LogInformation("Send loop terminated");
|
logger.LogInformation("Send loop terminated");
|
||||||
}
|
}
|
||||||
|
|
@ -68,30 +72,31 @@ namespace ClientSample
|
||||||
private static async Task StartReceiving(ILogger logger, Connection connection, CancellationToken cancellationToken)
|
private static async Task StartReceiving(ILogger logger, Connection connection, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Receive loop starting");
|
logger.LogInformation("Receive loop starting");
|
||||||
using (cancellationToken.Register(() => connection.Input.Complete()))
|
try
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (await connection.Input.WaitToReadAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
var result = await connection.Input.ReadAsync();
|
Message message;
|
||||||
var buffer = result.Buffer;
|
if (!connection.Input.TryRead(out message))
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (!buffer.IsEmpty)
|
continue;
|
||||||
{
|
|
||||||
var message = Encoding.UTF8.GetString(buffer.ToArray());
|
|
||||||
logger.LogInformation("Received: {0}", message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
using (message)
|
||||||
{
|
{
|
||||||
connection.Input.Advance(buffer.End);
|
logger.LogInformation("Received: {0}", Encoding.UTF8.GetString(message.Payload.Buffer.ToArray()));
|
||||||
}
|
|
||||||
if (result.IsCompleted)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Connection is closing");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(0, ex, "Connection terminated due to an exception");
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("Receive loop terminated");
|
logger.LogInformation("Receive loop terminated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks.Channels;
|
|
||||||
using Microsoft.AspNetCore.Sockets;
|
using Microsoft.AspNetCore.Sockets;
|
||||||
|
|
||||||
namespace SocketsSample.EndPoints
|
namespace SocketsSample.EndPoints
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Sockets;
|
||||||
using Microsoft.AspNetCore.Sockets.Client;
|
using Microsoft.AspNetCore.Sockets.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -19,7 +20,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
public class HubConnection : IDisposable
|
public class HubConnection : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Task _reader;
|
private readonly Task _reader;
|
||||||
private readonly Stream _stream;
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Connection _connection;
|
private readonly Connection _connection;
|
||||||
private readonly IInvocationAdapter _adapter;
|
private readonly IInvocationAdapter _adapter;
|
||||||
|
|
@ -42,13 +42,11 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
{
|
{
|
||||||
_binder = new HubBinder(this);
|
_binder = new HubBinder(this);
|
||||||
_connection = connection;
|
_connection = connection;
|
||||||
_stream = connection.GetStream();
|
|
||||||
_adapter = adapter;
|
_adapter = adapter;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_reader = ReceiveMessages(_readerCts.Token);
|
_reader = ReceiveMessages(_readerCts.Token);
|
||||||
Completion = _connection.Output.Writing.ContinueWith(
|
Completion = _connection.Input.Completion.ContinueWith(t => Shutdown(t)).Unwrap();
|
||||||
t => Shutdown(t)).Unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Client return values/tasks?
|
// TODO: Client return values/tasks?
|
||||||
|
|
@ -98,9 +96,21 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
_logger.LogTrace("Invocation #{0}: {1} {2}({3})", descriptor.Id, returnType.FullName, methodName, argsList);
|
_logger.LogTrace("Invocation #{0}: {1} {2}({3})", descriptor.Id, returnType.FullName, methodName, argsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the invocation to the stream
|
var ms = new MemoryStream();
|
||||||
|
await _adapter.WriteMessageAsync(descriptor, ms, cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("Sending Invocation #{0}", descriptor.Id);
|
_logger.LogInformation("Sending Invocation #{0}", descriptor.Id);
|
||||||
await _adapter.WriteMessageAsync(descriptor, _stream, cancellationToken);
|
|
||||||
|
// TODO: Format.Text - who, where and when decides about the format of outgoing messages
|
||||||
|
var message = new Message(ReadableBuffer.Create(ms.ToArray()).Preserve(), Format.Text);
|
||||||
|
while (await _connection.Output.WaitToWriteAsync())
|
||||||
|
{
|
||||||
|
if (_connection.Output.TryWrite(message))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Sending Invocation #{0} complete", descriptor.Id);
|
_logger.LogInformation("Sending Invocation #{0} complete", descriptor.Id);
|
||||||
|
|
||||||
// Return the completion task. It will be completed by ReceiveMessages when the response is received.
|
// Return the completion task. It will be completed by ReceiveMessages when the response is received.
|
||||||
|
|
@ -114,12 +124,12 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Clean up the API here. Negotiation of format would be better than providing an adapter instance. Similarly, we should not require a logger factory
|
// TODO: Clean up the API here. Negotiation of format would be better than providing an adapter instance. Similarly, we should not require a logger factory
|
||||||
public static Task<HubConnection> ConnectAsync(Uri url, IInvocationAdapter adapter, ITransport transport, PipelineFactory pipelineFactory, ILoggerFactory loggerFactory) => ConnectAsync(url, adapter, transport, new HttpClient(), pipelineFactory, loggerFactory);
|
public static Task<HubConnection> ConnectAsync(Uri url, IInvocationAdapter adapter, ITransport transport, ILoggerFactory loggerFactory) => ConnectAsync(url, adapter, transport, new HttpClient(), loggerFactory);
|
||||||
|
|
||||||
public static async Task<HubConnection> ConnectAsync(Uri url, IInvocationAdapter adapter, ITransport transport, HttpClient httpClient, PipelineFactory pipelineFactory, ILoggerFactory loggerFactory)
|
public static async Task<HubConnection> ConnectAsync(Uri url, IInvocationAdapter adapter, ITransport transport, HttpClient httpClient, ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
// Connect the underlying connection
|
// Connect the underlying connection
|
||||||
var connection = await Connection.ConnectAsync(url, transport, httpClient, pipelineFactory, loggerFactory);
|
var connection = await Connection.ConnectAsync(url, transport, httpClient, loggerFactory);
|
||||||
|
|
||||||
// Create the RPC connection wrapper
|
// Create the RPC connection wrapper
|
||||||
return new HubConnection(connection, adapter, loggerFactory.CreateLogger<HubConnection>());
|
return new HubConnection(connection, adapter, loggerFactory.CreateLogger<HubConnection>());
|
||||||
|
|
@ -132,30 +142,38 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
_logger.LogTrace("Beginning receive loop");
|
_logger.LogTrace("Beginning receive loop");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (await _connection.Input.WaitToReadAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
// This is a little odd... we want to remove the InvocationRequest once and only once so we pull it out in the callback,
|
Message incomingMessage;
|
||||||
// and stash it here because we know the callback will have finished before the end of the await.
|
while (_connection.Input.TryRead(out incomingMessage))
|
||||||
var message = await _adapter.ReadMessageAsync(_stream, _binder, cancellationToken);
|
{
|
||||||
|
|
||||||
var invocationDescriptor = message as InvocationDescriptor;
|
InvocationMessage message;
|
||||||
if (invocationDescriptor != null)
|
using (incomingMessage)
|
||||||
{
|
|
||||||
DispatchInvocation(invocationDescriptor, cancellationToken);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var invocationResultDescriptor = message as InvocationResultDescriptor;
|
|
||||||
if (invocationResultDescriptor != null)
|
|
||||||
{
|
{
|
||||||
InvocationRequest irq;
|
message = await _adapter.ReadMessageAsync(
|
||||||
lock (_pendingCallsLock)
|
new MemoryStream(incomingMessage.Payload.Buffer.ToArray()), _binder, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var invocationDescriptor = message as InvocationDescriptor;
|
||||||
|
if (invocationDescriptor != null)
|
||||||
|
{
|
||||||
|
DispatchInvocation(invocationDescriptor, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var invocationResultDescriptor = message as InvocationResultDescriptor;
|
||||||
|
if (invocationResultDescriptor != null)
|
||||||
{
|
{
|
||||||
_connectionActive.Token.ThrowIfCancellationRequested();
|
InvocationRequest irq;
|
||||||
irq = _pendingCalls[invocationResultDescriptor.Id];
|
lock (_pendingCallsLock)
|
||||||
_pendingCalls.Remove(invocationResultDescriptor.Id);
|
{
|
||||||
|
_connectionActive.Token.ThrowIfCancellationRequested();
|
||||||
|
irq = _pendingCalls[invocationResultDescriptor.Id];
|
||||||
|
_pendingCalls.Remove(invocationResultDescriptor.Id);
|
||||||
|
}
|
||||||
|
DispatchInvocationResult(invocationResultDescriptor, irq, cancellationToken);
|
||||||
}
|
}
|
||||||
DispatchInvocationResult(invocationResultDescriptor, irq, cancellationToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,56 +2,45 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO.Pipelines;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading.Tasks.Channels;
|
||||||
|
using Microsoft.AspNetCore.Sockets.Internal;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Sockets.Client
|
namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
public class Connection : IPipelineConnection
|
public class Connection : IChannelConnection<Message>
|
||||||
{
|
{
|
||||||
private IPipelineConnection _consumerPipe;
|
private IChannelConnection<Message> _transportChannel;
|
||||||
private ITransport _transport;
|
private ITransport _transport;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public Uri Url { get; }
|
public Uri Url { get; }
|
||||||
|
|
||||||
// TODO: Review. This is really only designed to be used from ConnectAsync
|
// TODO: Review. This is really only designed to be used from ConnectAsync
|
||||||
private Connection(Uri url, ITransport transport, IPipelineConnection consumerPipe, ILogger logger)
|
private Connection(Uri url, ITransport transport, IChannelConnection<Message> transportChannel, ILogger logger)
|
||||||
{
|
{
|
||||||
Url = url;
|
Url = url;
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_transport = transport;
|
_transport = transport;
|
||||||
_consumerPipe = consumerPipe;
|
_transportChannel = transportChannel;
|
||||||
|
|
||||||
_consumerPipe.Output.Writing.ContinueWith(t =>
|
|
||||||
{
|
|
||||||
if (t.IsFaulted)
|
|
||||||
{
|
|
||||||
_consumerPipe.Input.Complete(t.Exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPipelineReader Input => _consumerPipe.Input;
|
public ReadableChannel<Message> Input => _transportChannel.Input;
|
||||||
public IPipelineWriter Output => _consumerPipe.Output;
|
public WritableChannel<Message> Output => _transportChannel.Output;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_consumerPipe.Dispose();
|
|
||||||
_transport.Dispose();
|
_transport.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: More overloads. PipelineFactory should be optional but someone needs to dispose the pool, if we're OK with it being the GC, then this is easy.
|
public static Task<Connection> ConnectAsync(Uri url, ITransport transport) => ConnectAsync(url, transport, new HttpClient(), NullLoggerFactory.Instance);
|
||||||
public static Task<Connection> ConnectAsync(Uri url, ITransport transport, PipelineFactory pipelineFactory) => ConnectAsync(url, transport, new HttpClient(), pipelineFactory, NullLoggerFactory.Instance);
|
public static Task<Connection> ConnectAsync(Uri url, ITransport transport, ILoggerFactory loggerFactory) => ConnectAsync(url, transport, new HttpClient(), loggerFactory);
|
||||||
public static Task<Connection> ConnectAsync(Uri url, ITransport transport, PipelineFactory pipelineFactory, ILoggerFactory loggerFactory) => ConnectAsync(url, transport, new HttpClient(), pipelineFactory, loggerFactory);
|
public static Task<Connection> ConnectAsync(Uri url, ITransport transport, HttpClient httpClient) => ConnectAsync(url, transport, httpClient, NullLoggerFactory.Instance);
|
||||||
public static Task<Connection> ConnectAsync(Uri url, ITransport transport, HttpClient httpClient, PipelineFactory pipelineFactory) => ConnectAsync(url, transport, httpClient, pipelineFactory, NullLoggerFactory.Instance);
|
|
||||||
|
|
||||||
public static async Task<Connection> ConnectAsync(Uri url, ITransport transport, HttpClient httpClient, PipelineFactory pipelineFactory, ILoggerFactory loggerFactory)
|
public static async Task<Connection> ConnectAsync(Uri url, ITransport transport, HttpClient httpClient, ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
if (url == null)
|
if (url == null)
|
||||||
{
|
{
|
||||||
|
|
@ -68,11 +57,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
throw new ArgumentNullException(nameof(httpClient));
|
throw new ArgumentNullException(nameof(httpClient));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pipelineFactory == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(pipelineFactory));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loggerFactory == null)
|
if (loggerFactory == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(loggerFactory));
|
throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
|
|
@ -97,12 +81,16 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
var connectedUrl = Utils.AppendQueryString(url, "id=" + connectionId);
|
var connectedUrl = Utils.AppendQueryString(url, "id=" + connectionId);
|
||||||
|
|
||||||
var pair = pipelineFactory.CreatePipelinePair();
|
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||||
|
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||||
|
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
||||||
|
var transportSide = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
||||||
|
|
||||||
|
|
||||||
// Start the transport, giving it one end of the pipeline
|
// Start the transport, giving it one end of the pipeline
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await transport.StartAsync(connectedUrl, pair.Item1);
|
await transport.StartAsync(connectedUrl, applicationSide);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -111,7 +99,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the connection, giving it the other end of the pipeline
|
// Create the connection, giving it the other end of the pipeline
|
||||||
return new Connection(url, transport, pair.Item2, logger);
|
return new Connection(url, transport, transportSide, logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO.Pipelines;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Sockets.Client
|
namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
public interface ITransport : IDisposable
|
public interface ITransport : IDisposable
|
||||||
{
|
{
|
||||||
Task StartAsync(Uri url, IPipelineConnection pipeline);
|
Task StartAsync(Uri url, IChannelConnection<Message> application);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
@ -20,12 +21,10 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly CancellationTokenSource _senderCts = new CancellationTokenSource();
|
private IChannelConnection<Message> _application;
|
||||||
private readonly CancellationTokenSource _pollCts = new CancellationTokenSource();
|
|
||||||
|
|
||||||
private IPipelineConnection _pipeline;
|
|
||||||
private Task _sender;
|
private Task _sender;
|
||||||
private Task _poller;
|
private Task _poller;
|
||||||
|
private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
|
||||||
|
|
||||||
public Task Running { get; private set; }
|
public Task Running { get; private set; }
|
||||||
|
|
||||||
|
|
@ -37,26 +36,21 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_senderCts.Cancel();
|
_transportCts.Cancel();
|
||||||
_pollCts.Cancel();
|
|
||||||
_pipeline?.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(Uri url, IPipelineConnection pipeline)
|
public Task StartAsync(Uri url, IChannelConnection<Message> application)
|
||||||
{
|
{
|
||||||
_pipeline = pipeline;
|
_application = application;
|
||||||
|
|
||||||
// Schedule shutdown of the poller when the output is closed
|
|
||||||
pipeline.Output.Writing.ContinueWith(_ =>
|
|
||||||
{
|
|
||||||
_pollCts.Cancel();
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start sending and polling
|
// Start sending and polling
|
||||||
_poller = Poll(Utils.AppendPath(url, "poll"), _pollCts.Token);
|
_poller = Poll(Utils.AppendPath(url, "poll"), _transportCts.Token);
|
||||||
_sender = SendMessages(Utils.AppendPath(url, "send"), _senderCts.Token);
|
_sender = SendMessages(Utils.AppendPath(url, "send"), _transportCts.Token);
|
||||||
Running = Task.WhenAll(_sender, _poller);
|
|
||||||
|
Running = Task.WhenAll(_sender, _poller).ContinueWith(t => {
|
||||||
|
_application.Output.TryComplete(t.IsFaulted ? t.Exception.InnerException : null);
|
||||||
|
return t;
|
||||||
|
}).Unwrap();
|
||||||
|
|
||||||
return TaskCache.CompletedTask;
|
return TaskCache.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
@ -80,64 +74,71 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Write the data to the output
|
var ms = new MemoryStream();
|
||||||
var buffer = _pipeline.Output.Alloc();
|
await response.Content.CopyToAsync(ms);
|
||||||
var stream = new WriteableBufferStream(buffer);
|
var message = new Message(ReadableBuffer.Create(ms.ToArray()).Preserve(), Format.Text);
|
||||||
await response.Content.CopyToAsync(stream);
|
|
||||||
await buffer.FlushAsync();
|
while (await _application.Output.WaitToWriteAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
if (_application.Output.TryWrite(message))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Polling complete
|
catch (OperationCanceledException)
|
||||||
_pipeline.Output.Complete();
|
{
|
||||||
|
// transport is being closed
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Shut down the output pipeline and log
|
|
||||||
_logger.LogError("Error while polling '{0}': {1}", pollUrl, ex);
|
_logger.LogError("Error while polling '{0}': {1}", pollUrl, ex);
|
||||||
_pipeline.Output.Complete(ex);
|
throw;
|
||||||
_pipeline.Input.Complete(ex);
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Make sure the send loop is terminated
|
||||||
|
_transportCts.Cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendMessages(Uri sendUrl, CancellationToken cancellationToken)
|
private async Task SendMessages(Uri sendUrl, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (cancellationToken.Register(() => _pipeline.Input.Complete()))
|
try
|
||||||
{
|
{
|
||||||
try
|
while (await _application.Input.WaitToReadAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
Message message;
|
||||||
|
while (!cancellationToken.IsCancellationRequested && _application.Input.TryRead(out message))
|
||||||
{
|
{
|
||||||
var result = await _pipeline.Input.ReadAsync();
|
using (message)
|
||||||
var buffer = result.Buffer;
|
|
||||||
if (buffer.IsEmpty || result.IsCompleted)
|
|
||||||
{
|
{
|
||||||
// No more data to send
|
var request = new HttpRequestMessage(HttpMethod.Post, sendUrl);
|
||||||
break;
|
request.Headers.UserAgent.Add(DefaultUserAgentHeader);
|
||||||
|
request.Content = new ReadableBufferContent(message.Payload.Buffer);
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a message to send
|
|
||||||
var message = new HttpRequestMessage(HttpMethod.Post, sendUrl);
|
|
||||||
message.Headers.UserAgent.Add(DefaultUserAgentHeader);
|
|
||||||
message.Content = new ReadableBufferContent(buffer);
|
|
||||||
|
|
||||||
// Send it
|
|
||||||
var response = await _httpClient.SendAsync(message);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
_pipeline.Input.Advance(buffer.End);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sending complete
|
|
||||||
_pipeline.Input.Complete();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Shut down the input pipeline and log
|
|
||||||
_logger.LogError("Error while sending to '{0}': {1}", sendUrl, ex);
|
|
||||||
_pipeline.Input.Complete(ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// transport is being closed
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError("Error while sending to '{0}': {1}", sendUrl, ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Make sure the poll loop is terminated
|
||||||
|
_transportCts.Cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
{
|
||||||
|
public struct Message : IDisposable
|
||||||
|
{
|
||||||
|
public Format MessageFormat { get; }
|
||||||
|
public PreservedBuffer Payload { get; }
|
||||||
|
|
||||||
|
public Message(PreservedBuffer payload, Format messageFormat)
|
||||||
|
{
|
||||||
|
MessageFormat = messageFormat;
|
||||||
|
Payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Payload.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,18 +4,16 @@
|
||||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
<PropertyGroup Label="Globals">
|
<PropertyGroup Label="Globals">
|
||||||
<ProjectGuid>623fd372-36de-41a9-a564-f6040d570dbd</ProjectGuid>
|
<ProjectGuid>623fd372-36de-41a9-a564-f6040d570dbd</ProjectGuid>
|
||||||
<RootNamespace>Microsoft.AspNetCore.SignalR.Client</RootNamespace>
|
<RootNamespace>Microsoft.AspNetCore.Sockets.Client</RootNamespace>
|
||||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System.IO.Pipelines;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Sockets.Client
|
|
||||||
{
|
|
||||||
internal class PipelineConnection : IPipelineConnection
|
|
||||||
{
|
|
||||||
public IPipelineReader Input { get; }
|
|
||||||
public IPipelineWriter Output { get; }
|
|
||||||
|
|
||||||
public PipelineConnection(IPipelineReader input, IPipelineWriter output)
|
|
||||||
{
|
|
||||||
Input = input;
|
|
||||||
Output = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Input.Complete();
|
|
||||||
Output.Complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.IO.Pipelines;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Sockets.Client
|
|
||||||
{
|
|
||||||
// TODO: Move to System.IO.Pipelines
|
|
||||||
public static class PipelineFactoryExtensions
|
|
||||||
{
|
|
||||||
// TODO: Use a named tuple? Though there aren't really good names for these ... client/server? left/right?
|
|
||||||
public static Tuple<IPipelineConnection, IPipelineConnection> CreatePipelinePair(this PipelineFactory self)
|
|
||||||
{
|
|
||||||
// Create a pair of pipelines for "Server" and "Client"
|
|
||||||
var clientToServer = self.Create();
|
|
||||||
var serverToClient = self.Create();
|
|
||||||
|
|
||||||
// "Server" reads from clientToServer and writes to serverToClient
|
|
||||||
var server = new PipelineConnection(
|
|
||||||
input: clientToServer,
|
|
||||||
output: serverToClient);
|
|
||||||
|
|
||||||
// "Client" reads from serverToClient and writes to clientToServer
|
|
||||||
var client = new PipelineConnection(
|
|
||||||
input: serverToClient,
|
|
||||||
output: clientToServer);
|
|
||||||
|
|
||||||
return Tuple.Create((IPipelineConnection)server, (IPipelineConnection)client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,7 +22,8 @@
|
||||||
"compile": {
|
"compile": {
|
||||||
"include": [
|
"include": [
|
||||||
"../Microsoft.AspNetCore.Sockets/IChannelConnection.cs",
|
"../Microsoft.AspNetCore.Sockets/IChannelConnection.cs",
|
||||||
"../Microsoft.AspNetCore.Sockets/Internal/ChannelConnection.cs"
|
"../Microsoft.AspNetCore.Sockets/Internal/ChannelConnection.cs",
|
||||||
|
"../Microsoft.AspNetCore.Sockets/Format.cs"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO.Pipelines;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Sockets;
|
using Microsoft.AspNetCore.Sockets;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
@ -18,7 +14,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
services.TryAddSingleton<ConnectionManager>();
|
services.TryAddSingleton<ConnectionManager>();
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, SocketsApplicationLifetimeService>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, SocketsApplicationLifetimeService>());
|
||||||
services.TryAddSingleton<PipelineFactory>();
|
|
||||||
services.TryAddSingleton<HttpConnectionDispatcher>();
|
services.TryAddSingleton<HttpConnectionDispatcher>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO.Pipelines;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
@ -49,10 +48,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
var loggerFactory = CreateLogger();
|
var loggerFactory = CreateLogger();
|
||||||
|
|
||||||
using (var httpClient = _testServer.CreateClient())
|
using (var httpClient = _testServer.CreateClient())
|
||||||
using (var pipelineFactory = new PipelineFactory())
|
|
||||||
{
|
{
|
||||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||||
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"),
|
||||||
|
new JsonNetInvocationAdapter(), transport, httpClient, loggerFactory))
|
||||||
{
|
{
|
||||||
EnsureConnectionEstablished(connection);
|
EnsureConnectionEstablished(connection);
|
||||||
|
|
||||||
|
|
@ -70,10 +69,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
const string originalMessage = "SignalR";
|
const string originalMessage = "SignalR";
|
||||||
|
|
||||||
using (var httpClient = _testServer.CreateClient())
|
using (var httpClient = _testServer.CreateClient())
|
||||||
using (var pipelineFactory = new PipelineFactory())
|
|
||||||
{
|
{
|
||||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||||
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"),
|
||||||
|
new JsonNetInvocationAdapter(), transport, httpClient, loggerFactory))
|
||||||
{
|
{
|
||||||
EnsureConnectionEstablished(connection);
|
EnsureConnectionEstablished(connection);
|
||||||
|
|
||||||
|
|
@ -91,10 +90,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
const string originalMessage = "SignalR";
|
const string originalMessage = "SignalR";
|
||||||
|
|
||||||
using (var httpClient = _testServer.CreateClient())
|
using (var httpClient = _testServer.CreateClient())
|
||||||
using (var pipelineFactory = new PipelineFactory())
|
|
||||||
{
|
{
|
||||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||||
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"),
|
||||||
|
new JsonNetInvocationAdapter(), transport, httpClient, loggerFactory))
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<string>();
|
var tcs = new TaskCompletionSource<string>();
|
||||||
connection.On("Echo", new[] { typeof(string) }, a =>
|
connection.On("Echo", new[] { typeof(string) }, a =>
|
||||||
|
|
@ -118,10 +117,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
var loggerFactory = CreateLogger();
|
var loggerFactory = CreateLogger();
|
||||||
|
|
||||||
using (var httpClient = _testServer.CreateClient())
|
using (var httpClient = _testServer.CreateClient())
|
||||||
using (var pipelineFactory = new PipelineFactory())
|
|
||||||
{
|
{
|
||||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||||
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"),
|
||||||
|
new JsonNetInvocationAdapter(), transport, httpClient, loggerFactory))
|
||||||
{
|
{
|
||||||
EnsureConnectionEstablished(connection);
|
EnsureConnectionEstablished(connection);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Moq.Protected;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
|
{
|
||||||
|
public class ConnectionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectionReturnsUrlUsedToStartTheConnection()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
var connectionUrl = new Uri("http://fakeuri.org/");
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
{
|
||||||
|
using (var connection = await Connection.ConnectAsync(connectionUrl, longPollingTransport, httpClient))
|
||||||
|
{
|
||||||
|
Assert.Equal(connectionUrl, connection.Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(longPollingTransport.Running, await Task.WhenAny(Task.Delay(1000), longPollingTransport.Running));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TransportIsClosedWhenConnectionIsDisposed()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
{
|
||||||
|
using (var connection = await Connection.ConnectAsync(new Uri("http://fakeuri.org/"), longPollingTransport, httpClient))
|
||||||
|
{
|
||||||
|
Assert.False(longPollingTransport.Running.IsCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(longPollingTransport.Running, await Task.WhenAny(Task.Delay(1000), longPollingTransport.Running));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanSendData()
|
||||||
|
{
|
||||||
|
var sendTcs = new TaskCompletionSource<byte[]>();
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (request.RequestUri.AbsolutePath.EndsWith("/send"))
|
||||||
|
{
|
||||||
|
sendTcs.SetResult(await request.Content.ReadAsByteArrayAsync());
|
||||||
|
}
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
using (var connection = await Connection.ConnectAsync(new Uri("http://fakeuri.org/"), longPollingTransport, httpClient))
|
||||||
|
{
|
||||||
|
Assert.False(connection.Input.Completion.IsCompleted);
|
||||||
|
|
||||||
|
var data = new byte[] { 1, 1, 2, 3, 5, 8 };
|
||||||
|
connection.Output.TryWrite(
|
||||||
|
new Message(ReadableBuffer.Create(data).Preserve(), Format.Binary));
|
||||||
|
|
||||||
|
Assert.Equal(sendTcs.Task, await Task.WhenAny(Task.Delay(1000), sendTcs.Task));
|
||||||
|
Assert.Equal(data, sendTcs.Task.Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanReceiveData()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
var content = string.Empty;
|
||||||
|
if (request.RequestUri.AbsolutePath.EndsWith("/poll"))
|
||||||
|
{
|
||||||
|
content = "42";
|
||||||
|
}
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(content) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
using (var connection = await Connection.ConnectAsync(new Uri("http://fakeuri.org/"), longPollingTransport, httpClient))
|
||||||
|
{
|
||||||
|
Assert.False(connection.Input.Completion.IsCompleted);
|
||||||
|
|
||||||
|
await connection.Input.WaitToReadAsync();
|
||||||
|
Message message;
|
||||||
|
connection.Input.TryRead(out message);
|
||||||
|
using (message)
|
||||||
|
{
|
||||||
|
Assert.Equal("42", Encoding.UTF8.GetString(message.Payload.Buffer.ToArray(), 0, message.Payload.Buffer.Length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanCloseConnection()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
using (var connection = await Connection.ConnectAsync(new Uri("http://fakeuri.org/"), longPollingTransport, httpClient))
|
||||||
|
{
|
||||||
|
Assert.False(connection.Input.Completion.IsCompleted);
|
||||||
|
connection.Output.TryComplete();
|
||||||
|
|
||||||
|
var whenAnyTask = Task.WhenAny(Task.Delay(1000), connection.Input.Completion);
|
||||||
|
|
||||||
|
// The channel needs to be drained for the Completion task to be completed
|
||||||
|
Message message;
|
||||||
|
while (!whenAnyTask.IsCompleted)
|
||||||
|
{
|
||||||
|
connection.Input.TryRead(out message);
|
||||||
|
message.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(connection.Input.Completion, await whenAnyTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading.Tasks.Channels;
|
||||||
|
using Microsoft.AspNetCore.Sockets.Internal;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Moq.Protected;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
|
{
|
||||||
|
public class LongPollingTransportTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task LongPollingTransportStopsPollAndSendLoopsWhenTransportDisposed()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
Task transportActiveTask;
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
{
|
||||||
|
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
||||||
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
|
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
||||||
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
|
transportActiveTask = longPollingTransport.Running;
|
||||||
|
|
||||||
|
Assert.False(transportActiveTask.IsCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(transportActiveTask, await Task.WhenAny(Task.Delay(1000), transportActiveTask));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LongPollingTransportStopsWhenPollReceives204()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.NoContent) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
{
|
||||||
|
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
||||||
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
|
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
||||||
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
|
Assert.Equal(longPollingTransport.Running, await Task.WhenAny(Task.Delay(1000), longPollingTransport.Running));
|
||||||
|
Assert.True(transportToConnection.In.Completion.IsCompleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LongPollingTransportStopsWhenPollRequestFails()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
{
|
||||||
|
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
||||||
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
|
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
||||||
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
|
Assert.Equal(longPollingTransport.Running, await Task.WhenAny(Task.Delay(1000), longPollingTransport.Running));
|
||||||
|
var exception = await Assert.ThrowsAsync<HttpRequestException>(async () => await transportToConnection.In.Completion);
|
||||||
|
Assert.Contains(" 500 ", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LongPollingTransportStopsWhenSendRequestFails()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var statusCode = request.RequestUri.AbsolutePath.EndsWith("send")
|
||||||
|
? HttpStatusCode.InternalServerError
|
||||||
|
: HttpStatusCode.OK;
|
||||||
|
return new HttpResponseMessage(statusCode) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
{
|
||||||
|
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
||||||
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
|
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
||||||
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
|
await connectionToTransport.Out.WriteAsync(new Message());
|
||||||
|
|
||||||
|
Assert.Equal(longPollingTransport.Running, await Task.WhenAny(Task.Delay(1000), longPollingTransport.Running));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<HttpRequestException>(async () => await longPollingTransport.Running);
|
||||||
|
|
||||||
|
// The channel needs to be drained for the Completion task to be completed
|
||||||
|
Message message;
|
||||||
|
while (transportToConnection.In.TryRead(out message))
|
||||||
|
{
|
||||||
|
message.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<HttpRequestException>(async () => await transportToConnection.In.Completion);
|
||||||
|
Assert.Contains(" 500 ", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LongPollingTransportShutsDownWhenChannelIsClosed()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
using (var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory()))
|
||||||
|
{
|
||||||
|
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
||||||
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
|
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
||||||
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
|
connectionToTransport.Out.Complete();
|
||||||
|
|
||||||
|
Assert.Equal(longPollingTransport.Running, await Task.WhenAny(Task.Delay(1000), longPollingTransport.Running));
|
||||||
|
Assert.Equal(connectionToTransport.In.Completion, await Task.WhenAny(Task.Delay(1000), connectionToTransport.In.Completion));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,11 @@
|
||||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"Microsoft.AspNetCore.Sockets.Client": "1.0.0-*",
|
||||||
|
"Microsoft.Extensions.Logging": "1.2.0-*",
|
||||||
"dotnet-test-xunit": "2.2.0-*",
|
"dotnet-test-xunit": "2.2.0-*",
|
||||||
|
"Moq": "4.6.36-*",
|
||||||
"xunit": "2.2.0-*"
|
"xunit": "2.2.0-*"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks.Channels;
|
using System.Threading.Tasks.Channels;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue