add all
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP1
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const byte CR = 13;
|
||||
public const byte LF = 10;
|
||||
|
||||
public static readonly byte[] EOL = { Constants.CR, Constants.LF };
|
||||
|
||||
public static readonly byte[] HeaderValueSeparator = { (byte)':', (byte)' ' };
|
||||
|
||||
// expect: 100-continue
|
||||
//public static readonly byte[] Expect100Continue = { (byte)'e', (byte)'x', (byte)'p', (byte)'e', (byte)'c', (byte)'t', (byte)':', (byte)' ', (byte)'1', (byte)'0', (byte)'0', (byte)'-', (byte)'c', (byte)'o', (byte)'n', (byte)'t', (byte)'i', (byte)'n', (byte)'u', (byte)'e', CR, LF };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7fa489dfa1912f42974e02a6cf10e9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,605 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Request.Upload;
|
||||
using Best.HTTP.Response;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
||||
using Best.HTTP.Shared.PlatformSupport.Threading;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP1
|
||||
{
|
||||
public sealed class HTTP1ContentConsumer : IHTTPRequestHandler, IContentConsumer, IDownloadContentBufferAvailable, IThreadSignaler
|
||||
{
|
||||
public ShutdownTypes ShutdownType { get; private set; }
|
||||
|
||||
public KeepAliveHeader KeepAlive { get { return this._keepAlive; } }
|
||||
private KeepAliveHeader _keepAlive;
|
||||
|
||||
public bool CanProcessMultiple { get { return false; } }
|
||||
|
||||
/// <summary>
|
||||
/// Number of assigned requests to process.
|
||||
/// </summary>
|
||||
public int AssignedRequests => this.conn.CurrentRequest == null ? 0 : 1;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of assignable requests
|
||||
/// </summary>
|
||||
public int MaxAssignedRequests => 1;
|
||||
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
public PeekableContentProviderStream ContentProvider { get; private set; }
|
||||
|
||||
private readonly HTTPOverTCPConnection conn;
|
||||
private PeekableHTTP1Response _response;
|
||||
private int _isAlreadyProcessingContent;
|
||||
private WriteOnlyBufferedStream _upStream;
|
||||
|
||||
private int _isSendingContent;
|
||||
private int _hasMoreData;
|
||||
private bool _isContentSendingFinished;
|
||||
private int _acceptContentSignals;
|
||||
|
||||
// Initialize the progress report variables
|
||||
private long _uploaded = 0;
|
||||
|
||||
public HTTP1ContentConsumer(HTTPOverTCPConnection conn)
|
||||
{
|
||||
this.Context = new LoggingContext(this);
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
public void RunHandler()
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), "Started processing request", this.Context);
|
||||
|
||||
//ThreadedRunner.SetThreadName("Best.HTTP1 Write");
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (this.conn.CurrentRequest.TimeoutSettings.IsTimedOut(now))
|
||||
throw new TimeoutException();
|
||||
|
||||
if (this.conn.CurrentRequest.IsCancellationRequested)
|
||||
throw new Exception("Cancellation requested!");
|
||||
|
||||
// create the response before we would send out the request, because sending out might cause an exception
|
||||
// and the response is used for decision making in the FinishedProcessing call.
|
||||
if (this._response == null)
|
||||
this.conn.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.conn.CurrentRequest, false, this);
|
||||
|
||||
// Write the request to the stream
|
||||
this.conn.CurrentRequest.TimeoutSettings.SetProcessing(now);
|
||||
|
||||
this.conn.CurrentRequest.Timing.StartNext(TimingEventNames.Request_Sent);
|
||||
|
||||
SendOutTo(this.conn.CurrentRequest, this.conn.TopStream);
|
||||
|
||||
this.conn.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendOutTo(HTTPRequest request, System.IO.Stream stream)
|
||||
{
|
||||
request.Prepare();
|
||||
|
||||
string requestPathAndQuery =
|
||||
request.ProxySettings.HasProxyFor(request.CurrentUri) ?
|
||||
request.ProxySettings.Proxy.GetRequestPath(request.CurrentUri) :
|
||||
request.CurrentUri.GetRequestPathAndQueryURL();
|
||||
|
||||
string requestLine = string.Format("{0} {1} HTTP/1.1", HTTPRequest.MethodNames[(byte)request.MethodType], requestPathAndQuery);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HTTPRequest", string.Format("Sending request: '{0}'", requestLine), request.Context);
|
||||
|
||||
// Create a buffer stream that will not close 'stream' when disposed or closed.
|
||||
// buffersize should be larger than UploadChunkSize as it might be used for uploading user data and
|
||||
// it should have enough room for UploadChunkSize data and additional chunk information.
|
||||
using (WriteOnlyBufferedStream bufferStream = new WriteOnlyBufferedStream(stream, (int)(request.UploadSettings.UploadChunkSize * 1.5f), request.Context))
|
||||
{
|
||||
var requestLineBytes = requestLine.GetASCIIBytes();
|
||||
bufferStream.WriteBufferSegment(requestLineBytes);
|
||||
bufferStream.WriteArray(EOL);
|
||||
|
||||
BufferPool.Release(requestLineBytes);
|
||||
|
||||
// Write headers to the buffer
|
||||
request.EnumerateHeaders((header, values) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(header) || values == null)
|
||||
return;
|
||||
|
||||
//var headerName = string.Concat(header, ": ").GetASCIIBytes();
|
||||
var headerName = header.GetASCIIBytes();
|
||||
|
||||
for (int i = 0; i < values.Count; ++i)
|
||||
{
|
||||
if (string.IsNullOrEmpty(values[i]))
|
||||
{
|
||||
HTTPManager.Logger.Warning("HTTPRequest", string.Format("Null/empty value for header: {0}", header), request.Context);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Verbose("HTTPRequest", $"Header - '{header}': '{values[i]}'", request.Context);
|
||||
|
||||
var valueBytes = values[i].GetASCIIBytes();
|
||||
|
||||
bufferStream.WriteBufferSegment(headerName);
|
||||
bufferStream.WriteArray(HeaderValueSeparator);
|
||||
bufferStream.WriteBufferSegment(valueBytes);
|
||||
bufferStream.WriteArray(EOL);
|
||||
|
||||
BufferPool.Release(valueBytes);
|
||||
}
|
||||
|
||||
BufferPool.Release(headerName);
|
||||
}, /*callBeforeSendCallback:*/ true);
|
||||
|
||||
bufferStream.WriteArray(EOL);
|
||||
|
||||
// Send body content
|
||||
if (this.conn.CurrentRequest.UploadSettings.UploadStream is Request.Upload.UploadStreamBase upStream)
|
||||
upStream.BeforeSendBody(this.conn.CurrentRequest, this);
|
||||
|
||||
Interlocked.Exchange(ref this._isSendingContent, 1);
|
||||
Interlocked.Exchange(ref this._acceptContentSignals, 1);
|
||||
|
||||
SendContent(bufferStream);
|
||||
} // bufferStream.Dispose
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("HTTPRequest", "Sent out '" + requestLine + "'", this.Context);
|
||||
}
|
||||
|
||||
void SendContent(WriteOnlyBufferedStream streamToSend)
|
||||
{
|
||||
Interlocked.Exchange(ref this._hasMoreData, 0);
|
||||
|
||||
var request = this.conn.CurrentRequest;
|
||||
System.IO.Stream uploadStream = request.UploadSettings.UploadStream;
|
||||
|
||||
try
|
||||
{
|
||||
if (uploadStream == null)
|
||||
{
|
||||
this._isContentSendingFinished = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (streamToSend == null)
|
||||
{
|
||||
if (this._upStream == null)
|
||||
this._upStream = new WriteOnlyBufferedStream(this.conn.TopStream, (int)(request.UploadSettings.UploadChunkSize * 1.5f), request.Context);
|
||||
streamToSend = this._upStream;
|
||||
}
|
||||
long uploadLength = uploadStream.Length;
|
||||
bool isChunked = uploadLength == BodyLengths.UnknownWithChunkedTransferEncoding;
|
||||
|
||||
// Upload buffer. First we will read the data into this buffer from the UploadStream, then write this buffer to our outStream
|
||||
byte[] buffer = BufferPool.Get(this.conn.CurrentRequest.UploadSettings.UploadChunkSize, true);
|
||||
using var _ = new AutoReleaseBuffer(buffer);
|
||||
|
||||
// How many bytes was read from the UploadStream
|
||||
int count = uploadStream.Read(buffer, 0, buffer.Length);
|
||||
while (count > 0)
|
||||
{
|
||||
if (isChunked)
|
||||
{
|
||||
var countBytes = count.ToString("X").GetASCIIBytes();
|
||||
streamToSend.WriteBufferSegment(countBytes);
|
||||
streamToSend.WriteArray(EOL);
|
||||
|
||||
BufferPool.Release(countBytes);
|
||||
}
|
||||
|
||||
// write out the buffer to the wire
|
||||
streamToSend.Write(buffer, 0, count);
|
||||
|
||||
// chunk trailing EOL
|
||||
if (uploadLength < 0)
|
||||
streamToSend.WriteArray(EOL);
|
||||
|
||||
// update how many bytes are uploaded
|
||||
_uploaded += count;
|
||||
|
||||
if (this.conn.CurrentRequest.UploadSettings.OnUploadProgress != null)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.conn.CurrentRequest, RequestEvents.UploadProgress, _uploaded, uploadLength));
|
||||
|
||||
if (this.conn.CurrentRequest.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
count = uploadStream.Read(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
this._isContentSendingFinished = count == UploadReadConstants.Completed;
|
||||
|
||||
// All data from the stream are sent, write the 'end' chunk if necessary
|
||||
if (isChunked && this._isContentSendingFinished)
|
||||
{
|
||||
ReadOnlySpan<byte> bytes = stackalloc byte[] { (byte)'0' };
|
||||
|
||||
streamToSend.Write(bytes);
|
||||
streamToSend.WriteArray(EOL);
|
||||
streamToSend.WriteArray(EOL);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Make sure all remaining data will be on the wire
|
||||
streamToSend?.Flush();
|
||||
|
||||
if (this._isContentSendingFinished)
|
||||
{
|
||||
this._upStream?.Dispose();
|
||||
this._upStream = null;
|
||||
if (this.conn.CurrentRequest.UploadSettings.DisposeStream)
|
||||
uploadStream?.Dispose();
|
||||
|
||||
this.conn.CurrentRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
|
||||
|
||||
Interlocked.Exchange(ref this._isSendingContent, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Exchange(ref this._isSendingContent, 0);
|
||||
|
||||
if (this._hasMoreData > 0)
|
||||
(this as IThreadSignaler).SignalThread(SignalHandlerTypes.Signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
|
||||
{
|
||||
//HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), "IDownloadContentBufferAvailable.BufferAvailable", this.Context);
|
||||
|
||||
// TODO: Do NOT call OnContent on the Unity main thread
|
||||
if (this._response != null)
|
||||
OnContent();
|
||||
}
|
||||
|
||||
public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
|
||||
|
||||
public void UnsetBinding() => this.ContentProvider = null;
|
||||
|
||||
public void OnContent()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref this._isAlreadyProcessingContent, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
//HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnContent({peekable?.Length}, {this._response?.ReadState})", this.Context, this.conn.CurrentRequest.Context);
|
||||
try
|
||||
{
|
||||
if (this.conn.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
|
||||
throw new TimeoutException();
|
||||
|
||||
if (this.conn.CurrentRequest.IsCancellationRequested)
|
||||
throw new Exception("Cancellation requested!");
|
||||
|
||||
this._response.ProcessPeekable(this.ContentProvider);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
// After an exception, this._response will be null!
|
||||
if (this._response != null)
|
||||
{
|
||||
if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
FinishedProcessing(null);
|
||||
else if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.WaitForContentSent)
|
||||
{
|
||||
(this as IThreadSignaler).SignalThread(SignalHandlerTypes.Signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref this._isAlreadyProcessingContent, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnConnectionClosed()
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnConnectionClosed({this.ContentProvider?.Length}, {this._response?.ReadState})", this.Context);
|
||||
|
||||
if (this._response != null &&
|
||||
this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Content &&
|
||||
this._response.DeliveryMode == PeekableHTTP1Response.ContentDeliveryMode.RawUnknownLength &&
|
||||
"close".Equals(this._response.GetFirstHeaderValue("connection"), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FinishedProcessing(null);
|
||||
}
|
||||
else if (this.ContentProvider != null && this.ContentProvider.Length > 0 &&
|
||||
this._response != null &&
|
||||
this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Content &&
|
||||
this._response.DownStream != null)
|
||||
{
|
||||
// Let the stream comsume any buffered data first, and handle closure when the buffer depletes.
|
||||
// TODO: This require however that the PeekableResponse ping this HTTP1ContentConsumer when buffer space is available in the down-stream.
|
||||
// Or force-add all remaining data to the stream and see whether we finished downloading or not.
|
||||
// Problems:
|
||||
// 1.) OnContent might be already called and a call to it would be dropped. We could spin up a new thread waiting for its finish, then call it again.
|
||||
//throw new NotImplementedException();
|
||||
ThreadedRunner.RunShortLiving(() =>
|
||||
{
|
||||
SpinWait spinWait = new SpinWait();
|
||||
|
||||
while (Interlocked.CompareExchange(ref this._isAlreadyProcessingContent, 1, 0) == 1)
|
||||
spinWait.SpinOnce();
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
this._response.DownStream.EmergencyIncreaseMaxBuffered();
|
||||
this._response.ProcessPeekable(this.ContentProvider);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// After an exception, this._response will be null!
|
||||
if (this._response != null)
|
||||
{
|
||||
if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
FinishedProcessing(null);
|
||||
else
|
||||
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref this._isAlreadyProcessingContent, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the consumer still have a request: error it and close the connection
|
||||
if (this.conn.CurrentRequest != null && this._response != null)
|
||||
{
|
||||
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
|
||||
}
|
||||
else // If no current request: close the connection
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, HTTPConnectionStates.Closed));
|
||||
}
|
||||
|
||||
public void OnError(Exception e)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnError({this.ContentProvider?.Length}, {this._response?.ReadState}, {this.ShutdownType})", this.Context);
|
||||
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
private void OnCancellationRequested(HTTPRequest req)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), "OnCancellationRequested()", this.Context);
|
||||
|
||||
Interlocked.Exchange(ref this._response, null);
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
this.conn?.Streamer?.Dispose();
|
||||
}
|
||||
|
||||
void FinishedProcessing(Exception ex)
|
||||
{
|
||||
// Warning: FinishedProcessing might be called from different threads in parallel:
|
||||
// - send thread triggered by a write failure
|
||||
// - read thread oncontent/OnError/OnConnectionClosed
|
||||
|
||||
var resp = Interlocked.Exchange(ref this._response, null);
|
||||
if (resp == null)
|
||||
return;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), $"{nameof(FinishedProcessing)}({resp.ReadState}, {ex})", this.Context);
|
||||
|
||||
// Unset the consumer, we no longer expect another OnContent call until further notice.
|
||||
//if (conn.TopStream is IPeekableContentProvider provider && provider?.Consumer == this)
|
||||
// provider.Consumer = null;
|
||||
this.ContentProvider.UnbindIf(this);
|
||||
|
||||
var req = this.conn.CurrentRequest;
|
||||
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
|
||||
bool resendRequest = false;
|
||||
HTTPRequestStates requestState = HTTPRequestStates.Finished;
|
||||
HTTPConnectionStates connectionState = ex != null ? HTTPConnectionStates.Closed : HTTPConnectionStates.Recycle;
|
||||
|
||||
// We could finish the request, ignore the error.
|
||||
if (resp.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
ex = null;
|
||||
|
||||
Exception error = ex;
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
// Timeout is a non-retryable error
|
||||
if (ex is TimeoutException)
|
||||
{
|
||||
error = null;
|
||||
requestState = HTTPRequestStates.TimedOut;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries && !HTTPManager.IsQuitting)
|
||||
{
|
||||
req.RetrySettings.Retries++;
|
||||
error = null;
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
requestState = HTTPRequestStates.Error;
|
||||
}
|
||||
}
|
||||
|
||||
// Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
|
||||
connectionState = HTTPConnectionStates.Closed;
|
||||
|
||||
resp.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// After HandleResponse connectionState can have the following values:
|
||||
// - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
|
||||
// - Closed: server sent an connection: close header.
|
||||
// - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
|
||||
// In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.
|
||||
|
||||
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref this._keepAlive, this.Context);
|
||||
|
||||
if (error != null)
|
||||
requestState = HTTPRequestStates.Error;
|
||||
else if (!resendRequest && resp.IsUpgraded)
|
||||
requestState = HTTPRequestStates.Processing;
|
||||
}
|
||||
|
||||
req.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);
|
||||
|
||||
// If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
|
||||
if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
|
||||
{
|
||||
ConnectionHelper.ResendRequestAndCloseConnection(this.conn, req);
|
||||
}
|
||||
else if (resendRequest && requestState == HTTPRequestStates.Finished)
|
||||
{
|
||||
(conn.TopStream as IPeekableContentProvider).SwitchIf(null, this.conn as IContentConsumer);
|
||||
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, connectionState));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (resp == null || !resp.IsUpgraded)
|
||||
(conn.TopStream as IPeekableContentProvider).SwitchIf(null, this.conn as IContentConsumer);
|
||||
|
||||
// Otherwise set the request's then the connection's state
|
||||
ConnectionHelper.EnqueueEvents(this.conn, connectionState, req, requestState, error);
|
||||
}
|
||||
}
|
||||
|
||||
public void Process(HTTPRequest request)
|
||||
{
|
||||
(conn.TopStream as IPeekableContentProvider).SetTwoWayBinding(this);
|
||||
|
||||
// https://github.com/Benedicht/BestHTTP-Issues/issues/179
|
||||
// Toughts:
|
||||
// - Many requests, especially if they are uploading slowly, can occupy all background threads.
|
||||
// Use short-living thread when:
|
||||
// - It's a GET request
|
||||
// - It's not an upgrade request
|
||||
|
||||
bool isRequestWithoutBody = request.MethodType == HTTPMethods.Get ||
|
||||
request.MethodType == HTTPMethods.Head ||
|
||||
request.MethodType == HTTPMethods.Delete ||
|
||||
request.MethodType == HTTPMethods.Options;
|
||||
bool isUpgrade = request.HasHeader("upgrade");
|
||||
|
||||
var useShortLivingThread = HTTPManager.PerHostSettings.Get(request.CurrentHostKey).HTTP1ConnectionSettings.ForceUseThreadPool ||
|
||||
(isRequestWithoutBody && !isUpgrade);
|
||||
|
||||
if (useShortLivingThread)
|
||||
ThreadedRunner.RunShortLiving(RunHandler);
|
||||
else
|
||||
ThreadedRunner.RunLongLiving(RunHandler);
|
||||
}
|
||||
|
||||
public void Shutdown(ShutdownTypes type)
|
||||
{
|
||||
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), string.Format($"Shutdown({type})"), this.Context);
|
||||
this.ShutdownType = type;
|
||||
}
|
||||
|
||||
void IThreadSignaler.SignalThread(SignalHandlerTypes signalType)
|
||||
{
|
||||
Interlocked.Exchange(ref this._hasMoreData, 1);
|
||||
|
||||
if (this._acceptContentSignals == 1 && Interlocked.CompareExchange(ref this._isSendingContent, 1, 0) == 0)
|
||||
{
|
||||
switch (signalType)
|
||||
{
|
||||
case SignalHandlerTypes.Signal:
|
||||
ThreadedRunner.RunShortLiving(SignalThreadHandler);
|
||||
break;
|
||||
|
||||
case SignalHandlerTypes.InlineIfPossible:
|
||||
SignalThreadHandler();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalThreadHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
SendContent(null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
this._upStream?.Dispose();
|
||||
this._upStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c761c8505fda1a4e9e6be9f79b74054
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,540 @@
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Response.Decompression;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
||||
using static Best.HTTP.Response.HTTPStatusCodes;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP1
|
||||
{
|
||||
/// <summary>
|
||||
/// An HTTP 1.1 response implementation that can utilize a peekable stream.
|
||||
/// Its main entry point is the ProcessPeekable method that should be called after every chunk of data downloaded.
|
||||
/// </summary>
|
||||
public class PeekableHTTP1Response : HTTPResponse
|
||||
{
|
||||
public PeekableReadState ReadState
|
||||
{
|
||||
get => this._readState;
|
||||
private set
|
||||
{
|
||||
if (this._readState != value && HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), $"{this._readState} => {value}", this.Context);
|
||||
this._readState = value;
|
||||
}
|
||||
}
|
||||
private PeekableReadState _readState;
|
||||
|
||||
public bool ForceDepleteContent;
|
||||
|
||||
public enum ContentDeliveryMode
|
||||
{
|
||||
Raw,
|
||||
RawUnknownLength,
|
||||
Chunked,
|
||||
}
|
||||
|
||||
public enum PeekableReadState
|
||||
{
|
||||
StatusLine,
|
||||
Headers,
|
||||
WaitForContentSent, // when received a 100-continue
|
||||
PrepareForContent,
|
||||
ContentSetup,
|
||||
Content,
|
||||
Finished
|
||||
}
|
||||
|
||||
public ContentDeliveryMode DeliveryMode => this._deliveryMode;
|
||||
private ContentDeliveryMode _deliveryMode;
|
||||
private long _expectedLength;
|
||||
private Dictionary<string, List<string>> _newHeaders;
|
||||
|
||||
long _downloaded = 0;
|
||||
IDecompressor _decompressor = null;
|
||||
bool _compressed = false;
|
||||
bool sendProgressChanged;
|
||||
|
||||
int _chunkLength = -1;
|
||||
|
||||
enum ReadChunkedStates
|
||||
{
|
||||
ReadChunkLength,
|
||||
ReadChunk,
|
||||
ReadTrailingCRLF,
|
||||
ReadTrailingHeaders
|
||||
}
|
||||
ReadChunkedStates _readChunkedState = ReadChunkedStates.ReadChunkLength;
|
||||
|
||||
IDownloadContentBufferAvailable _bufferAvailableHandler;
|
||||
|
||||
public PeekableHTTP1Response(HTTPRequest request, bool isFromCache, IDownloadContentBufferAvailable bufferAvailableHandler)
|
||||
: base(request, isFromCache)
|
||||
{
|
||||
this._bufferAvailableHandler = bufferAvailableHandler;
|
||||
}
|
||||
|
||||
private int _isProccessing;
|
||||
|
||||
public void ProcessPeekable(PeekableContentProviderStream peekable)
|
||||
{
|
||||
// To avoid executing ProcessPeekable in parallel on two threads, do an atomic CompareExchange and return if the old value wasn't 0.
|
||||
if (Interlocked.CompareExchange(ref this._isProccessing, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"ProcessPeekable({this.ReadState}, {peekable.Length})", this.Context);
|
||||
|
||||
try
|
||||
{
|
||||
// The first call after setting it to PeekableReadState.WaitForContentSent is after the the client could send its content.
|
||||
// This also works when "If the request did not contain an Expect header field containing the 100-continue expectation,
|
||||
// the client can simply discard this interim response."
|
||||
// (https://www.rfc-editor.org/rfc/rfc9110#section-15.2.1-3)
|
||||
if (this._readState == PeekableReadState.WaitForContentSent)
|
||||
{
|
||||
this._newHeaders?.Clear();
|
||||
this.Headers?.Clear();
|
||||
|
||||
this._readState = PeekableReadState.StatusLine;
|
||||
}
|
||||
|
||||
// It's an unexpected network closure, except when we reading the content in the RawUnknownLength delivery mode.
|
||||
if (peekable == null && ReadState != PeekableReadState.Content && this._deliveryMode != ContentDeliveryMode.RawUnknownLength)
|
||||
throw new Exception("Server closed the connection unexpectedly!");
|
||||
|
||||
switch (ReadState)
|
||||
{
|
||||
case PeekableReadState.StatusLine:
|
||||
if (!IsNewLinePresent(peekable))
|
||||
return;
|
||||
|
||||
Request.Timing.StartNext(TimingEventNames.Headers);
|
||||
|
||||
var statusLine = HTTPResponse.ReadTo(peekable, (byte)' ');
|
||||
string[] versions = statusLine.Split(new char[] { '/', '.' });
|
||||
|
||||
this.HTTPVersion = new Version(int.Parse(versions[1]), int.Parse(versions[2]));
|
||||
|
||||
int statusCode;
|
||||
string statusCodeStr = NoTrimReadTo(peekable, (byte)' ', LF);
|
||||
|
||||
if (!int.TryParse(statusCodeStr, out statusCode))
|
||||
throw new Exception($"Couldn't parse '{statusCodeStr}' as a status code!");
|
||||
|
||||
this.StatusCode = statusCode;
|
||||
|
||||
if (statusCodeStr.Length > 0 && (byte)statusCodeStr[statusCodeStr.Length - 1] != LF && (byte)statusCodeStr[statusCodeStr.Length - 1] != CR)
|
||||
this.Message = ReadTo(peekable, LF);
|
||||
else
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(PeekableHTTP1Response), "Skipping Status Message reading!", this.Context);
|
||||
|
||||
this.Message = string.Empty;
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"HTTP/'{this.HTTPVersion}' '{this.StatusCode}' '{this.Message}'", this.Context);
|
||||
|
||||
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
|
||||
this._newHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
this.ReadState = PeekableReadState.Headers;
|
||||
goto case PeekableReadState.Headers;
|
||||
|
||||
case PeekableReadState.Headers:
|
||||
ProcessReadHeaders(peekable, PeekableReadState.PrepareForContent);
|
||||
if (this.ReadState == PeekableReadState.PrepareForContent)
|
||||
{
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
// When upgraded, we don't want to read the content here, so set the state to Finished.
|
||||
if (this.StatusCode == 101 && (HasHeaderWithValue("connection", "upgrade") || HasHeader("upgrade")) && this.Request?.DownloadSettings?.OnUpgraded != null)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), "Request Upgraded!", this.Context);
|
||||
|
||||
this.IsUpgraded = this.Request.DownloadSettings.OnUpgraded(this.Request, this, peekable);
|
||||
|
||||
if (this.IsUpgraded)
|
||||
{
|
||||
this._readState = PeekableReadState.Finished;
|
||||
goto case PeekableReadState.Finished;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If it's a 100-continue, restart reading the response after the client could send its content.
|
||||
if (this.StatusCode == Continue)
|
||||
{
|
||||
this._readState = PeekableReadState.WaitForContentSent;
|
||||
break;
|
||||
}
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#name-informational-1xx
|
||||
// A 1xx response is terminated by the end of the header section; it cannot contain content or trailers.
|
||||
if ((this.StatusCode >= Continue && this.StatusCode < OK) ||
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#name-204-no-content
|
||||
// A 204 response is terminated by the end of the header section; it cannot contain content or trailers.
|
||||
this.StatusCode == NoContent ||
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#name-304-not-modified
|
||||
// A 304 response is terminated by the end of the header section; it cannot contain content or trailers.
|
||||
this.StatusCode == NotModified ||
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3
|
||||
// Responses to the HEAD request method (Section 4.3.2
|
||||
// of [RFC7231]) never include a message body because the associated
|
||||
// response header fields (e.g., Transfer-Encoding, Content-Length,
|
||||
// etc.), if present, indicate only what their values would have been if
|
||||
// the request method had been GET
|
||||
this.Request.MethodType == HTTPMethods.Head)
|
||||
{
|
||||
this._readState = PeekableReadState.Finished;
|
||||
goto case PeekableReadState.Finished;
|
||||
}
|
||||
|
||||
Request.Timing.StartNext(TimingEventNames.Response_Received);
|
||||
|
||||
// if not an upgraded response, or OnUpgraded returned false, go for the content too.
|
||||
goto case PeekableReadState.PrepareForContent;
|
||||
}
|
||||
break;
|
||||
|
||||
case PeekableReadState.PrepareForContent:
|
||||
BeginReceiveContent();
|
||||
|
||||
// A content-length header might come with chunked transfer-encoding too.
|
||||
var contentLengthHeader = GetFirstHeaderValue("content-length");
|
||||
long.TryParse(contentLengthHeader, out this._expectedLength);
|
||||
|
||||
if (HasHeaderWithValue("transfer-encoding", "chunked") && string.IsNullOrEmpty(contentLengthHeader))
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.Chunked;
|
||||
this.ReadState = PeekableReadState.ContentSetup;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.Raw;
|
||||
this.ReadState = PeekableReadState.ContentSetup;
|
||||
var contentRangeHeaders = GetHeaderValues("content-range");
|
||||
|
||||
if (contentLengthHeader == null && contentRangeHeaders == null)
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.RawUnknownLength;
|
||||
}
|
||||
else if (contentLengthHeader == null && contentRangeHeaders != null)
|
||||
{
|
||||
HTTPRange range = GetRange();
|
||||
|
||||
this._expectedLength = (range.LastBytePos - range.FirstBytePos) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), $"PrepareForContent - delivery mode selected: {this._deliveryMode}, {this._expectedLength}!", this.Context);
|
||||
|
||||
CreateDownloadStream(this._bufferAvailableHandler);
|
||||
|
||||
string encoding = IsFromCache ? null : GetFirstHeaderValue("content-encoding");
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
this._compressed = !string.IsNullOrEmpty(encoding);
|
||||
|
||||
// https://github.com/Benedicht/BestHTTP-Issues/issues/183
|
||||
// If _decompressor is still null, remove the compressed flag and serve the content as-is.
|
||||
if ((this._decompressor = DecompressorFactory.GetDecompressor(encoding, this.Context)) == null)
|
||||
this._compressed = false;
|
||||
#endif
|
||||
|
||||
this.sendProgressChanged = this.Request.DownloadSettings.OnDownloadProgress != null && this.IsSuccess;
|
||||
|
||||
this.ReadState = PeekableReadState.Content;
|
||||
goto case PeekableReadState.Content;
|
||||
|
||||
case PeekableReadState.Content:
|
||||
var downStream = this.DownStream;
|
||||
if (downStream != null && downStream.MaxBuffered <= downStream.Length)
|
||||
return;
|
||||
|
||||
switch (this._deliveryMode)
|
||||
{
|
||||
case ContentDeliveryMode.Raw: ProcessReadRaw(peekable); break;
|
||||
case ContentDeliveryMode.RawUnknownLength: ProcessReadRawUnknownLength(peekable); break;
|
||||
case ContentDeliveryMode.Chunked: ProcessReadChunked(peekable); break;
|
||||
}
|
||||
|
||||
if (this.ReadState == PeekableReadState.Finished)
|
||||
goto case PeekableReadState.Finished;
|
||||
break;
|
||||
|
||||
case PeekableReadState.Finished:
|
||||
//baseRequest.Timing.StartNext(TimingEventNames.Queued_For_Disptach);
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref this._isProccessing, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsNewLinePresent(PeekableStream peekable)
|
||||
{
|
||||
peekable.BeginPeek();
|
||||
|
||||
int nextByte = peekable.PeekByte();
|
||||
while (nextByte >= 0 && nextByte != 0x0A)
|
||||
nextByte = peekable.PeekByte();
|
||||
|
||||
return nextByte == 0x0A;
|
||||
}
|
||||
|
||||
private void ProcessReadHeaders(PeekableStream peekable, PeekableReadState targetState)
|
||||
{
|
||||
if (!IsNewLinePresent(peekable))
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
string headerName = ReadTo(peekable, (byte)':', LF);
|
||||
if (headerName == string.Empty)
|
||||
{
|
||||
this.ReadState = targetState;
|
||||
|
||||
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, this._newHeaders));
|
||||
return;
|
||||
}
|
||||
|
||||
string value = ReadTo(peekable, LF);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"Header - '{headerName}': '{value}'", this.Context);
|
||||
|
||||
AddHeader(headerName, value);
|
||||
|
||||
if (this._newHeaders != null)
|
||||
{
|
||||
List<string> values;
|
||||
if (!this._newHeaders.TryGetValue(headerName, out values))
|
||||
this._newHeaders.Add(headerName, values = new List<string>(1));
|
||||
|
||||
values.Add(value);
|
||||
}
|
||||
} while (IsNewLinePresent(peekable));
|
||||
}
|
||||
|
||||
private void ProcessReadRawUnknownLength(PeekableStream peekable)
|
||||
{
|
||||
if (peekable == null)
|
||||
{
|
||||
if (sendProgressChanged)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
|
||||
|
||||
PostProcessContent();
|
||||
|
||||
this.ReadState = PeekableReadState.Finished;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
while (peekable.Length > 0)
|
||||
{
|
||||
var buffer = BufferPool.Get(64 * 1024, true, this.Context);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, buffer.Length);
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
|
||||
if (sendProgressChanged)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
|
||||
}
|
||||
|
||||
private bool TryReadChunkLength(PeekableStream peekable, out int result)
|
||||
{
|
||||
result = -1;
|
||||
if (!IsNewLinePresent(peekable))
|
||||
return false;
|
||||
|
||||
// Read until the end of line, then split the string so we will discard any optional chunk extensions
|
||||
|
||||
var buff = ReadToAsByte(peekable, LF);
|
||||
|
||||
if (buff == BufferSegment.Empty)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var chars = MemoryMarshal.Cast<byte, char>(buff.AsSpan());
|
||||
|
||||
var idx = chars.IndexOf(';');
|
||||
|
||||
if (idx == -1)
|
||||
idx = chars.Length;
|
||||
|
||||
return int.TryParse(chars.Slice(0, idx), System.Globalization.NumberStyles.AllowHexSpecifier, null, out result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BufferPool.Release(buff);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessReadChunked(PeekableStream peekable)
|
||||
{
|
||||
switch(this._readChunkedState)
|
||||
{
|
||||
case ReadChunkedStates.ReadChunkLength:
|
||||
this._readChunkedState = ReadChunkedStates.ReadChunkLength;
|
||||
|
||||
if (TryReadChunkLength(peekable, out this._chunkLength))
|
||||
{
|
||||
if (this._chunkLength == 0)
|
||||
{
|
||||
PostProcessContent();
|
||||
|
||||
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
|
||||
this._newHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
goto case ReadChunkedStates.ReadTrailingHeaders;
|
||||
}
|
||||
|
||||
goto case ReadChunkedStates.ReadChunk;
|
||||
}
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadChunk:
|
||||
this._readChunkedState = ReadChunkedStates.ReadChunk;
|
||||
|
||||
while (this._chunkLength > 0 && peekable.Length > 0)
|
||||
{
|
||||
int targetReadCount = Math.Min(Math.Min(64 * 1024, this._chunkLength), (int)peekable.Length);
|
||||
|
||||
var buffer = BufferPool.Get(targetReadCount, true, this.Context);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, targetReadCount);
|
||||
|
||||
if (readCount < 0)
|
||||
{
|
||||
BufferPool.Release(buffer);
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
}
|
||||
|
||||
this._chunkLength -= readCount;
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
|
||||
if (sendProgressChanged)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
|
||||
|
||||
// Every chunk data has a trailing CRLF
|
||||
if (this._chunkLength == 0)
|
||||
goto case ReadChunkedStates.ReadTrailingCRLF;
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadTrailingCRLF:
|
||||
this._readChunkedState = ReadChunkedStates.ReadTrailingCRLF;
|
||||
|
||||
if (IsNewLinePresent(peekable))
|
||||
{
|
||||
BufferPool.Release(HTTPResponse.ReadToAsByte(peekable, LF));
|
||||
|
||||
goto case ReadChunkedStates.ReadChunkLength;
|
||||
}
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadTrailingHeaders:
|
||||
this._readChunkedState = ReadChunkedStates.ReadTrailingHeaders;
|
||||
|
||||
ProcessReadHeaders(peekable, PeekableReadState.Finished);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessReadRaw(PeekableStream peekable)
|
||||
{
|
||||
if (this.DownStream == null)
|
||||
throw new ArgumentNullException(nameof(this.DownStream));
|
||||
if (peekable == null)
|
||||
throw new ArgumentNullException(nameof(peekable));
|
||||
|
||||
while (peekable.Length > 0 && !this.DownStream.IsFull)
|
||||
{
|
||||
var buffer = BufferPool.Get(64 * 1024, true, this.Context);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, buffer.Length);
|
||||
|
||||
if (readCount < 0)
|
||||
{
|
||||
BufferPool.Release(buffer);
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
}
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
|
||||
if (sendProgressChanged)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
|
||||
|
||||
if (this._downloaded >= this._expectedLength)
|
||||
{
|
||||
PostProcessContent();
|
||||
this.ReadState = PeekableReadState.Finished;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessChunk(BufferSegment chunk)
|
||||
{
|
||||
this._downloaded += chunk.Count;
|
||||
|
||||
if (this._compressed)
|
||||
{
|
||||
var (decompressed, release) = this._decompressor.Decompress(chunk, false, true, this.Context);
|
||||
if (decompressed != BufferSegment.Empty)
|
||||
FeedDownloadedContentChunk(decompressed);
|
||||
|
||||
//if (decompressed.Data != chunk.Data)
|
||||
if (release)
|
||||
BufferPool.Release(chunk);
|
||||
}
|
||||
else
|
||||
{
|
||||
FeedDownloadedContentChunk(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessContent()
|
||||
{
|
||||
if (this._compressed)
|
||||
{
|
||||
var (decompressed, release) = this._decompressor.Decompress(BufferSegment.Empty, true, true, this.Context);
|
||||
if (decompressed != BufferSegment.Empty)
|
||||
FeedDownloadedContentChunk(decompressed);
|
||||
}
|
||||
|
||||
FinishedContentReceiving();
|
||||
|
||||
if (this._decompressor != null)
|
||||
{
|
||||
this._decompressor.Dispose();
|
||||
this._decompressor = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
this._decompressor?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52e5f71e9a1d66d4ab61f2f2eca03ff0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user