Files
ichni_Official/Packages/com.tivadar.best.http/Runtime/HTTP/Hosts/Connections/WebGL/WebGLXHRConnection.cs
2026-06-15 18:18:16 +08:00

333 lines
13 KiB
C#

#if UNITY_WEBGL && !UNITY_EDITOR
using System;
using System.IO;
using System.Threading;
using Best.HTTP.Caching;
using Best.HTTP.Hosts.Connections.File;
using Best.HTTP.Hosts.Connections.HTTP1;
using Best.HTTP.HostSetting;
using Best.HTTP.Request.Authentication;
using Best.HTTP.Request.Timings;
using Best.HTTP.Shared;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Hosts.Connections.WebGL
{
class PeekableIncomingSegmentContentProviderStream : PeekableContentProviderStream
{
private int peek_listIdx;
private int peek_pos;
public override void BeginPeek()
{
peek_listIdx = 0;
peek_pos = base.bufferList.Count > 0 ? base.bufferList[0].Offset : 0;
}
public override int PeekByte()
{
if (base.bufferList.Count == 0)
return -1;
var segment = base.bufferList[this.peek_listIdx];
if (peek_pos >= segment.Offset + segment.Count)
{
if (base.bufferList.Count <= this.peek_listIdx + 1)
return -1;
segment = base.bufferList[++this.peek_listIdx];
this.peek_pos = segment.Offset;
}
return segment.Data[this.peek_pos++];
}
}
internal sealed class WebGLXHRConnection : ConnectionBase
{
public PeekableContentProviderStream ContentProvider { get; private set; }
int NativeId;
PeekableHTTP1Response _response;
public WebGLXHRConnection(HostKey hostKey)
: base(hostKey, false)
{
WebGLXHRNativeInterface.XHR_SetLoglevel((byte)HTTPManager.Logger.Level);
}
public override void Shutdown(ShutdownTypes type)
{
base.Shutdown(type);
WebGLXHRNativeInterface.XHR_Abort(this.NativeId);
}
protected override void ThreadFunc()
{
// XmlHttpRequest setup
CurrentRequest.Prepare();
Credentials credentials = null;// CurrentRequest.Authenticator?.Credentials;
this.NativeId = WebGLXHRNativeInterface.XHR_Create(HTTPRequest.MethodNames[(byte)CurrentRequest.MethodType],
CurrentRequest.CurrentUri.OriginalString,
credentials?.UserName, credentials?.Password, CurrentRequest.WithCredentials ? 1 : 0);
WebGLXHRNativeConnectionLayer.Add(NativeId, this);
CurrentRequest.EnumerateHeaders((header, values) =>
{
if (!header.Equals("Content-Length"))
for (int i = 0; i < values.Count; ++i)
WebGLXHRNativeInterface.XHR_SetRequestHeader(NativeId, header, values[i]);
}, /*callBeforeSendCallback:*/ true);
WebGLXHRNativeConnectionLayer.SetupHandlers(NativeId, CurrentRequest);
WebGLXHRNativeInterface.XHR_SetTimeout(NativeId, (uint)(CurrentRequest.TimeoutSettings.ConnectTimeout.TotalMilliseconds + CurrentRequest.TimeoutSettings.Timeout.TotalMilliseconds));
Stream upStream = CurrentRequest.UploadSettings.UploadStream;
byte[] body = null;
int length = 0;
bool releaseBodyBuffer = false;
if (upStream != null)
{
var internalBuffer = BufferPool.Get(upStream.Length > 0 ? upStream.Length : CurrentRequest.UploadSettings.UploadChunkSize, true);
using (BufferPoolMemoryStream ms = new BufferPoolMemoryStream(internalBuffer, 0, internalBuffer.Length, true, true, false, true))
{
var buffer = BufferPool.Get(CurrentRequest.UploadSettings.UploadChunkSize, true);
int readCount = -1;
while ((readCount = upStream.Read(buffer, 0, buffer.Length)) > 0)
ms.Write(buffer, 0, readCount);
BufferPool.Release(buffer);
length = (int)ms.Position;
body = ms.GetBuffer();
releaseBodyBuffer = true;
}
}
if (this._response == null)
this.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.CurrentRequest, false, null);
this.ContentProvider = new PeekableIncomingSegmentContentProviderStream();
WebGLXHRNativeInterface.XHR_Send(NativeId, body, length);
if (releaseBodyBuffer)
BufferPool.Release(body);
this.CurrentRequest.TimeoutSettings.QueuedAt = DateTime.MinValue;
this.CurrentRequest.TimeoutSettings.ProcessingStarted = DateTime.UtcNow;
this.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
}
#region Callback Implementations
private void OnCancellationRequested(HTTPRequest req)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnCancellationRequested()", this.Context);
Interlocked.Exchange(ref this._response, null);
req.OnCancellationRequested -= OnCancellationRequested;
WebGLXHRNativeInterface.XHR_Abort(this.NativeId);
}
internal void OnBuffer(BufferSegment buffer)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnBuffer({buffer})", this.Context);
try
{
if (this.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
throw new TimeoutException();
if (this.CurrentRequest.IsCancellationRequested)
throw new Exception("Cancellation requested!");
this.ContentProvider?.Write(buffer);
this._response?.ProcessPeekable(this.ContentProvider);
}
catch (Exception e)
{
BufferPool.Release(buffer);
if (this.ShutdownType == ShutdownTypes.Immediate)
return;
FinishedProcessing(e);
}
// After an exception, this._response will be null!
if (this._response != null && this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
FinishedProcessing(null);
}
internal void OnError(string error)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnError({error})", this.Context);
FinishedProcessing(new Exception(error));
}
internal void OnResponse(BufferSegment payload)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnResponse({payload})", this.Context);
this._response?.DownStream?.EmergencyIncreaseMaxBuffered();
OnBuffer(payload);
}
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;
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{nameof(FinishedProcessing)}({resp}, {ex})", this.Context);
// Unset the consumer, we no longer expect another OnContent call until further notice.
this.ContentProvider?.Unbind();
this.ContentProvider?.Dispose();
this.ContentProvider = null;
var req = this.CurrentRequest;
req.OnCancellationRequested -= OnCancellationRequested;
bool resendRequest = false;
HTTPRequestStates requestState = HTTPRequestStates.Finished;
HTTPConnectionStates connectionState = HTTPConnectionStates.Recycle;
Exception error = ex;
if (error != null)
{
// Timeout is a non-retryable error
if (ex is TimeoutException)
{
error = null;
requestState = HTTPRequestStates.TimedOut;
}
else if (ex is HTTPCacheAcquireLockException)
{
error = null;
resendRequest = true;
}
else
{
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries)
{
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.
KeepAliveHeader keepAlive = null;
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref keepAlive, this.Context);
connectionState = HTTPConnectionStates.Recycle;
if (error != null)
requestState = HTTPRequestStates.Error;
else if (!resendRequest && resp.IsUpgraded)
requestState = HTTPRequestStates.Processing;
}
req.Timing.StartNext(TimingEventNames.Queued);
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{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, req);
}
else if (resendRequest && requestState == HTTPRequestStates.Finished)
{
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, connectionState));
}
else
{
// Otherwise set the request's then the connection's state
ConnectionHelper.EnqueueEvents(this, connectionState, req, requestState, error);
}
}
internal void OnDownloadProgress(int down, int total) => RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.CurrentRequest, RequestEvents.DownloadProgress, down, total));
internal void OnUploadProgress(int up, int total) => RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.CurrentRequest, RequestEvents.UploadProgress, up, total));
internal void OnTimeout()
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnTimeout", this.Context);
CurrentRequest.Response = null;
CurrentRequest.State = HTTPRequestStates.TimedOut;
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
}
internal void OnAborted()
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnAborted", this.Context);
CurrentRequest.Response = null;
CurrentRequest.State = HTTPRequestStates.Aborted;
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
WebGLXHRNativeConnectionLayer.Remove(NativeId);
WebGLXHRNativeInterface.XHR_Release(NativeId);
this.ContentProvider?.Dispose();
this.ContentProvider = null;
}
}
#endregion
}
}
#endif