This commit is contained in:
2026-06-15 18:18:16 +08:00
parent 97c9fba14e
commit 2b9f134e5f
4164 changed files with 386922 additions and 79 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 82bd7454309118f4e9fa14dd5afaae6d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5db467dd3130b1349ab57a28f34db155
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,259 @@
using System;
using System.Linq;
using System.ComponentModel;
using System.Collections.Generic;
using System.Collections;
using specialized = PlatformSupport.Collections.Specialized;
namespace PlatformSupport.Collections.ObjectModel
{
public class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, specialized.INotifyCollectionChanged, INotifyPropertyChanged
{
private const string CountString = "Count";
private const string IndexerName = "Item[]";
private const string KeysName = "Keys";
private const string ValuesName = "Values";
private IDictionary<TKey, TValue> _Dictionary;
protected IDictionary<TKey, TValue> Dictionary
{
get { return _Dictionary; }
}
#region Constructors
public ObservableDictionary()
{
_Dictionary = new Dictionary<TKey, TValue>();
}
public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
{
_Dictionary = new Dictionary<TKey, TValue>(dictionary);
}
public ObservableDictionary(IEqualityComparer<TKey> comparer)
{
_Dictionary = new Dictionary<TKey, TValue>(comparer);
}
public ObservableDictionary(int capacity)
{
_Dictionary = new Dictionary<TKey, TValue>(capacity);
}
public ObservableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
{
_Dictionary = new Dictionary<TKey, TValue>(dictionary, comparer);
}
public ObservableDictionary(int capacity, IEqualityComparer<TKey> comparer)
{
_Dictionary = new Dictionary<TKey, TValue>(capacity, comparer);
}
#endregion
#region IDictionary<TKey,TValue> Members
public void Add(TKey key, TValue value)
{
Insert(key, value, true);
}
public bool ContainsKey(TKey key)
{
return Dictionary.ContainsKey(key);
}
public ICollection<TKey> Keys
{
get { return Dictionary.Keys; }
}
public bool Remove(TKey key)
{
if (key == null) throw new ArgumentNullException("key");
TValue value;
Dictionary.TryGetValue(key, out value);
var removed = Dictionary.Remove(key);
if (removed)
//OnCollectionChanged(NotifyCollectionChangedAction.Remove, new KeyValuePair<TKey, TValue>(key, value));
OnCollectionChanged();
return removed;
}
public bool TryGetValue(TKey key, out TValue value)
{
return Dictionary.TryGetValue(key, out value);
}
public ICollection<TValue> Values
{
get { return Dictionary.Values; }
}
public TValue this[TKey key]
{
get
{
return Dictionary[key];
}
set
{
Insert(key, value, false);
}
}
#endregion
#region ICollection<KeyValuePair<TKey,TValue>> Members
public void Add(KeyValuePair<TKey, TValue> item)
{
Insert(item.Key, item.Value, true);
}
public void Clear()
{
if (Dictionary.Count > 0)
{
Dictionary.Clear();
OnCollectionChanged();
}
}
public bool Contains(KeyValuePair<TKey, TValue> item)
{
return Dictionary.Contains(item);
}
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
Dictionary.CopyTo(array, arrayIndex);
}
public int Count
{
get { return Dictionary.Count; }
}
public bool IsReadOnly
{
get { return Dictionary.IsReadOnly; }
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
return Remove(item.Key);
}
#endregion
#region IEnumerable<KeyValuePair<TKey,TValue>> Members
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return Dictionary.GetEnumerator();
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)Dictionary).GetEnumerator();
}
#endregion
#region INotifyCollectionChanged Members
public event specialized.NotifyCollectionChangedEventHandler CollectionChanged;
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
public void AddRange(IDictionary<TKey, TValue> items)
{
if (items == null) throw new ArgumentNullException("items");
if (items.Count > 0)
{
if (Dictionary.Count > 0)
{
if (items.Keys.Any((k) => Dictionary.ContainsKey(k)))
throw new ArgumentException("An item with the same key has already been added.");
else
foreach (var item in items) Dictionary.Add(item);
}
else
_Dictionary = new Dictionary<TKey, TValue>(items);
OnCollectionChanged(specialized.NotifyCollectionChangedAction.Add, items.ToArray());
}
}
private void Insert(TKey key, TValue value, bool add)
{
if (key == null) throw new ArgumentNullException("key");
TValue item;
if (Dictionary.TryGetValue(key, out item))
{
if (add) throw new ArgumentException("An item with the same key has already been added.");
if (Equals(item, value)) return;
Dictionary[key] = value;
OnCollectionChanged(specialized.NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, item));
}
else
{
Dictionary[key] = value;
OnCollectionChanged(specialized.NotifyCollectionChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value));
}
}
private void OnPropertyChanged()
{
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnPropertyChanged(KeysName);
OnPropertyChanged(ValuesName);
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private void OnCollectionChanged()
{
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new specialized.NotifyCollectionChangedEventArgs(specialized.NotifyCollectionChangedAction.Reset));
}
private void OnCollectionChanged(specialized.NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> changedItem)
{
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new specialized.NotifyCollectionChangedEventArgs(action, changedItem));
}
private void OnCollectionChanged(specialized.NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem)
{
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new specialized.NotifyCollectionChangedEventArgs(action, newItem, oldItem));
}
private void OnCollectionChanged(specialized.NotifyCollectionChangedAction action, IList newItems)
{
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new specialized.NotifyCollectionChangedEventArgs(action, newItems));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 902ef36b7f3ace94a83e085968c22e44
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0bded87d86b03f344a35f9b83d8be3b0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,464 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections;
using System.Diagnostics;
namespace PlatformSupport.Collections.Specialized
{
public delegate void NotifyCollectionChangedEventHandler(object sender, PlatformSupport.Collections.Specialized.NotifyCollectionChangedEventArgs e);
public interface INotifyCollectionChanged
{
event NotifyCollectionChangedEventHandler CollectionChanged;
}
/// <summary>
/// This enum describes the action that caused a CollectionChanged event.
/// </summary>
public enum NotifyCollectionChangedAction
{
/// <summary> One or more items were added to the collection. </summary>
Add,
/// <summary> One or more items were removed from the collection. </summary>
Remove,
/// <summary> One or more items were replaced in the collection. </summary>
Replace,
/// <summary> One or more items were moved within the collection. </summary>
Move,
/// <summary> The contents of the collection changed dramatically. </summary>
Reset,
}
/// <summary>
/// Arguments for the CollectionChanged event.
/// A collection that supports INotifyCollectionChangedThis raises this event
/// whenever an item is added or removed, or when the contents of the collection
/// changes dramatically.
/// </summary>
public class NotifyCollectionChangedEventArgs : EventArgs
{
//------------------------------------------------------
//
// Constructors
//
//------------------------------------------------------
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a reset change.
/// </summary>
/// <param name="action">The action that caused the event (must be Reset).</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item change.
/// </summary>
/// <param name="action">The action that caused the event; can only be Reset, Add or Remove action.</param>
/// <param name="changedItem">The item affected by the change.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object changedItem)
{
if ((action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add) && (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
&& (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset))
throw new ArgumentException("action");
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
{
if (changedItem != null)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
else
{
InitializeAddOrRemove(action, new object[] { changedItem }, -1);
}
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item change.
/// </summary>
/// <param name="action">The action that caused the event.</param>
/// <param name="changedItem">The item affected by the change.</param>
/// <param name="index">The index where the change occurred.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object changedItem, int index)
{
if ((action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add) && (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
&& (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset))
throw new ArgumentException("action");
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
{
if (changedItem != null)
throw new ArgumentException("action");
if (index != -1)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
else
{
InitializeAddOrRemove(action, new object[] { changedItem }, index);
}
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item change.
/// </summary>
/// <param name="action">The action that caused the event.</param>
/// <param name="changedItems">The items affected by the change.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList changedItems)
{
if ((action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add) && (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
&& (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset))
throw new ArgumentException("action");
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
{
if (changedItems != null)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
else
{
if (changedItems == null)
throw new ArgumentNullException("changedItems");
InitializeAddOrRemove(action, changedItems, -1);
}
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item change (or a reset).
/// </summary>
/// <param name="action">The action that caused the event.</param>
/// <param name="changedItems">The items affected by the change.</param>
/// <param name="startingIndex">The index where the change occurred.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList changedItems, int startingIndex)
{
if ((action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add) && (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
&& (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset))
throw new ArgumentException("action");
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Reset)
{
if (changedItems != null)
throw new ArgumentException("action");
if (startingIndex != -1)
throw new ArgumentException("action");
InitializeAdd(action, null, -1);
}
else
{
if (changedItems == null)
throw new ArgumentNullException("changedItems");
if (startingIndex < -1)
throw new ArgumentException("startingIndex");
InitializeAddOrRemove(action, changedItems, startingIndex);
}
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item Replace event.
/// </summary>
/// <param name="action">Can only be a Replace action.</param>
/// <param name="newItem">The new item replacing the original item.</param>
/// <param name="oldItem">The original item that is replaced.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object newItem, object oldItem)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Replace)
throw new ArgumentException("action");
InitializeMoveOrReplace(action, new object[] { newItem }, new object[] { oldItem }, -1, -1);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item Replace event.
/// </summary>
/// <param name="action">Can only be a Replace action.</param>
/// <param name="newItem">The new item replacing the original item.</param>
/// <param name="oldItem">The original item that is replaced.</param>
/// <param name="index">The index of the item being replaced.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object newItem, object oldItem, int index)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Replace)
throw new ArgumentException("action");
InitializeMoveOrReplace(action, new object[] { newItem }, new object[] { oldItem }, index, index);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item Replace event.
/// </summary>
/// <param name="action">Can only be a Replace action.</param>
/// <param name="newItems">The new items replacing the original items.</param>
/// <param name="oldItems">The original items that are replaced.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, IList oldItems)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Replace)
throw new ArgumentException("action");
if (newItems == null)
throw new ArgumentNullException("newItems");
if (oldItems == null)
throw new ArgumentNullException("oldItems");
InitializeMoveOrReplace(action, newItems, oldItems, -1, -1);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item Replace event.
/// </summary>
/// <param name="action">Can only be a Replace action.</param>
/// <param name="newItems">The new items replacing the original items.</param>
/// <param name="oldItems">The original items that are replaced.</param>
/// <param name="startingIndex">The starting index of the items being replaced.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, IList oldItems, int startingIndex)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Replace)
throw new ArgumentException("action");
if (newItems == null)
throw new ArgumentNullException("newItems");
if (oldItems == null)
throw new ArgumentNullException("oldItems");
InitializeMoveOrReplace(action, newItems, oldItems, startingIndex, startingIndex);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a one-item Move event.
/// </summary>
/// <param name="action">Can only be a Move action.</param>
/// <param name="changedItem">The item affected by the change.</param>
/// <param name="index">The new index for the changed item.</param>
/// <param name="oldIndex">The old index for the changed item.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, object changedItem, int index, int oldIndex)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Move)
throw new ArgumentException("action");
if (index < 0)
throw new ArgumentException("index");
object[] changedItems = new object[] { changedItem };
InitializeMoveOrReplace(action, changedItems, changedItems, index, oldIndex);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs that describes a multi-item Move event.
/// </summary>
/// <param name="action">The action that caused the event.</param>
/// <param name="changedItems">The items affected by the change.</param>
/// <param name="index">The new index for the changed items.</param>
/// <param name="oldIndex">The old index for the changed items.</param>
public NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList changedItems, int index, int oldIndex)
{
if (action != PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Move)
throw new ArgumentException("action");
if (index < 0)
throw new ArgumentException("index");
InitializeMoveOrReplace(action, changedItems, changedItems, index, oldIndex);
}
/// <summary>
/// Construct a NotifyCollectionChangedEventArgs with given fields (no validation). Used by WinRT marshaling.
/// </summary>
internal NotifyCollectionChangedEventArgs(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, IList oldItems, int newIndex, int oldIndex)
{
_action = action;
_newItems = (newItems == null) ? null : new ReadOnlyList(newItems);
_oldItems = (oldItems == null) ? null : new ReadOnlyList(oldItems);
_newStartingIndex = newIndex;
_oldStartingIndex = oldIndex;
}
private void InitializeAddOrRemove(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList changedItems, int startingIndex)
{
if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Add)
InitializeAdd(action, changedItems, startingIndex);
else if (action == PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction.Remove)
InitializeRemove(action, changedItems, startingIndex);
else
Debug.Assert(false, String.Format("Unsupported action: {0}", action.ToString()));
}
private void InitializeAdd(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, int newStartingIndex)
{
_action = action;
_newItems = (newItems == null) ? null : new ReadOnlyList(newItems);
_newStartingIndex = newStartingIndex;
}
private void InitializeRemove(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList oldItems, int oldStartingIndex)
{
_action = action;
_oldItems = (oldItems == null) ? null : new ReadOnlyList(oldItems);
_oldStartingIndex = oldStartingIndex;
}
private void InitializeMoveOrReplace(PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction action, IList newItems, IList oldItems, int startingIndex, int oldStartingIndex)
{
InitializeAdd(action, newItems, startingIndex);
InitializeRemove(action, oldItems, oldStartingIndex);
}
//------------------------------------------------------
//
// Public Properties
//
//------------------------------------------------------
/// <summary>
/// The action that caused the event.
/// </summary>
public PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction Action
{
get { return _action; }
}
/// <summary>
/// The items affected by the change.
/// </summary>
public IList NewItems
{
get { return _newItems; }
}
/// <summary>
/// The old items affected by the change (for Replace events).
/// </summary>
public IList OldItems
{
get { return _oldItems; }
}
/// <summary>
/// The index where the change occurred.
/// </summary>
public int NewStartingIndex
{
get { return _newStartingIndex; }
}
/// <summary>
/// The old index where the change occurred (for Move events).
/// </summary>
public int OldStartingIndex
{
get { return _oldStartingIndex; }
}
//------------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
private PlatformSupport.Collections.Specialized.NotifyCollectionChangedAction _action;
private IList _newItems, _oldItems;
private int _newStartingIndex = -1;
private int _oldStartingIndex = -1;
}
internal sealed class ReadOnlyList : IList
{
private readonly IList _list;
internal ReadOnlyList(IList list)
{
Debug.Assert(list != null);
_list = list;
}
public int Count
{
get { return _list.Count; }
}
public bool IsReadOnly
{
get { return true; }
}
public bool IsFixedSize
{
get { return true; }
}
public bool IsSynchronized
{
get { return _list.IsSynchronized; }
}
public object this[int index]
{
get
{
return _list[index];
}
set
{
throw new NotSupportedException();
}
}
public object SyncRoot
{
get { return _list.SyncRoot; }
}
public int Add(object value)
{
throw new NotSupportedException();
}
public void Clear()
{
throw new NotSupportedException();
}
public bool Contains(object value)
{
return _list.Contains(value);
}
public void CopyTo(Array array, int index)
{
_list.CopyTo(array, index);
}
public IEnumerator GetEnumerator()
{
return _list.GetEnumerator();
}
public int IndexOf(object value)
{
return _list.IndexOf(value);
}
public void Insert(int index, object value)
{
throw new NotSupportedException();
}
public void Remove(object value)
{
throw new NotSupportedException();
}
public void RemoveAt(int index)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9cb2ba99046317b4b8fb03f76d994f00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fd7ddc2ed89fa8643bc32e214898f855
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,87 @@
#define _ENABLE_LOGGING
using System;
using System.IO;
namespace Best.HTTP.Shared.PlatformSupport.FileSystem
{
public sealed class DefaultIOService : IIOService
{
public Stream CreateFileStream(string path, FileStreamModes mode)
{
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(CreateFileStream)}('{path}', {mode})");
#endif
switch (mode)
{
case FileStreamModes.Create:
return new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
case FileStreamModes.OpenRead:
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read /*, 64 * 1024, FileOptions.SequentialScan*/);
case FileStreamModes.OpenReadWrite:
return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
case FileStreamModes.Append:
return new FileStream(path, FileMode.Append);
}
throw new NotImplementedException($"{nameof(DefaultIOService)}.{nameof(CreateFileStream)} - '{mode}' not implemented!");
}
public void DirectoryCreate(string path)
{
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(DirectoryCreate)}('{path}')");
#endif
Directory.CreateDirectory(path);
}
public void DirectoryDelete(string path) => Directory.Delete(path, true);
public bool DirectoryExists(string path)
{
bool exists = Directory.Exists(path);
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(DirectoryExists)}('{path}', {exists})");
#endif
return exists;
}
public string[] GetFiles(string path)
{
var files = Directory.GetFiles(path);
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(GetFiles)}('{path}', {files?.Length})");
#endif
return files;
}
public void FileDelete(string path)
{
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(FileDelete)}('{path}')");
#endif
File.Delete(path);
}
public bool FileExists(string path)
{
bool exists = File.Exists(path);
#if ENABLE_LOGGING
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DefaultIOService), $"{nameof(FileExists)}('{path}', {exists})");
#endif
return exists;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70d8cd7c804fca64ca518e3bb0210fb0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,74 @@
using System;
using System.IO;
namespace Best.HTTP.Shared.PlatformSupport.FileSystem
{
/// <summary>
/// These are the different modes that the plugin want's to use a filestream.
/// </summary>
public enum FileStreamModes
{
/// <summary>
/// Create a new file.
/// </summary>
Create,
/// <summary>
/// Open an existing file for reading.
/// </summary>
OpenRead,
/// <summary>
/// Open or create a file for read and write.
/// </summary>
OpenReadWrite,
/// <summary>
/// Open an existing file for writing to the end.
/// </summary>
Append
}
/// <summary>
/// Interface for file-system abstraction.
/// </summary>
public interface IIOService
{
/// <summary>
/// Create a directory for the given path.
/// </summary>
void DirectoryCreate(string path);
/// <summary>
/// Return true if the directory exists for the given path.
/// </summary>
bool DirectoryExists(string path);
/// <summary>
/// Delete the directory.
/// </summary>
void DirectoryDelete(string path);
/// <summary>
/// Return with the file names for the given path.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
string[] GetFiles(string path);
/// <summary>
/// Delete the file for the given path.
/// </summary>
void FileDelete(string path);
/// <summary>
/// Return true if the file exists on the given path.
/// </summary>
bool FileExists(string path);
/// <summary>
/// Create a stream that can read and/or write a file on the given path.
/// </summary>
Stream CreateFileStream(string path, FileStreamModes mode);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9e114e4d1a092534796049799a691aa8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 599e163332b86fa4ba9b1f6ba0819c04
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
using System;
namespace Best.HTTP.Shared.PlatformSupport.IL2CPP
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
public class Il2CppEagerStaticClassConstructionAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2fa59793a441f54da418bb0438edbc3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
// https://forum.unity.com/threads/il2cpp-code-generation-options.367074/
using System;
namespace Best.HTTP.Shared.PlatformSupport.IL2CPP
{
/// <summary>
/// The code generation options available for IL to C++ conversion.
/// Enable or disabled these with caution.
/// </summary>
public enum Option
{
/// <summary>
/// Enable or disable code generation for null checks.
///
/// Global null check support is enabled by default when il2cpp.exe
/// is launched from the Unity editor.
///
/// Disabling this will prevent NullReferenceException exceptions from
/// being thrown in generated code. In *most* cases, code that dereferences
/// a null pointer will crash then. Sometimes the point where the crash
/// happens is later than the location where the null reference check would
/// have been emitted though.
/// </summary>
NullChecks = 1,
/// <summary>
/// Enable or disable code generation for array bounds checks.
///
/// Global array bounds check support is enabled by default when il2cpp.exe
/// is launched from the Unity editor.
///
/// Disabling this will prevent IndexOutOfRangeException exceptions from
/// being thrown in generated code. This will allow reading and writing to
/// memory outside of the bounds of an array without any runtime checks.
/// Disable this check with extreme caution.
/// </summary>
ArrayBoundsChecks = 2,
/// <summary>
/// Enable or disable code generation for divide by zero checks.
///
/// Global divide by zero check support is disabled by default when il2cpp.exe
/// is launched from the Unity editor.
///
/// Enabling this will cause DivideByZeroException exceptions to be
/// thrown in generated code. Most code doesn't need to handle this
/// exception, so it is probably safe to leave it disabled.
/// </summary>
DivideByZeroChecks = 3,
}
/// <summary>
/// Use this attribute on a class, method, or property to inform the IL2CPP code conversion utility to override the
/// global setting for one of a few different runtime checks.
///
/// Example:
///
/// [Il2CppSetOption(Option.NullChecks, false)]
/// public static string MethodWithNullChecksDisabled()
/// {
/// var tmp = new Object();
/// return tmp.ToString();
/// }
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class Il2CppSetOptionAttribute : Attribute
{
public Option Option { get; private set; }
public object Value { get; private set; }
public Il2CppSetOptionAttribute(Option option, object value)
{
Option = option;
Value = value;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54d985a2d9f4dd540a34861291c3508e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
using System;
namespace Best.HTTP.Shared.PlatformSupport.IL2CPP
{
/// <summary>
/// https://docs.unity3d.com/Manual/ManagedCodeStripping.html
/// </summary>
[AttributeUsage(
AttributeTargets.Assembly |
AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Delegate |
AttributeTargets.Enum |
AttributeTargets.Event |
AttributeTargets.Field |
AttributeTargets.Interface |
AttributeTargets.Method |
AttributeTargets.Property |
AttributeTargets.Struct)]
public sealed class PreserveAttribute : Attribute {}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 332261a86aa5bba4b823b2d2bf5f8a26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3bab02f6ca42fa64b8cfb7d3e2c2d28b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,103 @@
using System;
using Best.HTTP.Shared.PlatformSupport.Text;
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
public struct AutoReleaseBuffer : IDisposable
{
public static readonly AutoReleaseBuffer Empty = new AutoReleaseBuffer(null);
public byte[] Data;
public int Offset;
public int Count;
public AutoReleaseBuffer(byte[] data)
{
this.Data = data;
this.Offset = 0;
this.Count = data != null ? data.Length : 0;
}
public AutoReleaseBuffer(BufferSegment segment)
{
this.Data = segment.Data;
this.Offset = segment.Offset;
this.Count = segment.Count;
}
public AutoReleaseBuffer(byte[] data, int offset, int count)
{
this.Data = data;
this.Offset = offset;
this.Count = count;
}
public BufferSegment Slice(int newOffset) => new BufferSegment(this.Data, newOffset, this.Count - (newOffset - this.Offset));
public BufferSegment Slice(int offset, int count) => new BufferSegment(this.Data, offset, count);
public override bool Equals(object obj)
{
if (obj == null || !(obj is BufferSegment))
return false;
return Equals((BufferSegment)obj);
}
public bool Equals(BufferSegment other) => this.Data == other.Data && this.Offset == other.Offset && this.Count == other.Count;
public bool Equals(AutoReleaseBuffer other) => this.Data == other.Data && this.Offset == other.Offset && this.Count == other.Count;
public override int GetHashCode() => (this.Data != null ? this.Data.GetHashCode() : 0) * 21 + this.Offset + this.Count;
public static bool operator ==(AutoReleaseBuffer left, AutoReleaseBuffer right) => left.Equals(right);
public static bool operator !=(AutoReleaseBuffer left, AutoReleaseBuffer right) => !left.Equals(right);
public static bool operator ==(AutoReleaseBuffer left, BufferSegment right) => left.Equals(right);
public static bool operator !=(AutoReleaseBuffer left, BufferSegment right) => !left.Equals(right);
public static implicit operator byte[](AutoReleaseBuffer left) => left.Data;
public static implicit operator BufferSegment(AutoReleaseBuffer left) => new BufferSegment(left.Data, left.Offset, left.Count);
public override string ToString()
{
var sb = StringBuilderPool.Get(this.Count + 5);
sb.Append("[AutoReleaseBuffer ");
if (this.Count > 0)
{
sb.AppendFormat("Offset: {0:N0} ", this.Offset);
sb.AppendFormat("Count: {0:N0} ", this.Count);
sb.Append("Data: [");
if (this.Count > 0)
{
int dumpCount = Math.Min(this.Count, BufferSegment.ToStringMaxDumpLength);
sb.AppendFormat("{0:X2}", this.Data[this.Offset]);
for (int i = 1; i < dumpCount; ++i)
sb.AppendFormat(", {0:X2}", this.Data[this.Offset + i]);
if (this.Count > dumpCount)
sb.Append(", ...");
}
sb.Append("]]");
}
else
sb.Append(']');
return StringBuilderPool.ReleaseAndGrab(sb);
}
public void Dispose()
{
if (this.Data != null)
BufferPool.Release(this.Data);
this.Data = null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 510c03648931e35459c6a7b1f09cb549
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
internal struct Bucket
{
#if UNITY_EDITOR
/// <summary>
/// Size the Bucket is associated with. Serves mostly debug purposes.
/// </summary>
public readonly int Size;
#endif
/// <summary>
/// What was Items' minimum Count between two checks.
/// </summary>
public int MinCount;
/// <summary>
/// Direct access to a buffer, without going throug ConcurrentStack's pop/push logic.
/// </summary>
public byte[] FastItem;
private int _count;
private readonly ConcurrentStack<byte[]> _items;
public Bucket(int size)
{
#if UNITY_EDITOR
this.Size = size;
#endif
this.FastItem = null;
this._items = new ConcurrentStack<byte[]>();
this.MinCount = int.MaxValue;
this._count = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryPop(out byte[] item)
{
if (this._items.TryPop(out item))
{
Interlocked.Decrement(ref this._count);
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Push(byte[] item)
{
this._items.Push(item);
Interlocked.Increment(ref this._count);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear()
{
this._items.Clear();
Interlocked.Exchange(ref this._count, 0);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateMinCount(int fastItem)
{
int newMinCount = _count + fastItem;
int oldMinCount = MinCount;
while (newMinCount < oldMinCount && Interlocked.CompareExchange(ref MinCount, newMinCount, oldMinCount) != oldMinCount)
oldMinCount = MinCount;
}
#if UNITY_EDITOR
public override string ToString() => $"[{Size}, {(FastItem != null ? "y" : "n")}, {_count}, {MinCount}]";
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1cd00a5f117ec2c41b301e57bd825200
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,542 @@
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
using Best.HTTP.Shared.Extensions;
#endif
using Best.HTTP.Shared.Logger;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Unity.Burst;
using static Unity.Burst.Intrinsics.X86.Bmi1;
using static System.Math;
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
using System.Linq;
#endif
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
/// <summary>
/// The BufferPool is a foundational element of the Best HTTP package, aiming to reduce dynamic memory allocation overheads by reusing byte arrays. The concept is elegantly simple: rather than allocating and deallocating memory for every requirement, byte arrays can be "borrowed" and "returned" within this pool. Once returned, these arrays are retained for subsequent use, minimizing repetitive memory operations.
/// <para>While the BufferPool is housed within the Best HTTP package, its benefits are not limited to just HTTP operations. All protocols and packages integrated with or built upon the Best HTTP package utilize and benefit from the BufferPool. This ensures that memory is used efficiently and performance remains optimal across all integrated components.</para>
/// </summary>
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppSetOptionAttribute(Best.HTTP.Shared.PlatformSupport.IL2CPP.Option.NullChecks, false)]
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppSetOptionAttribute(Best.HTTP.Shared.PlatformSupport.IL2CPP.Option.ArrayBoundsChecks, false)]
public static class BufferPool
{
/// <summary>
/// Specifies the minimum buffer size that will be allocated. If a request is made for a size smaller than this and canBeLarger is <c>true</c>,
/// this size will be used.
/// </summary>
public const long MIN_BUFFER_SIZE = 512;
/// <summary>
/// Specifies the maximum size of a buffer that the system will consider storing back into the pool.
/// </summary>
public const long MAX_BUFFER_SIZE = 32 * 1024 * 1024;
/// <summary>
/// Represents an empty byte array that can be returned for zero-length requests.
/// </summary>
public static readonly byte[] NoData = Array.Empty<byte>();
/// <summary>
/// Gets or sets a value indicating whether the buffer pooling mechanism is enabled or disabled.
/// Disabling will also clear all stored entries.
/// </summary>
public static bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
// When set to non-enabled remove all stored entries
if (!_isEnabled)
Clear();
}
}
private static volatile bool _isEnabled = true;
/// <summary>
/// Specifies how frequently the maintenance cycle should run to manage old buffers.
/// </summary>
public static TimeSpan RunMaintenanceEvery = TimeSpan.FromSeconds(30);
/// <summary>
/// Specifies the maximum total size of all stored buffers. When the buffer reach this threshold, new releases will be declined.
/// </summary>
public static long MaxPoolSize = 100 * 1024 * 1024;
public static long MinBufferSize => MIN_BUFFER_SIZE;
#if BESTHTTP_ENABLE_BUFFERPOOL_BUFFER_STEALING
/// <summary>
/// Index threshold that getting a larger buffer is allowed in.
/// </summary>
public static byte MaxIndexThreshold = 3;
/// <summary>
/// The maximum size that the pool will try to steal from.
/// </summary>
/// <remarks>It must be power of <c>2</c>!</remarks>
public static uint StealLimit = 1 << 20;
#endif
private static DateTime lastMaintenance = DateTime.MinValue;
// Statistics
private static long PoolSize = 0;
private static long GetBuffers = 0;
private static long ReleaseBuffers = 0;
private static long Borrowed = 0;
private static long ArrayAllocations = 0;
private static readonly Bucket[] _buckets;
private static readonly int firstIdx;
private static readonly int lastIdx;
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
private static ConditionalWeakTable<byte[], Tracker> _trackers = new ConditionalWeakTable<byte[], Tracker>();
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
private static ConcurrentDictionary<byte[], (byte[] buffer, string stackTrace)> _doubleReleaseTracker = new ConcurrentDictionary<byte[], (byte[] buffer, string stackTrace)>();
#endif
static BufferPool()
{
#if UNITY_ANDROID || UNITY_IOS
UnityEngine.Application.lowMemory -= OnLowMemory;
UnityEngine.Application.lowMemory += OnLowMemory;
#endif
firstIdx = GetIdx(BufferPool.MIN_BUFFER_SIZE);
lastIdx = GetIdx(BufferPool.MAX_BUFFER_SIZE);
_buckets = new Bucket[(lastIdx - firstIdx) + 1];
for (int i = 0; i < _buckets.Length; i++)
_buckets[i] = new Bucket((int)(MIN_BUFFER_SIZE << i));
}
#if UNITY_EDITOR
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
public static void ResetSetup()
{
HTTPManager.Logger.Information("BufferPool", "Reset called!");
PoolSize = 0;
GetBuffers = 0;
ReleaseBuffers = 0;
Borrowed = 0;
ArrayAllocations = 0;
for (int i = 0; i < _buckets.Length; i++)
_buckets[i] = new Bucket((int)(MIN_BUFFER_SIZE << i));
lastMaintenance = DateTime.MinValue;
#if UNITY_ANDROID || UNITY_IOS
UnityEngine.Application.lowMemory -= OnLowMemory;
UnityEngine.Application.lowMemory += OnLowMemory;
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
ClearTrackers();
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
_doubleReleaseTracker.Clear();
#endif
}
#endif
#if UNITY_ANDROID || UNITY_IOS
private static void OnLowMemory()
{
HTTPManager.Logger.Warning(nameof(BufferPool), nameof(OnLowMemory));
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
ClearTrackers();
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
_doubleReleaseTracker.Clear();
#endif
// Drop all buffers
Clear();
}
#endif
/// <summary>
/// Fetches a byte array from the pool.
/// </summary>
/// <remarks>Depending on the `canBeLarger` parameter, the returned buffer may be larger than the requested size!</remarks>
/// <param name="size">Requested size of the buffer.</param>
/// <param name="canBeLarger">If <c>true</c>, the returned buffer can be larger than the requested size.</param>
/// <param name="context">Optional context for logging purposes.</param>
/// <returns>A byte array from the pool or a newly allocated one if suitable size is not available.</returns>
public static byte[] Get(long size, bool canBeLarger, LoggingContext context = null)
{
if (!_isEnabled)
return new byte[size];
// Return a fix reference for 0 length requests. Any resize call (even Array.Resize) creates a new reference
// so we are safe to expose it to multiple callers.
if (size == 0)
return BufferPool.NoData;
if (size > BufferPool.MAX_BUFFER_SIZE)
return new byte[size];
if (size < BufferPool.MIN_BUFFER_SIZE)
size = BufferPool.MIN_BUFFER_SIZE;
else if (!BufferPool.IsPowerOf2(size))
size = BufferPool.NextPowerOf2(size);
int idx = GetIdx(size >> firstIdx);
var buckets = _buckets;
#if BESTHTTP_ENABLE_BUFFERPOOL_BUFFER_STEALING
int maxIdx = Max(idx, Min(idx + MaxIndexThreshold, GetIdx(StealLimit >> firstIdx)));
while (idx < buckets.Length && idx <= maxIdx)
#else
if (idx < buckets.Length)
#endif
{
ref Bucket bucket = ref buckets[idx];
var item = bucket.FastItem;
if ((item != null && Interlocked.CompareExchange(ref bucket.FastItem, null, item) == item) ||
bucket.TryPop(out item))
{
Interlocked.Increment(ref GetBuffers);
Interlocked.Add(ref PoolSize, -item.Length);
Interlocked.Add(ref Borrowed, item.Length);
bucket.UpdateMinCount(0);
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
_trackers.Add(item, new Tracker(context));
#endif
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
_doubleReleaseTracker.Remove(item, out var _);
#endif
return item;
}
#if BESTHTTP_ENABLE_BUFFERPOOL_BUFFER_STEALING
idx++;
#endif
}
Interlocked.Increment(ref ArrayAllocations);
Interlocked.Add(ref Borrowed, size);
var result = new byte[size];
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
_trackers.Add(result, new Tracker(context));
#endif
return result;
}
/// <summary>
/// Releases a list of buffer segments back to the pool in a bulk operation.
/// </summary>
/// <param name="segments">List of buffer segments to release.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReleaseBulk(ConcurrentQueue<BufferSegment> segments)
{
if (!_isEnabled || segments == null)
return;
while (segments.TryDequeue(out var segment))
Release(segment);
}
/// <summary>
/// Releases a list of buffer segments back to the pool in a bulk operation.
/// </summary>
/// <param name="segments">List of buffer segments to release.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReleaseBulk(List<BufferSegment> segments)
{
if (!_isEnabled || segments == null)
return;
for (int i = 0; i < segments.Count; i++)
Release(segments[i]);
segments.Clear();
}
/// <summary>
/// Releases a byte array back to the pool.
/// </summary>
/// <param name="buffer">Buffer to be released back to the pool.</param>
public static void Release(byte[] buffer)
{
if (!_isEnabled || buffer == null)
return;
int size = buffer.Length;
if (size == 0 || size < MIN_BUFFER_SIZE || size > MAX_BUFFER_SIZE)
return;
if (!IsPowerOf2(size))
return;
#if BESTHTTP_ENABLE_BUFFERPOOL_DOUBLE_RELEASE_CHECKER
if (_doubleReleaseTracker.TryGetValue(buffer, out var entry))
{
HTTPManager.Logger.Error("BufferPool", $"Buffer already added to the pool! Previously added from: {ProcessStackTrace(entry.stackTrace)}. Buffer: {entry.buffer.AsBuffer()}");
return;
}
else
_doubleReleaseTracker.TryAdd(buffer, new(buffer, Environment.StackTrace));
#endif
Interlocked.Add(ref Borrowed, -size);
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
if (_trackers.TryGetValue(buffer, out var tracker))
{
_trackers.Remove(buffer);
tracker.Dispose();
}
#endif
var ps = Interlocked.Read(ref PoolSize);
if (ps + size > MaxPoolSize)
return;
Interlocked.Add(ref PoolSize, size);
Interlocked.Increment(ref ReleaseBuffers);
int idx = GetIdx(size >> firstIdx);
var buckets = _buckets;
ref var bucket = ref buckets[idx];
if (Interlocked.CompareExchange(ref bucket.FastItem, buffer, null) != null)
{
bucket.Push(buffer);
bucket.UpdateMinCount(0);
}
else
{
bucket.UpdateMinCount(1);
}
}
/// <summary>
/// Resizes a byte array by returning the old one to the pool and fetching (or creating) a new one of the specified size.
/// </summary>
/// <param name="buffer">Buffer to resize.</param>
/// <param name="newSize">New size for the buffer.</param>
/// <param name="canBeLarger">If <c>true</c>, the new buffer can be larger than the specified size.</param>
/// <param name="clear">If <c>true</c>, the new buffer will be cleared (set to all zeros).</param>
/// <param name="context">Optional context for logging purposes.</param>
/// <returns>A resized buffer.</returns>
public static byte[] Resize(ref byte[] buffer, int newSize, bool canBeLarger, bool clear, LoggingContext context = null)
{
if (!_isEnabled)
{
Array.Resize<byte>(ref buffer, newSize);
return buffer;
}
byte[] newBuf = BufferPool.Get(newSize, canBeLarger, context);
if (buffer != null)
{
if (!clear)
Array.Copy(buffer, 0, newBuf, 0, Min(newBuf.Length, buffer.Length));
BufferPool.Release(buffer);
}
if (clear)
Array.Clear(newBuf, 0, newSize);
return buffer = newBuf;
}
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
public static KeyValuePair<byte[], Tracker>[] GetBorrowedBuffers() => _trackers.ToArray();
#endif
#if BESTHTTP_PROFILE
public static void GetStatistics(ref BufferPoolStats stats)
{
//using (new ReadLock(rwLock))
stats.GetBuffers = Interlocked.Read(ref GetBuffers);
stats.ReleaseBuffers = Interlocked.Read(ref ReleaseBuffers);
stats.PoolSize = Interlocked.Read(ref PoolSize);
stats.MinBufferSize = MIN_BUFFER_SIZE;
stats.MaxBufferSize = MAX_BUFFER_SIZE;
stats.MaxPoolSize = Interlocked.Read(ref MaxPoolSize);
stats.Borrowed = Interlocked.Read(ref Borrowed);
stats.ArrayAllocations = Interlocked.Read(ref ArrayAllocations);
/*stats.FreeBufferCount = FreeBuffers.Count;
if (stats.FreeBufferStats == null)
stats.FreeBufferStats = new List<BufferStats>(FreeBuffers.Count);
else
stats.FreeBufferStats.Clear();
for (int i = 0; i < FreeBuffers.Count; ++i)
{
BufferStore store = FreeBuffers[i];
List<BufferDesc> buffers = store.buffers;
BufferStats bufferStats = new BufferStats();
bufferStats.Size = store.Size;
bufferStats.Count = buffers.Count;
stats.FreeBufferStats.Add(bufferStats);
}*/
stats.NextMaintenance = (lastMaintenance + RunMaintenanceEvery) - DateTime.UtcNow;
}
#endif
/// <summary>
/// Clears all stored entries in the buffer pool instantly, releasing memory.
/// </summary>
public static void Clear()
{
var buckets = _buckets;
for (int i = 0; i < _buckets.Length; ++i)
{
ref Bucket bucket = ref buckets[i];
bucket.Clear();
Interlocked.Exchange(ref bucket.FastItem, null);
}
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
ClearTrackers();
#endif
Interlocked.Exchange(ref PoolSize, 0);
}
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
private static void ClearTrackers()
{
var all = _trackers.ToArray();
foreach (var item in all)
_trackers.Remove(item.Key);
_trackers.Clear();
}
#endif
/// <summary>
/// Internal method called by the plugin to remove old, non-used buffers.
/// </summary>
internal static void Maintain()
{
DateTime now = DateTime.UtcNow;
if (!_isEnabled || lastMaintenance + RunMaintenanceEvery > now)
return;
lastMaintenance = now;
var buckets = _buckets;
for (int i = 0; i < buckets.Length; ++i)
{
ref Bucket bucket = ref buckets[i];
// Size of the bucket, already negated.
int bucketSize = -(int)(MIN_BUFFER_SIZE << i);
var remove = Interlocked.Exchange(ref bucket.MinCount, int.MaxValue);
// Remove half of the unused items, releasing them back to the GC in a few steps
if (remove != int.MaxValue && remove > 1)
remove >>= 1;
for (int counter = 0; counter < remove && bucket.TryPop(out var _); ++counter)
Interlocked.Add(ref PoolSize, bucketSize);
// Remove FastItem too, when it wasn't used in a full maintenance round
if (remove == int.MaxValue && Interlocked.Exchange(ref bucket.FastItem, null) != null)
Interlocked.Add(ref PoolSize, bucketSize);
}
}
#region Helper functions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPowerOf2(long x) => (x & (x - 1)) == 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long NextPowerOf2(long x)
{
long pow = 1;
while (pow <= x)
pow *= 2;
return pow;
}
[ThreadStatic]
private static System.Text.StringBuilder stacktraceBuilder;
public static string ProcessStackTrace(string stackTrace)
{
if (string.IsNullOrEmpty(stackTrace))
return string.Empty;
stacktraceBuilder ??= new System.Text.StringBuilder();
var lines = stackTrace.Split('\n');
for (int i = 0; i < lines.Length; ++i)
{
var line = lines[i];
if (!line.Contains("Shared.PlatformSupport.Memory.Tracker..ctor") &&
!line.Contains(".Memory.BufferPool") &&
!line.Contains("Environment") &&
!line.Contains("System.Threading"))
stacktraceBuilder.Append(line.Replace("Best.HTTP.", ""));
}
return stacktraceBuilder.ToString();
}
[BurstCompile]
private static int GetIdx(long po2)
{
if (IsBmi1Supported)
return (int)tzcnt_u64((ulong)(po2 >> firstIdx));
/*else if (IsNeonSupported)
{
v64 v = new v64((uint)po2, (uint)po2);
var result = vclz_s32(v);
return (int)result.UInt0;
}*/
ulong a = (ulong)po2;
ulong c = 64;
a &= (ulong)-(long)(a);
if (a != 0) c--;
if ((a & 0x00000000FFFFFFFF) != 0) c -= 32;
if ((a & 0x0000FFFF0000FFFF) != 0) c -= 16;
if ((a & 0x00FF00FF00FF00FF) != 0) c -= 8;
if ((a & 0x0F0F0F0F0F0F0F0F) != 0) c -= 4;
if ((a & 0x3333333333333333) != 0) c -= 2;
if ((a & 0x5555555555555555) != 0) c -= 1;
return (int)c;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d2975ff23d129949bbef756327c52fc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
#if BESTHTTP_PROFILE
using System;
using System.Collections.Generic;
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
public struct BufferStats
{
public long Size;
public int Count;
}
public struct BufferPoolStats
{
public long GetBuffers;
public long ReleaseBuffers;
public long PoolSize;
public long MaxPoolSize;
public long MinBufferSize;
public long MaxBufferSize;
public long Borrowed;
public long ArrayAllocations;
public int FreeBufferCount;
public List<BufferStats> FreeBufferStats;
public TimeSpan NextMaintenance;
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a63d88aa7b47f049b4f066dc154285a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,130 @@
using Best.HTTP.Shared.PlatformSupport.Text;
using System;
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
/// <summary>
/// Represents a segment (a continuous section) of a byte array, providing functionalities to
/// work with a portion of the data without copying.
/// </summary>
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
public readonly struct BufferSegment
{
internal const int ToStringMaxDumpLength = 128;
/// <summary>
/// Represents an empty buffer segment.
/// </summary>
public static readonly BufferSegment Empty = new BufferSegment(null, 0, 0);
/// <summary>
/// The underlying data of the buffer segment.
/// </summary>
public readonly byte[] Data;
/// <summary>
/// The starting offset of the segment within the data.
/// </summary>
public readonly int Offset;
/// <summary>
/// The number of bytes in the segment that contain valid data.
/// </summary>
public readonly int Count;
/// <summary>
/// Initializes a new instance of the BufferSegment struct.
/// </summary>
/// <param name="data">The data for the buffer segment.</param>
/// <param name="offset">The starting offset of the segment.</param>
/// <param name="count">The number of bytes in the segment.</param>
public BufferSegment(byte[] data, int offset, int count)
{
this.Data = data;
this.Offset = offset;
this.Count = count;
}
/// <summary>
/// Converts the buffer segment to an AutoReleaseBuffer to use it in a local using statement.
/// </summary>
/// <returns>A new AutoReleaseBuffer instance containing the data of the buffer segment.</returns>
public AutoReleaseBuffer AsAutoRelease() => new AutoReleaseBuffer(this);
/// <summary>
/// Creates a new segment starting from the specified offset.
/// </summary>
/// <remarks>The new segment will reference the same underlying byte[] as the original, without creating a copy of the data.</remarks>
/// <param name="newOffset">The starting offset of the new segment.</param>
/// <returns>A new buffer segment that references the same underlying data.</returns>
public BufferSegment Slice(int newOffset) => new BufferSegment(this.Data, newOffset, this.Count - (newOffset - this.Offset));
/// <summary>
/// Creates a new segment with the specified offset and count.
/// </summary>
/// <remarks>The new segment will reference the same underlying byte[] as the original, without creating a copy of the data.</remarks>
/// <param name="offset">The starting offset of the new segment.</param>
/// <param name="count">The number of bytes in the new segment.</param>
/// <returns>A new buffer segment that references the same underlying data.</returns>
public BufferSegment Slice(int offset, int count) => new BufferSegment(this.Data, offset, count);
/// <summary>
/// Copyies the buffer's content to the received array.
/// </summary>
/// <param name="to">The array the data will be copied into.</param>
public void CopyTo(byte[] to) => Array.Copy(this.Data, this.Offset, to, 0, this.Count);
public Span<byte> AsSpan() => new Span<byte>(this.Data, this.Offset, this.Count);
public override bool Equals(object obj)
{
if (obj == null || !(obj is BufferSegment))
return false;
return Equals((BufferSegment)obj);
}
public bool Equals(BufferSegment other) => this.Data == other.Data && this.Offset == other.Offset && this.Count == other.Count;
public bool Equals(AutoReleaseBuffer other) => this.Data == other.Data && this.Offset == other.Offset && this.Count == other.Count;
public override int GetHashCode() => (this.Data != null ? this.Data.GetHashCode() : 0) * 21 + this.Offset + this.Count;
public static bool operator ==(BufferSegment left, BufferSegment right) => left.Equals(right);
public static bool operator !=(BufferSegment left, BufferSegment right) => !left.Equals(right);
public static bool operator ==(BufferSegment left, AutoReleaseBuffer right) => left.Equals(right);
public static bool operator !=(BufferSegment left, AutoReleaseBuffer right) => !left.Equals(right);
public static implicit operator byte[](BufferSegment left) => left.Data;
public override string ToString()
{
var sb = StringBuilderPool.Get(this.Count + 5);
sb.Append("[BufferSegment ");
if (this.Count > 0)
{
sb.AppendFormat("Offset: {0:N0} ", this.Offset);
sb.AppendFormat("Count: {0:N0} ", this.Count);
sb.Append("Data: [");
if (this.Count > 0)
{
int dumpCount = Math.Min(this.Count, ToStringMaxDumpLength);
sb.AppendFormat("{0:X2}", this.Data[this.Offset]);
for (int i = 1; i < dumpCount; ++i)
sb.AppendFormat(", {0:X2}", this.Data[this.Offset + i]);
if (this.Count > dumpCount)
sb.Append(", ...");
}
sb.Append("]]");
}
else
sb.Append(']');
return StringBuilderPool.ReleaseAndGrab(sb);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c6843dc813cfc342b940e61916005b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cfb0978cefc760443902a234b8369702
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
#if BESTHTTP_ENABLE_BUFFERPOOL_BORROW_CHECKER
using Best.HTTP.Shared.Logger;
using System;
namespace Best.HTTP.Shared.PlatformSupport.Memory
{
public sealed class Tracker : IDisposable
{
public string Stack => this._stack;
public LoggingContext Context => this._context;
private readonly string _stack;
private readonly LoggingContext _context;
private bool _disposed;
public Tracker(LoggingContext context)
{
this._stack = BufferPool.ProcessStackTrace(Environment.StackTrace);
this._context = context;
}
public void Dispose()
{
_disposed = true;
GC.SuppressFinalize(this);
}
~Tracker()
{
if (!_disposed && !Environment.HasShutdownStarted)
HTTPManager.Logger.Error("BufferPool", $"Buffer Leaked! Borrowed at: {_stack}", this._context);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 258c141187c3a464894a9da73c179d50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 739408cecf3da7349b1a0ae731cdaa54
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a7fb60ba1f30cc348a4327d2ef83c4c1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ecb01125967f0e44988bb3d7e893be7f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,519 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using UnityEngine;
using Best.HTTP.Shared.PlatformSupport.Text;
using System.Net;
using System.Net.Sockets;
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
using Best.HTTP.Shared.PlatformSupport.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Network.DNS.Cache
{
/// <summary>
/// Represents the result of a DNS query, including the original host name, resolved IP addresses, and any error.
/// </summary>
public readonly struct DNSQueryResult
{
/// <summary>
/// The host name used in the DNS query.
/// </summary>
public readonly string HostName;
/// <summary>
/// The resolved IP addresses associated with the host name.
/// </summary>
public readonly DNSIPAddress[] Addresses;
/// <summary>
/// Any error that occurred during the DNS query.
/// </summary>
public readonly Exception Error;
internal DNSQueryResult(string hostName, DNSIPAddress[] addresses, Exception error)
{
this.HostName = hostName;
this.Addresses = addresses;
this.Error = error;
}
public override string ToString()
{
if (this.Error != null)
return $"[{nameof(DNSQueryResult)}(\"{HostName}\", {this.Addresses?.Length}, Error: \"{this.Error?.Message}\")]";
else
return $"[{nameof(DNSQueryResult)}(\"{HostName}\", {this.Addresses?.Length})]";
}
}
/// <summary>
/// Represents an IP address obtained from DNS resolution.
/// </summary>
public sealed class DNSIPAddress
{
/// <summary>
/// The resolved IP address.
/// </summary>
public IPAddress IPAddress { get; private set; }
/// <summary>
/// Indicates whether this IP address worked during the last connection attempt.
/// </summary>
public bool IsWorkedLastTime { get; internal set; }
internal DNSIPAddress(IPAddress iPAddress)
{
this.IPAddress = iPAddress;
// By default, assumme it's a working IP address
this.IsWorkedLastTime = true;
}
public override string ToString() => $"[{nameof(DNSIPAddress)}({this.IPAddress}, Working: {this.IsWorkedLastTime})]";
}
/// <summary>
/// Represents options for configuring the DNS cache behavior.
/// </summary>
public sealed class DNSCacheOptions
{
/// <summary>
/// The time interval after which DNS cache entries should be refreshed.
/// </summary>
public TimeSpan RefreshAfter = TimeSpan.FromSeconds(30);
/// <summary>
/// The time interval after which DNS cache entries should be removed if not used.
/// </summary>
public TimeSpan RemoveAfter = TimeSpan.FromSeconds(70);
/// <summary>
/// The granularity of cancellation checks for DNS queries.
/// </summary>
public TimeSpan CancellationCheckGranularity = TimeSpan.FromMilliseconds(100);
/// <summary>
/// The frequency of cache maintenance.
/// </summary>
public TimeSpan MaintenanceFrequency = TimeSpan.FromSeconds(1);
}
/// <summary>
/// Represents parameters for a DNS query, including the host name, address, cancellation token, logging context, callback, and tag.
/// </summary>
public sealed class DNSQueryParameters
{
/// <summary>
/// The hash key associated with the DNS query.
/// </summary>
public Hash128 Key { get; private set; }
/// <summary>
/// The host name used in the DNS query.
/// </summary>
public string Hostname { get => this.Address.Host; }
/// <summary>
/// The URI address used in the DNS query.
/// </summary>
public Uri Address { get; private set; }
/// <summary>
/// The cancellation token used to cancel the DNS query.
/// </summary>
public CancellationToken Token;
/// <summary>
/// Optional logging context.
/// </summary>
public LoggingContext Context;
/// <summary>
/// The callback to be invoked upon completion of the DNS query.
/// </summary>
public Action<DNSQueryParameters, DNSQueryResult> Callback;
/// <summary>
/// An optional object reference associated with the DNS query.
/// </summary>
public object Tag;
/// <summary>
/// Indicates whether the DNS query is a prefetch query.
/// </summary>
public bool IsPrefetch { get => this.Context == null; }
public DNSQueryParameters(Uri address)
{
this.Address = address;
this.Key = Hash128.Compute(this.Hostname);
}
public override string ToString()
{
return $"{Key} => {Address}";
}
}
/// <summary>
/// The DNSCache class is a static utility that manages DNS caching and queries within the Best HTTP library.
/// It helps improve network efficiency by caching DNS query results, reducing the need for redundant DNS resolutions.
/// </summary>
/// <remarks>
/// <para>By utilizing the DNSCache class and its associated features, you can optimize DNS resolution in your network communication, leading to improved performance and reduced latency in your applications.</para>
/// <para>
/// Its key features include:
/// <list type="bullet">
/// <item>
/// <term>Improving Network Efficiency</term>
/// <description>The DNSCache class is designed to enhance network efficiency by caching DNS query results.
/// When your application needs to resolve hostnames to IP addresses for making network requests, the DNSCache stores previously resolved results.
/// This reduces the need for redundant DNS resolutions, making network communication faster and more efficient.
/// </description>
/// </item>
///
/// <item>
/// <term>DNS Prefetching</term>
/// <description>You can use the DNSCache to initiate DNS prefetch operations.
/// Prefetching allows you to resolve and cache DNS records for hostnames in advance, reducing latency for future network requests.
/// This is particularly useful when you expect to make multiple network requests to the same hostnames, as it helps to avoid DNS resolution delays.
/// </description>
/// </item>
///
/// <item>
/// <term>Marking IP Addresses as Non-Working</term>
/// <description>In cases where a previously resolved IP address is determined to be non-functional (e.g., due to network issues), you can use the DNSCache to report IP addresses as non-working.
/// This information helps the cache make better decisions about which IP addresses to use for future network connections. <see cref="Best.HTTP.Shared.PlatformSupport.Network.Tcp.TCPRingmaster"/> gives higher priority for adresses not marked as non-working.
/// </description>
/// </item>
///
/// <item>
/// <term>Clearing the DNS Cache</term>
/// <description>If you need to reset the DNS cache and remove all stored DNS resolutions, you can use the Clear method provided by the DNSCache class.
/// This operation can be useful in scenarios where you want to start with a fresh cache.
/// </description>
/// </item>
///
/// <item>
/// <term>Performing DNS Queries</term>
/// <description>The primary function of the DNSCache class is to perform DNS queries with specified parameters.
/// It resolves DNS records for a given hostname and caches the results. This can be called directly or used internally by the Best HTTP library for resolving hostnames.
/// </description>
/// </item>
///
/// <item>
/// <term>Configuring Cache Behavior</term>
/// <description>You can configure the behavior of the DNS cache using the DNSCacheOptions class.
/// This includes setting refresh intervals for cache entries, defining the granularity of cancellation checks for DNS queries, and specifying the frequency of cache maintenance.
/// </description>
/// </item>
/// </list>
/// </para>
/// </remarks>
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstruction]
public static class DNSCache
{
/// <summary>
/// Options for configuring the DNS cache behavior, including refresh intervals and maintenance frequency.
/// </summary>
public static DNSCacheOptions Options = new DNSCacheOptions();
private static ConcurrentDictionary<Hash128, DNSCacheEntry> _cache = new ConcurrentDictionary<Hash128, DNSCacheEntry>();
private static int _isMaintenanceScheduled = 0;
/// <summary>
/// Initiates a DNS prefetch operation for the specified host name. DNS prefetching is used to resolve and cache
/// DNS records for host names in advance, reducing latency for future network requests.
/// </summary>
/// <param name="hostName">The host name to prefetch.</param>
public static void Prefetch(string hostName)
{
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Prefetch)}(\"{hostName}\")");
Query(new DNSQueryParameters(new Uri($"prefetch://{hostName}")));
}
/// <summary>
/// Reports an IP address as non-working for the specified host name. In cases where a previously resolved IP address
/// is determined to be non-functional, this method updates the cache to mark the IP address as non-working.
/// </summary>
/// <param name="hostName">The host name associated with the IP address.</param>
/// <param name="address">The <see cref="IPAddress"/> to report as non-working.</param>
/// <param name="context">Optional logging context for debugging purposes.</param>
public static void ReportAsNonWorking(string hostName, IPAddress address, LoggingContext context)
{
var key = Hash128.Compute(hostName);
if (_cache.TryGetValue(key, out var entry))
{
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(ReportAsNonWorking)}(\"{hostName}\", {address}) - CacheEntry found", context);
entry.ReportNonWorking(address, context);
}
else
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(ReportAsNonWorking)}(\"{hostName}\", {address}) - CacheEntry not found!", context);
}
/// <summary>
/// Clears the DNS cache, removing all cached DNS records. This operation can be used to reset the cache
/// and remove all stored DNS resolutions.
/// </summary>
public static void Clear()
{
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Clear)}()");
_cache.Clear();
#if BESTHTTP_PROFILE && UNITY_2021_2_OR_NEWER
Profiler.Network.NetworkStats.TotalDNSCacheMissCounter.Value = 0;
Profiler.Network.NetworkStats.TotalDNSCacheHitsCounter.Value = 0;
#endif
}
/// <summary>
/// Performs a DNS query with the specified parameters. It resolves DNS records for a given host name,
/// caching the results to reduce the need for redundant DNS resolutions.
/// </summary>
/// <param name="parameters">The parameters for the DNS query.</param>
public static void Query(DNSQueryParameters parameters)
{
// First check whether it's already an IP address. If so, call the callback without touching any DNS query.
if (IPAddress.TryParse(parameters.Hostname, out var ip) &&
(ip.AddressFamily == AddressFamily.InterNetwork || ip.AddressFamily == AddressFamily.InterNetworkV6))
{
try
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Query)}(\"{parameters.Hostname}\") - It's already an IP address, skipping DNS query...", parameters.Context);
parameters.Callback?.Invoke(parameters, new DNSQueryResult(parameters.Hostname, new DNSIPAddress[] { new DNSIPAddress(ip) }, null));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(DNSCache), $"{nameof(Query)} - Callback", ex, parameters.Context);
}
return;
}
// When context is null, it's a call to refresh cached entry, so it must skip the cache check and have to go straight for the DNS query
if (!parameters.IsPrefetch && _cache.TryGetValue(parameters.Key, out var entry))
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Query)}(\"{parameters.Hostname}\", \"{parameters.Key}\") - Cache hit: {entry}", parameters.Context);
var addresses = entry.GetAddresses();
if (addresses != null && addresses.Length > 0)
{
#if BESTHTTP_PROFILE && UNITY_2021_2_OR_NEWER
Profiler.Network.NetworkStats.TotalDNSCacheHitsCounter.Value++;
#endif
try
{
var result = new DNSQueryResult(parameters.Hostname, addresses, null);
ThreadedRunner.RunShortLiving<DNSQueryParameters, DNSQueryResult>((par, res) => par?.Callback?.Invoke(par, res), parameters, result);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(DNSCache), $"{nameof(Query)}(\"{parameters.Hostname}\", \"{parameters.Key}\") - Cache hit - QueryImpl", ex, parameters.Context);
}
// Return now, if it's a stalled entry, the regular maintenance call will do its job.
return;
}
else if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Query)}(\"{parameters.Hostname}\", \"{parameters.Key}\") - CacheEntry found, but no addresses returned!", parameters.Context);
}
// TODO: Try to combine callbacks for the same query
// - Doing a new query while theres an active query (from prefetch for example) creates two queries. If we could combine callbacks safely, or steal active queries' callback it would be even faster.
// Cases to handle:
// - Thread-safe update of the list of callbacks
// - Query finishing while we try to update it
// - Query is in the middle of dispatching
var ar = Dns.BeginGetHostAddresses(parameters.Hostname, OnGetHostAddresses, /*query*/ parameters);
// Apply a timer only when there's a context == it's in the context of a request where it can be cancelled.
// If it's a refresh/prefech request, it would be a waste of resources.
if (!parameters.IsPrefetch)
{
#if BESTHTTP_PROFILE && UNITY_2021_2_OR_NEWER
Profiler.Network.NetworkStats.TotalDNSCacheMissCounter.Value++;
#endif
if (!ar.CompletedSynchronously && parameters.Token != CancellationToken.None)
Extensions.Timer.Add(new TimerData(Options.CancellationCheckGranularity, /*query*/ parameters, CheckForCanceled));
}
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Query)}(\"{parameters.Hostname}\", \"{parameters.Key}\") - Cache miss/Prefetch! {nameof(Dns.BeginGetHostAddresses)} called!", parameters.Context);
}
private static void OnGetHostAddresses(IAsyncResult ar)
{
var parameters = ar.AsyncState as /*DNSQuery*/DNSQueryParameters;
var callback = parameters.Callback;
callback = Interlocked.CompareExchange<Action<DNSQueryParameters, DNSQueryResult>>(ref parameters.Callback, null, callback);
try
{
// If something went wrong, this will throw an exception.
var addresses = Dns.EndGetHostAddresses(ar);
if (HTTPManager.Logger.IsDiagnostic)
{
var sb = StringBuilderPool.Get(1);
sb.Append('[');
for (int i = 0; i < addresses.Length; ++i)
{
sb.Append(addresses[i]);
if (i < addresses.Length - 1)
sb.Append(", ");
}
sb.Append(']');
var ips = StringBuilderPool.ReleaseAndGrab(sb);
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(OnGetHostAddresses)}({parameters}) - {nameof(Dns.EndGetHostAddresses)} returned with {addresses?.Length} IPs: {ips}", parameters.Context);
}
DNSCacheEntry AddCacheEntry(Hash128 key)
{
var resolved = new List<DNSIPAddress>(addresses.Length);
foreach (var address in addresses)
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
resolved.Add(new DNSIPAddress(address));
var entry = new DNSCacheEntry(key, parameters.Hostname, resolved);
return entry;
}
DNSCacheEntry UpdateCacheEntry(Hash128 key, DNSCacheEntry oldEntry)
{
var resolved = new List<DNSIPAddress>(addresses.Length);
foreach (var address in addresses)
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
resolved.Add(new DNSIPAddress(address));
var entry = oldEntry.DeriveWith(resolved);
return entry;
}
// Store/update cach entry
var entry = _cache.AddOrUpdate(parameters.Key, AddCacheEntry, UpdateCacheEntry);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(OnGetHostAddresses)}({parameters}) - entry added to/updated in the cache: {entry}. Cache size: {_cache.Count}", parameters.Context);
if (entry != null && Interlocked.CompareExchange(ref _isMaintenanceScheduled, 1, 0) == 0)
{
Extensions.Timer.Add(new TimerData(Options.MaintenanceFrequency, null, Maintenance));
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(OnGetHostAddresses)}({parameters}) - Scheduled maintenance rutin", parameters.Context);
}
try
{
callback?.Invoke(parameters, new DNSQueryResult(parameters.Hostname, entry.GetAddresses(), null));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(DNSCache), $"{nameof(OnGetHostAddresses)}({parameters}) - callback", ex, parameters.Context);
}
}
catch (Exception ex)
{
// If there's an error (like DNS couldn't resolve the host) old entries are will remain in the cache.
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(OnGetHostAddresses)}({parameters}) calling callback with no addresses!", parameters.Context);
try
{
callback?.Invoke(parameters, new DNSQueryResult(parameters.Hostname, null, ex));
}
catch (Exception e)
{
HTTPManager.Logger.Exception(nameof(DNSCache), $"{nameof(OnGetHostAddresses)}({parameters}) - callback", e, parameters.Context);
}
}
}
/// <summary>
/// It's plan-b for the case where BeginGetHostAddresses take too long and no reply in time. If the query's Token is canceled it will call the callback if it's still available.
/// </summary>
private static bool CheckForCanceled(DateTime now, object context)
{
var query = context as /*DNSQuery*/ DNSQueryParameters;
if (query.Token.IsCancellationRequested)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(CheckForCanceled)}({query}) - Token.IsCancellationRequested!", query.Context);
var callback = query.Callback;
callback = Interlocked.CompareExchange<Action<DNSQueryParameters, DNSQueryResult>>(ref query.Callback, null, callback);
try
{
callback?.Invoke(query, new DNSQueryResult(query.Hostname, null, new TimeoutException("DNS Query Timed Out")));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(DNSCache), $"{nameof(CheckForCanceled)}({query}) - callback", ex, query.Context);
}
return false;
}
return query.Callback != null;
}
private static bool Maintenance(DateTime now, object context)
{
using var __ = new Unity.Profiling.ProfilerMarker(nameof(DNSCache)).Auto();
foreach (var kvp in _cache)
{
Hash128 key = kvp.Key;
DNSCacheEntry entry = kvp.Value;
if (entry.IsReadyToRemove(now))
{
if (_cache.TryRemove(key, out _))
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Maintenance)}.{nameof(DNSCacheEntry.IsReadyToRemove)}: Removed entry from cache: {entry}");
}
else if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Maintenance)}.{nameof(DNSCacheEntry.IsReadyToRemove)}: Couldn't remove entry from cache: {entry}");
}
else if (entry.IsStalled(now))
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Maintenance)}.{nameof(DNSCacheEntry.IsStalled)}: Refreshing entry: {entry}");
entry.Refresh();
}
}
// return true to repeat. So return false if there's no more entries in the cache and we could change _isMaintenanceScheduled.
bool removeShedule = _cache.Count == 0 && Interlocked.CompareExchange(ref _isMaintenanceScheduled, 0, 1) == 1;
if (removeShedule && HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(DNSCache), $"{nameof(Maintenance)} - Remove scheduled rutin");
return !removeShedule; // Timer expects false to remove the timer, so here we actually have to negate removeSchedule to remove.
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7dffa1d4a664f694faa02c068034ce74
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,192 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Threading;
using Best.HTTP.Shared.PlatformSupport.Text;
using Best.HTTP.Shared.Logger;
using UnityEngine;
namespace Best.HTTP.Shared.PlatformSupport.Network.DNS.Cache
{
/// <summary>
/// Represents a cached entry for DNS query results, including resolved IP addresses and metadata.
/// </summary>
/// <remarks>
/// Almost immutable, all changes are done in-class in a thread-safe manner.
/// </remarks>
internal class DNSCacheEntry
{
/// <summary>
/// Gets the 128-bit hash derived from the host name.
/// </summary>
public readonly Hash128 Key;
/// <summary>
/// Gets the host name this entry stores the IP addresses for.
/// </summary>
public readonly string Host;
/// <summary>
/// Gets the timestamp when the entry was last resolved.
/// </summary>
public readonly DateTime ResolvedAt;
/// <summary>
/// Gets the timestamp when the entry was last used by calling <see cref="GetAddresses()"/>.
/// </summary>
public DateTime LastUsed { get => new DateTime(this._lastUsedTicks); }
private long _lastUsedTicks;
/// <summary>
/// Resolved IP addresses. It's private, accesible through the <see cref="GetAddresses()"/> call only.
/// </summary>
private readonly List<DNSIPAddress> _resolvedAddresses;
/// <summary>
/// Flag that is set to <c>true</c> when the cache is refreshing this host.
/// </summary>
/// <remarks>
/// When set to <c>true</c>, <see cref="IsStalled(DateTime)"/> will always return as non-stalled.
/// </remarks>
private int _isRefreshing;
public DNSCacheEntry(Hash128 key, string host, List<DNSIPAddress> resolvedAddresses)
: this(key, host, DateTime.UtcNow.Ticks, resolvedAddresses) { }
/// <summary>
/// Initializes a new instance of the DNSCacheEntry class.
/// </summary>
/// <param name="key">The 128-bit hash key derived from the host name.</param>
/// <param name="host">The host name associated with this entry.</param>
/// <param name="resolvedAddresses">The list of <see cref="DNSIPAddress"/> containing the resolved IP addresses.</param>
private DNSCacheEntry(Hash128 key, string host, long lastUsedTicks, List<DNSIPAddress> resolvedAddresses)
{
this.Key = key;
this.Host = host;
this.ResolvedAt = DateTime.UtcNow;
this._lastUsedTicks = lastUsedTicks;
this._resolvedAddresses = resolvedAddresses;
}
/// <summary>
/// Called to clone the entry. The new entry will inherit the last used timestamp.
/// </summary>
/// <param name="resolvedAddresses">The list of <see cref="DNSIPAddress"/> containing the resolved IP addresses.</param>
/// <returns>A new DNSCacheEntry instance with updated resolved addresses.</returns>
public DNSCacheEntry DeriveWith(List<DNSIPAddress> resolvedAddresses) => new DNSCacheEntry(this.Key, this.Host, this._lastUsedTicks, resolvedAddresses);
/// <summary>
/// Checks if the entry is stalled and needs to be refreshed.
/// </summary>
/// <param name="now">The current timestamp.</param>
/// <returns><c>true</c> if the entry is stalled; otherwise, <c>false</c>.</returns>
/// <remarks>
/// The entry is considered stalled if it is not currently being refreshed (i.e., <see cref="_isRefreshing"/> is false)
/// and the time since the last resolution exceeds the refresh interval specified in <see cref="DNSCacheOptions.RefreshAfter"/>.
/// </remarks>
public bool IsStalled(DateTime now) => Volatile.Read(ref this._isRefreshing) == 0 && this.ResolvedAt + DNSCache.Options.RefreshAfter < now;
/// <summary>
/// Checks if the entry is ready to be removed from the cache.
/// </summary>
/// <param name="now">The current timestamp.</param>
/// <returns><c>true</c> if the entry is ready for removal; otherwise, <c>false</c>.</returns>
/// <remarks>
/// The entry is considered ready for removal if the time since it was last used exceeds the removal interval specified in <see cref="DNSCacheOptions.RemoveAfter"/>.
/// </remarks>
public bool IsReadyToRemove(DateTime now) => this.LastUsed + DNSCache.Options.RemoveAfter < now;
/// <summary>
/// Refreshes the entry by initiating a DNS prefetch (by calling <see cref="DNSCache.Prefetch(string)"/>) for the associated host name.
/// </summary>
/// <remarks>
/// This method initiates a DNS prefetch operation for the host name associated with this entry.
/// DNS prefetching is used to resolve and cache DNS records for host names in advance, reducing latency for future network requests.
/// </remarks>
public void Refresh()
{
if (Volatile.Read(ref this._isRefreshing) != 0)
return;
// isRefreshing - we set it only to true, when the fresh records are in a new CacheEntry is created with isRefreshing beeing false.
if (Interlocked.CompareExchange(ref this._isRefreshing, 1, 0) == 0)
DNSCache.Prefetch(this.Host);
}
/// <summary>
/// Gets the resolved IP addresses associated with this entry.
/// </summary>
/// <returns>An array of <see cref="DNSIPAddress"/> representing resolved IP addresses.</returns>
/// <remarks>
/// This method returns the resolved IP addresses associated with this entry and updates the last used timestamp.
/// </remarks>
public DNSIPAddress[] GetAddresses()
{
Interlocked.Exchange(ref this._lastUsedTicks, DateTime.UtcNow.Ticks);
return this._resolvedAddresses.ToArray();
}
/// <summary>
/// Reports an IP address as non-working for the specified host name. In cases where a previously resolved IP address
/// is determined to be non-functional, this method updates the cache to mark the IP address as non-working.
/// </summary>
/// <param name="nonWorking">The non-working IP address to report.</param>
/// <param name="context">Optional logging context for debugging purposes.</param>
/// <remarks>
/// This method is used to report an IP address associated with a host name as non-working.
/// When a previously resolved IP address is determined to be non-functional, this method updates the cache to mark the IP address as non-working.
/// It can be useful in situations where network errors or issues with specific IP addresses need to be recorded and managed.
/// </remarks>
public void ReportNonWorking(System.Net.IPAddress nonWorking, LoggingContext context)
{
var address = this._resolvedAddresses.Find(adr => adr.IPAddress == nonWorking);
if (address != null)
{
// Because the TCP ringmaster not just probes an address, but a port of on that address, setting IsWorkedLastTime to false here
// means, that the address will be tried last even for a different port too.
address.IsWorkedLastTime = false;
}
else
{
// It could happen if a refresh query is started while a tcp race probing the previous addresses and when the refresh finished with different addresses the
// tcp race will try report non-working and now non-existing addresses.
//HTTPManager.Logger.Warning(nameof(DNSCacheEntry), $"{nameof(ReportNonWorking)}({nonWorking}) - couldn't find IP address in resolved addresses!", context);
}
}
public override string ToString()
{
var sb = StringBuilderPool.Get(1);
sb.Append('[');
sb.Append(nameof(DNSCacheEntry));
sb.Append(" Key: ");
sb.Append(this.Key.ToString());
sb.Append(" ResolvedAt: ");
sb.Append(this.ResolvedAt.ToString());
sb.Append(" LastUsed: ");
sb.Append(this.LastUsed.ToString());
sb.Append(" IsRefreshing: ");
sb.Append(this._isRefreshing.ToString());
sb.Append(" IsStalled: ");
sb.Append(this.IsStalled(DateTime.UtcNow));
sb.Append(" IsReadyToRemove: ");
sb.Append(this.IsReadyToRemove(DateTime.UtcNow));
sb.Append(" Resolved: [");
for (int i = 0; i < this._resolvedAddresses?.Count; ++i)
{
sb.Append(this._resolvedAddresses[i].ToString());
if (i < this._resolvedAddresses.Count - 1)
sb.Append(", ");
}
sb.Append(']');
return StringBuilderPool.ReleaseAndGrab(sb);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bf38c03c8439341498f03965f5313f67
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b4965c6a5019e2408513706975c29c9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,114 @@
using Best.HTTP.Shared.Streams;
using System;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp
{
/// <summary>
/// The IPeekableContentProvider interface defines an abstraction for providing content to an <see cref="IContentConsumer"/> with the ability to peek at the content without consuming it.
/// It is an essential part of content streaming over a TCP connection.
/// </summary>
/// <remarks>
/// <para>
/// Key Functions of IPeekableContentProvider:
/// </para>
/// <list type="bullet">
/// <item>
/// <term>Content Provision</term><description>It provides content to an associated <see cref="IContentConsumer"/> without immediately consuming the content. This allows the consumer to examine the data before processing.
/// </description></item>
/// <item>
/// <term>Two-Way Binding</term><description>Supports establishing a two-way binding between the <see cref="IPeekableContentProvider"/> and an <see cref="IContentConsumer"/>, enabling bidirectional communication between the provider and consumer.
/// </description></item>
/// <item>
/// <term>Unbinding</term><description>Provides methods for unbinding a content consumer, terminating the association between the provider and consumer.
/// </description></item>
/// </list>
/// </remarks>
public interface IPeekableContentProvider
{
/// <summary>
/// Gets the <see cref="PeekableContentProviderStream"/> associated with this content provider, which allows peeking at the content without consuming it.
/// </summary>
PeekableContentProviderStream Peekable { get; }
/// <summary>
/// Gets the <see cref="IContentConsumer"/> implementor that will be notified through <see cref="IContentConsumer.OnContent"/> calls when new data is available in the TCPStreamer.
/// </summary>
IContentConsumer Consumer { get; }
/// <summary>
/// Sets up a two-way binding between this content provider and an <see cref="IContentConsumer"/>. This enables bidirectional communication between the provider and consumer.
/// </summary>
/// <param name="consumer">The <see cref="IContentConsumer"/> to bind to.</param>
void SetTwoWayBinding(IContentConsumer consumer);
/// <summary>
/// Unbinds the content provider from its associated content consumer. This terminates the association between the provider and consumer.
/// </summary>
void Unbind();
/// <summary>
/// Unbinds the content provider from a specific content consumer if it is currently bound to that consumer.
/// </summary>
/// <param name="consumer">The <see cref="IContentConsumer"/> to unbind from.</param>
void UnbindIf(IContentConsumer consumer);
/// <summary>
/// Unbinds the content provider from a specific content consumer if it is currently bound to that consumer, and changes to the new consumer.
/// </summary>
/// <param name="consumer">The <see cref="IContentConsumer"/> to unbind from.</param>
/// <param name="consumer">The <see cref="IContentConsumer"/> to bin to.</param>
void SwitchIf(IContentConsumer from, IContentConsumer to);
}
/// <summary>
/// The IContentConsumer interface represents a consumer of content provided by an <see cref="IPeekableContentProvider"/>. It defines methods for handling received content and connection-related events.
/// </summary>
/// <remarks>
/// <para>
/// Key Functions of IContentConsumer:
/// </para>
/// <list type="bullet">
/// <item>
/// <term>Content Handling</term><description>Defines methods for handling incoming content, allowing consumers to process data as it becomes available.
/// </description></item>
/// <item>
/// <term>Connection Management</term><description>Provides event methods to notify consumers of connection closure and error conditions, facilitating graceful handling of connection-related issues.
/// </description></item>
/// </list>
/// </remarks>
public interface IContentConsumer
{
/// <summary>
/// Gets the <see cref="PeekableContentProviderStream"/> associated with this content consumer, which allows access to incoming content.
/// </summary>
PeekableContentProviderStream ContentProvider { get; }
/// <summary>
/// This method should not be called directly. It is used internally to set the binding between the content consumer and its associated content provider.
/// </summary>
/// <param name="contentProvider">The <see cref="PeekableContentProviderStream"/> to bind to.</param>
void SetBinding(PeekableContentProviderStream contentProvider);
/// <summary>
/// This method should not be called directly. It is used internally to unset the binding between the content consumer and its associated content provider.
/// </summary>
void UnsetBinding();
/// <summary>
/// Called when new content is available from the associated content provider.
/// </summary>
void OnContent();
/// <summary>
/// Called when the connection is closed by the remote peer. It notifies the content consumer about the connection closure.
/// </summary>
void OnConnectionClosed();
/// <summary>
/// Called when an error occurs during content processing or connection handling. It provides the exception that caused the error.
/// </summary>
/// <param name="ex">The <see cref="Exception"/> that represents the error condition.</param>
void OnError(Exception ex);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa167ead2479e9b4fb2ccef181e854e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,542 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Threading;
#if !BESTHTTP_DISABLE_ALTERNATE_SSL
using Best.HTTP.SecureProtocol.Org.BouncyCastle.Tls;
using Best.HTTP.Shared.TLS;
#endif
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Hosts.Settings;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Network.DNS.Cache;
using Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp
{
/// <summary>
/// Represents the different steps of the negotiation process.
/// </summary>
public enum NegotiationSteps
{
Start,
DNSQuery,
TCPRace,
Proxy,
TLSNegotiation,
Finish
}
/// <summary>
/// Interface for a peer that participates in the negotiation process.
/// </summary>
public interface INegotiationPeer
{
/// <summary>
/// Gets the list of supported ALPN protocol names for negotiation.
/// </summary>
/// <param name="negotiator">The negotiation instance.</param>
/// <returns>A list of supported ALPN protocol names.</returns>
List<string> GetSupportedProtocolNames(Negotiator negotiator);
/// <summary>
/// Indicates whether the negotiation process must stop advancing to the next step.
/// </summary>
/// <param name="negotiator">The negotiation instance.</param>
/// <param name="finishedStep">The step that has just finished.</param>
/// <param name="nextStep">The next step in the negotiation process.</param>
/// <param name="error">An optional error encountered during negotiation.</param>
/// <returns><c>true</c> if negotiation must stop for any reason advancing to the next step; otherwise, <c>false</c>.</returns>
bool MustStopAdvancingToNextStep(Negotiator negotiator, NegotiationSteps finishedStep, NegotiationSteps nextStep, Exception error);
/// <summary>
/// Handles the evaluation of a proxy negotiation failure.
/// </summary>
/// <param name="negotiator">The negotiation instance.</param>
/// <param name="error">The error encountered during proxy negotiation.</param>
/// <param name="resendForAuthentication">Indicates whether to resend for authentication.</param>
void EvaluateProxyNegotiationFailure(Negotiator negotiator, Exception error, bool resendForAuthentication);
/// <summary>
/// Handles the negotiation failure.
/// </summary>
/// <param name="negotiator">The negotiation instance.</param>
/// <param name="error">The error encountered during negotiation.</param>
void OnNegotiationFailed(Negotiator negotiator, Exception error);
/// <summary>
/// Handles the successful completion of negotiation.
/// </summary>
/// <param name="negotiator">The negotiation instance.</param>
/// <param name="stream">The negotiated stream.</param>
/// <param name="streamer">The TCP streamer.</param>
/// <param name="negotiatedProtocol">The negotiated protocol.</param>
void OnNegotiationFinished(Negotiator negotiator, PeekableContentProviderStream stream, TCPStreamer streamer, string negotiatedProtocol);
}
/// <summary>
/// Represents the parameters for a negotiation.
/// </summary>
public sealed class NegotiationParameters
{
/// <summary>
/// Optional proxy instance must be used during negotiation.
/// </summary>
public HTTP.Proxies.Proxy proxy;
/// <summary>
/// Sets a value indicating whether to create a proxy tunnel.
/// </summary>
public bool createProxyTunel;
/// <summary>
/// Sets the target URI for negotiation.
/// </summary>
public Uri targetUri;
/// <summary>
/// Sets a value indicating whether to negotiate TLS.
/// </summary>
public bool negotiateTLS;
/// <summary>
/// Sets the cancellation token for negotiation.
/// </summary>
public CancellationToken token;
/// <summary>
/// Sets the <see cref="HostSettings"/> that can be used during the negotiation process.
/// </summary>
public HostSettings hostSettings;
/// <summary>
/// Optional logging context for debugging purposes.
/// </summary>
public LoggingContext context;
}
/// <summary>
/// <para>The Negotiator class acts as a central coordinator for the negotiation of network connections, abstracting away the complexities of DNS resolution, TCP socket creation, proxy negotiation, and TLS setup.
/// It allows for customization and extensibility, making it a versatile tool for establishing network connections in a flexible and controlled manner.</para>
/// <list type="bullet">
/// <item><description>The Negotiator class represents a component responsible for managing the negotiation process. It helps facilitate communication with various network layers, such as DNS resolution, TCP socket creation, proxy handling, and TLS negotiation.</description></item>
/// <item><description>The class is designed to be flexible and extensible by allowing developers to define a custom negotiation peer that implements the INegotiationPeer interface. This allows developers to adapt the negotiation process to specific requirements and protocols.</description></item>
/// <item><description>It orchestrates the negotiation process through a series of steps defined by the NegotiationSteps enum. These steps include DNSQuery, TCPRace, Proxy, TLSNegotiation</description></item>
/// <item><description>Handles errors and exceptions that may occur during negotiation, ensuring graceful fallback or termination of the negotiation process when necessary.</description></item>
/// <item><description>When TLS negotiation is required, it selects the appropriate TLS negotiation method based on the configuration and available options, such as BouncyCastle or the system's TLS framework.</description></item>
/// <item><description>If a proxy is configured, the Negotiator class handles proxy negotiation and tunneling, allowing communication through a proxy server.</description></item>
/// <item><description>It supports cancellation through a CancellationToken, allowing the negotiation process to be canceled if needed.</description></item>
/// <item><description>The class uses extensive logging to provide information about the progress and outcomes of the negotiation process, making it easier to diagnose and debug issues.</description></item>
/// </list>
/// </summary>
public class Negotiator
{
/// <summary>
/// Gets the negotiation peer associated with this negotiator.
/// </summary>
public INegotiationPeer Peer { get => this._peer; }
private INegotiationPeer _peer;
/// <summary>
/// Gets the negotiation parameters for this negotiator.
/// </summary>
public NegotiationParameters Parameters { get => this._parameters; }
private NegotiationParameters _parameters;
/// <summary>
/// Gets the TCP streamer associated with this negotiator.
/// </summary>
public TCPStreamer Streamer { get => this._streamer; }
private TCPStreamer _streamer;
/// <summary>
/// Gets the peekable content provider stream associated with this negotiator.
/// </summary>
public PeekableContentProviderStream Stream { get => this._stream; }
private PeekableContentProviderStream _stream;
/// <summary>
/// Initializes a new instance of the Negotiator class.
/// </summary>
/// <param name="peer">The negotiation peer for this negotiator.</param>
/// <param name="parameters">The negotiation parameters for this negotiator.</param>
public Negotiator(INegotiationPeer peer, NegotiationParameters parameters)
{
this._peer = peer;
this._parameters = parameters;
}
/// <summary>
/// Starts the negotiation process.
/// </summary>
public void Start()
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(Start)}()", this._parameters.context);
if (!this._peer.MustStopAdvancingToNextStep(this, NegotiationSteps.Start, NegotiationSteps.DNSQuery, null))
{
Uri target = this._parameters.proxy != null && this._parameters.proxy.UseProxyForAddress(this._parameters.targetUri) ? this._parameters.proxy.Address : this._parameters.targetUri;
var parameters = new DNSQueryParameters(target);
parameters.Token = this._parameters.token;
parameters.Context = this._parameters.context;
parameters.Callback = OnDNSCacheQueryFinished;
DNSCache.Query(parameters);
}
}
/// <summary>
/// Handles cancellation requests during negotiation.
/// </summary>
public void OnCancellationRequested()
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(OnCancellationRequested)}()", this._parameters.context);
this._streamer?.Dispose();
}
private void OnDNSCacheQueryFinished(DNSQueryParameters dnsParameters, DNSQueryResult result)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(OnDNSCacheQueryFinished)}({dnsParameters}, {result})", this._parameters.context);
if (!this._peer.MustStopAdvancingToNextStep(this, NegotiationSteps.DNSQuery, NegotiationSteps.TCPRace, result.Error))
{
var tcpParameters = new TCPRaceParameters
{
Addresses = result.Addresses,
Hostname = dnsParameters.Address.Host,
Port = dnsParameters.Address.Port,
Context = this._parameters.context,
Token = this._parameters.token,
AnnounceWinnerCallback = OnTCPRaceFinished,
};
TCPRingmaster.StartCompetion(tcpParameters);
}
}
private void OnTCPRaceFinished(TCPRaceParameters parameters, TCPRaceResult raceResult)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(OnTCPRaceFinished)}({parameters}, {raceResult})", this._parameters.context);
NegotiationSteps nextStep = this._parameters.proxy != null ? NegotiationSteps.Proxy : (this._parameters.negotiateTLS ? NegotiationSteps.TLSNegotiation : NegotiationSteps.Finish);
if (!this._peer.MustStopAdvancingToNextStep(this, NegotiationSteps.TCPRace, nextStep, raceResult.Error))
{
try
{
SetupSocket(raceResult.WinningSocket, this.Parameters.hostSettings);
var lowLevelSettings = this.Parameters.hostSettings.LowLevelConnectionSettings;
this._streamer = new TCPStreamer(raceResult.WinningSocket,
lowLevelSettings.ReadBufferSize,
lowLevelSettings.TCPWriteBufferSize,
this._parameters.context);
//this._streamer._debugRequest = this._parameters.optionalRequest;
var str = new NonblockingTCPStream(this._streamer, false, lowLevelSettings.ReadBufferSize);
str.SetTwoWayBinding(this._peer as IContentConsumer);
this._stream = str;
if (this._parameters.proxy != null)
{
var proxyParameters = new HTTP.Proxies.ProxyConnectParameters()
{
proxy = this._parameters.proxy,
uri = this._parameters.targetUri,
token = this._parameters.token,
stream = this._stream,
context = this._parameters.context,
createTunel = this._parameters.negotiateTLS || this._parameters.createProxyTunel,
//request = this._parameters.optionalRequest,
OnSuccess = OnProxyNegotiated,
OnError = OnProxyNegotiationFailed
};
this._parameters.proxy.BeginConnect(proxyParameters);
}
else
{
NegotiateTLS();
}
}
catch (Exception ex)
{
this._peer.MustStopAdvancingToNextStep(this, NegotiationSteps.TCPRace, NegotiationSteps.Finish, ex);
}
}
}
private void OnProxyNegotiated(HTTP.Proxies.ProxyConnectParameters parameters)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(OnProxyNegotiated)}({parameters})", this._parameters.context);
NegotiationSteps nextStep = this._parameters.negotiateTLS ? NegotiationSteps.TLSNegotiation : NegotiationSteps.Finish;
if (!this._peer.MustStopAdvancingToNextStep(this, NegotiationSteps.Proxy, nextStep, null))
NegotiateTLS();
}
private void OnProxyNegotiationFailed(HTTP.Proxies.ProxyConnectParameters parameters, Exception error, bool resendForAuthentication)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(OnProxyNegotiationFailed)}({parameters}, {error}, {resendForAuthentication})", this._parameters.context);
this._peer.EvaluateProxyNegotiationFailure(this, error, resendForAuthentication);
}
private void NegotiateTLS()
{
try
{
var hostSettings = this.Parameters.hostSettings; //HTTPManager.PerHostSettings.Get(this._parameters.targetUri);
if (this._parameters.negotiateTLS)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(NegotiateTLS)}()", this._parameters.context);
var handlerType = hostSettings.TLSSettings.TLSHandler;
switch (handlerType)
{
#if !BESTHTTP_DISABLE_ALTERNATE_SSL
case TLSHandlers.BouncyCastle:
{
List<ProtocolName> protocols = new List<ProtocolName>();
foreach (var protocol in this._peer.GetSupportedProtocolNames(this))
protocols.Add(ProtocolName.AsUtf8Encoding(protocol));
AbstractTls13Client tlsClient = null;
if (hostSettings.TLSSettings.BouncyCastleSettings.TlsClientFactory == null)
{
tlsClient = BouncyCastleSettings.DefaultTlsClientFactory(this._parameters.targetUri, protocols, this._parameters.context);
}
else
{
try
{
tlsClient = hostSettings.TLSSettings.BouncyCastleSettings.TlsClientFactory(this._parameters.targetUri, protocols, this._parameters.context);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(Negotiator), nameof(hostSettings.TLSSettings.BouncyCastleSettings.TlsClientFactory), ex, this._parameters.context);
}
if (tlsClient == null)
tlsClient = BouncyCastleSettings.DefaultTlsClientFactory(this._parameters.targetUri, protocols, this._parameters.context);
}
var handler = new TlsClientProtocol();
handler.Connect(tlsClient);
var str = new NonblockingBCTLSStream(this._streamer, handler, tlsClient, true, this.Parameters.hostSettings.LowLevelConnectionSettings.ReadBufferSize);
str.OnNegotiated = OnBC_TLSNegotiated;
str.SetTwoWayBinding(this._peer as IContentConsumer);
}
break;
#endif
case TLSHandlers.Framework:
new FrameworkTLSStream(this._streamer, this._parameters.targetUri.Host, this.Parameters.hostSettings)
.OnNegotiated = OnFramework_TLSNegotiated;
break;
default:
throw new NotImplementedException($"Not Implemented: {handlerType} TLS Negotiation");
}
}
else
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(this._peer.OnNegotiationFinished)}()", this._parameters.context);
if (this._stream is not NonblockingTCPStream)
this._stream = new NonblockingTCPStream(this._streamer, true, this.Parameters.hostSettings.LowLevelConnectionSettings.ReadBufferSize);
this._peer.OnNegotiationFinished(this, this._stream, this._streamer, HTTPProtocolFactory.W3C_HTTP1);
}
}
catch (Exception ex)
{
this._peer.MustStopAdvancingToNextStep(this, NegotiationSteps.TLSNegotiation, NegotiationSteps.Finish, ex);
}
}
private void OnFramework_TLSNegotiated(FrameworkTLSStream stream, TCPStreamer streamer, string alpn, Exception error)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(OnFramework_TLSNegotiated)}(\"{alpn}\", {error})", this._parameters.context);
this._stream = stream;
if (error != null)
this._peer.OnNegotiationFailed(this, error);
else
this._peer.OnNegotiationFinished(this, stream, streamer, alpn);
}
#if !BESTHTTP_DISABLE_ALTERNATE_SSL
private void OnBC_TLSNegotiated(NonblockingBCTLSStream stream, TCPStreamer streamer, AbstractTls13Client tlsClient, Exception error)
{
string alpn = tlsClient.GetNegotiatedApplicationProtocol();
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(Negotiator), $"{nameof(OnBC_TLSNegotiated)}(\"{alpn}\", {error})", this._parameters.context);
// set the stream early if we return because of PreprocessRequestState, Dispose wouldn't call stream's Dispose
this._stream = stream;
if (error != null)
this._peer.OnNegotiationFailed(this, error);
else
this._peer.OnNegotiationFinished(this, stream, streamer, alpn);
}
#endif
private void SetupSocket(System.Net.Sockets.Socket socket, HostSettings hostSettings)
{
#if UNITY_WINDOWS || UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
// Set the keep-alive time and interval on windows
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd877220%28v=vs.85%29.aspx
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee470551%28v=vs.85%29.aspx
try
{
SetKeepAlive(socket, true, 30000, 1000);
}
catch { }
#endif
#if UNITY_STANDALONE_LINUX || UNITY_EDITOR_LINUX || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX || UNITY_IOS
if (this._parameters.hostSettings.LowLevelConnectionSettings.KeepAlive.EnableKeepAlive &&
this._parameters.hostSettings.LowLevelConnectionSettings.KeepAlive.IdleSeconds > 0 &&
this._parameters.hostSettings.LowLevelConnectionSettings.KeepAlive.IntervalSeconds > 0 &&
this._parameters.hostSettings.LowLevelConnectionSettings.KeepAlive.ProbeCount > 0)
{
unsafe
{
// Periodically test if connection is alive
// 0 = disables, 1 = enables
socket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.KeepAlive, true);
int handle = socket.Handle.ToInt32();
int value;
uint valueSize = sizeof(int);
try
{
const int SOL_TCP = 6;
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX || UNITY_IOS
const int TCP_KEEPIDLE = 16;
const int TCP_KEEPINTVL = 257;
const int TCP_KEEPCNT = 258;
#else
const int TCP_KEEPIDLE = 4;
const int TCP_KEEPINTVL = 5;
const int TCP_KEEPCNT = 6;
#endif
// When the SO_KEEPALIVE option is enabled, TCP probes a connection that has been idle for some amount of time. The default value for this idle period is 2 hours.
// The TCP_KEEPIDLE option can be used to affect this value for a given socket, and specifies the number of seconds of idle time between keepalive probes.
// Not cross platform. This option takes an int value, with a range of 1 to 32767.
// TCP_KEEPIDLE 7200 =>
value = this._parameters.hostSettings.LowLevelConnectionSettings.KeepAlive.IdleSeconds;
var ret = 0;
if ((ret = setsockopt(handle, SOL_TCP, TCP_KEEPIDLE, &value, valueSize)) != 0)
HTTPManager.Logger.Warning(nameof(Negotiator), $"setsockopt TCP_KEEPIDLE returned {ret}");
// Specifies the interval between packets that are sent to validate the connection.
// Not cross platform.
// TCP_KEEPINTVL 75 =>
value = this._parameters.hostSettings.LowLevelConnectionSettings.KeepAlive.IntervalSeconds;
if ((ret = setsockopt(handle, SOL_TCP, TCP_KEEPINTVL, &value, valueSize)) != 0)
HTTPManager.Logger.Warning(nameof(Negotiator), $"setsockopt TCP_KEEPINTVL returned {ret}");
// When the SO_KEEPALIVE option is enabled, TCP probes a connection that has been idle for some amount of time.
// If the remote system does not respond to a keepalive probe, TCP retransmits the probe a certain number of times before a connection is considered to be broken.
// The TCP_KEEPCNT option can be used to affect this value for a given socket, and specifies the maximum number of keepalive probes to be sent.
// This option takes an int value, with a range of 1 to 32767. Not cross platform.
// TCP_KEEPCNT 9 =>
value = this._parameters.hostSettings.LowLevelConnectionSettings.KeepAlive.ProbeCount;
if ((ret = setsockopt(handle, SOL_TCP, TCP_KEEPCNT, &value, valueSize)) != 0)
HTTPManager.Logger.Warning(nameof(Negotiator), $"setsockopt TCP_KEEPCNT returned {ret}");
// https://man7.org/linux/man-pages/man2/setsockopt.2.html
// int setsockopt(int sockfd, int level, int optname, const void optval[optlen], socklen_t optlen);
[System.Runtime.InteropServices.DllImport("libc", SetLastError = true)]
static extern int setsockopt(int socket, int level, int optname, void* optval, uint optlen);
}
catch (System.Exception ex)
{
HTTPManager.Logger.Exception(nameof(Negotiator), nameof(SetupSocket), ex, this._parameters.context);
}
}
}
#endif
try
{
// data sending is buffered for all protocols, so when we put data into the socket we want to send them asap
socket.NoDelay = true;
}
catch (Exception ex)
{
HTTPManager.Logger.Warning(nameof(Negotiator), $"{nameof(SetupSocket)} - NoDelay- {ex.Message}", this.Parameters.context);
}
try
{
// Don't let the sockets linger in the CLOSE_WAIT state
socket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.ReuseAddress, false);
}
catch { }
}
#if UNITY_WINDOWS || UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
private void SetKeepAlive(System.Net.Sockets.Socket socket, bool on, uint keepAliveTime, uint keepAliveInterval)
{
int size = System.Runtime.InteropServices.Marshal.SizeOf(new uint());
var inOptionValues = new byte[size * 3];
BitConverter.GetBytes((uint)(on ? 1 : 0)).CopyTo(inOptionValues, 0);
BitConverter.GetBytes((uint)keepAliveTime).CopyTo(inOptionValues, size);
BitConverter.GetBytes((uint)keepAliveInterval).CopyTo(inOptionValues, size * 2);
//client.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
int dwBytesRet = 0;
WSAIoctl(socket.Handle,
/*SIO_KEEPALIVE_VALS*/ System.Net.Sockets.IOControlCode.KeepAliveValues,
inOptionValues,
inOptionValues.Length,
/*NULL*/IntPtr.Zero,
0,
ref dwBytesRet,
/*NULL*/IntPtr.Zero,
/*NULL*/IntPtr.Zero);
}
[System.Runtime.InteropServices.DllImport("Ws2_32.dll")]
private static extern int WSAIoctl(
/* Socket, Mode */ IntPtr s, System.Net.Sockets.IOControlCode dwIoControlCode,
/* Optional Or IntPtr.Zero, 0 */ byte[] lpvInBuffer, int cbInBuffer,
/* Optional Or IntPtr.Zero, 0 */ IntPtr lpvOutBuffer, int cbOutBuffer,
/* reference to receive Size */ ref int lpcbBytesReturned,
/* IntPtr.Zero, IntPtr.Zero */ IntPtr lpOverlapped, IntPtr lpCompletionRoutine);
#endif
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5c0db4c5f3cd624998d9604696a485e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4a5f7f7b9c37db942b1913910fc1e5a5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,182 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams
{
public sealed class FrameworkTLSByteForwarder : Stream, ITCPStreamerContentConsumer
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length { get { return this._length; } }
private long _length;
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public long MaxBufferSize { get => Volatile.Read(ref this._maxBufferSize); set => Interlocked.Exchange(ref this._maxBufferSize, value); }
private long _maxBufferSize;
private TCPStreamer _streamer;
private LoggingContext _context;
private ITCPStreamerContentConsumer _contentConsumer;
private Queue<BufferSegment> _segmentsToReadFrom = new Queue<BufferSegment>(8);
private AutoResetEvent _are = new AutoResetEvent(false);
private ReaderWriterLockSlim _rws = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
private BufferSegment _currentReadSegment = BufferSegment.Empty;
public FrameworkTLSByteForwarder(TCPStreamer streamer, ITCPStreamerContentConsumer contentConsumer, long maxBufferSize, LoggingContext context)
{
this._streamer = streamer;
this._streamer.ContentConsumer = this;
this._contentConsumer = contentConsumer;
this._context = context;
this._maxBufferSize = maxBufferSize;
}
public void /*ITCPStreamerContentConsumer.*/ Write(BufferSegment buffer)
{
using var _ = new AutoReleaseBuffer(buffer);
this.Write(buffer.Data, buffer.Offset, buffer.Count);
}
int _pullContentInProgress;
void PullContentFromStreamer()
{
//using var _ = new WriteLock(this._rws);
if (Interlocked.CompareExchange(ref _pullContentInProgress, 1, 0) != 0)
return;
try
{
while (this._streamer.Length > 0 && this._length < this.MaxBufferSize)
{
var tmp = this._streamer.DequeueReceived();
if (tmp.Count <= 0)
{
HTTPManager.Logger.Verbose(nameof(FrameworkTLSByteForwarder), $"DequeueReceived({tmp}) !", this._context);
BufferPool.Release(tmp);
return;
}
this._segmentsToReadFrom.Enqueue(tmp);
this._length += tmp.Count;
}
}
finally
{
Interlocked.Exchange(ref _pullContentInProgress, 0);
}
}
public void /*ITCPStreamerContentConsumer.*/ OnContent(TCPStreamer streamer)
{
HTTPManager.Logger.Verbose(nameof(FrameworkTLSByteForwarder), $"OnContent({streamer?.Length})", this._context);
PullContentFromStreamer();
this._are?.Set();
this._contentConsumer?.OnContent(streamer);
}
public void /*ITCPStreamerContentConsumer.*/ OnConnectionClosed(TCPStreamer streamer) => this._contentConsumer?.OnConnectionClosed(streamer);
public void /*ITCPStreamerContentConsumer.*/ OnError(TCPStreamer streamer, Exception ex) => this._contentConsumer?.OnError(streamer, ex);
// Called by SslStream.Read expecting encrypted content
public override int Read(byte[] buffer, int offset, int count)
{
HTTPManager.Logger.Verbose(nameof(FrameworkTLSByteForwarder), $"Read({offset}, {count})", this._context);
PullContentFromStreamer();
int sumReadCount = 0;
while (this._currentReadSegment == BufferSegment.Empty && this._segmentsToReadFrom.Count == 0)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(FrameworkTLSByteForwarder), $"WaitOne() for new data!", this._context);
if (this.Length == 0)
this._are.WaitOne();
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(FrameworkTLSByteForwarder), $"WaitOne() returned!", this._context);
}
while ((this._currentReadSegment != BufferSegment.Empty || this._segmentsToReadFrom.Count > 0) && count > 0)
{
if (this._currentReadSegment != BufferSegment.Empty)
{
int readCount = Math.Min(count, this._currentReadSegment.Count);
Array.Copy(this._currentReadSegment.Data, this._currentReadSegment.Offset, buffer, offset, readCount);
offset += readCount;
count -= readCount;
sumReadCount += readCount;
if (this._currentReadSegment.Count <= readCount)
this._currentReadSegment = BufferSegment.Empty;
else
this._currentReadSegment = this._currentReadSegment.Slice(this._currentReadSegment.Offset + readCount);
}
else
{
this._currentReadSegment = this._segmentsToReadFrom.Dequeue();
}
}
this._length -= sumReadCount;
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(FrameworkTLSByteForwarder), $"Read() returns with readCount: {sumReadCount}, remaining: {this._length}", this._context);
return sumReadCount;
}
// Called by SslStream.Write with encrypted payload
public override void Write(byte[] buffer, int offset, int count)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(FrameworkTLSByteForwarder), $"Write({buffer.AsBuffer(offset, count)})", this._context);
var queued = BufferPool.Get(count, true, this._context);
Array.Copy(buffer, offset, queued, 0, count);
this._streamer.EnqueueToSend(queued.AsBuffer(count));
}
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._are?.Dispose();
this._are = null;
this._rws?.Dispose();
this._rws = null;
this._streamer?.Dispose();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c78a502070c09ea489f93f800efb2e28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,297 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Hosts.Settings;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams
{
/*
* --> FrameworkTLSStream.Write => SslStream.Write => TLSByteForwarder.Write => TCPStream.EnqueueToSend
*
* --> TLSByteForwarder.OnContent => SslStream.Read => FrameworkTLSStream.Read
* */
public sealed class FrameworkTLSStream : PeekableContentProviderStream, ITCPStreamerContentConsumer
{
public Action<FrameworkTLSStream, TCPStreamer, string /*negotiated appplication protocol*/, Exception> OnNegotiated;
public long MaxBufferSize { get => Volatile.Read(ref this._maxBufferSize); set => Interlocked.Exchange(ref this._maxBufferSize, value); }
private long _maxBufferSize;
private string _targetHost;
private TCPStreamer _streamer;
private FrameworkTLSByteForwarder _forwarder;
private SslStream _sslStream;
private LoggingContext _context;
private HostSettings _hostSettings;
private int peek_listIdx;
private int peek_pos;
#if UNITY_2021_2_OR_NEWER
private static bool loggedWarning = false;
#endif
private object locker = new object();
public FrameworkTLSStream(TCPStreamer streamer, string targetHost, HostSettings hostSettings)
{
this._streamer = streamer;
this._targetHost = targetHost;
this._context = new LoggingContext(this);
this._context.Add("streamer", this._streamer.Context);
this._hostSettings = hostSettings;
this._maxBufferSize = hostSettings.LowLevelConnectionSettings.ReadBufferSize;
this._forwarder = new FrameworkTLSByteForwarder(this._streamer, this, this.MaxBufferSize, this._context);
this._sslStream = new SslStream(this._forwarder,
leaveInnerStreamOpen: false,
OnUserCertificationValidation,
OnUserCertificationSelection,
EncryptionPolicy.RequireEncryption);
this._sslStream.BeginAuthenticateAsClient(targetHost,
null,
this._hostSettings.TLSSettings.FrameworkTLSSettings.TlsVersions,
true,
OnAuthenticatedAsClient,
null);
}
private bool OnUserCertificationValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
HTTPManager.Logger.Information(nameof(FrameworkTLSStream), $"{nameof(OnUserCertificationValidation)}({sender}, {certificate}, {chain}, {sslPolicyErrors})", this._context);
var validator = this._hostSettings.TLSSettings.FrameworkTLSSettings.CertificationValidator;
if (validator == null)
return FrameworkTLSSettings.DefaultCertificationValidator(_targetHost, certificate, chain, sslPolicyErrors);
return validator(this._targetHost, certificate, chain, sslPolicyErrors);
}
private X509Certificate OnUserCertificationSelection(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers)
{
HTTPManager.Logger.Information(nameof(FrameworkTLSStream), $"{nameof(OnUserCertificationSelection)}({sender}, {targetHost}, {localCertificates}, {remoteCertificate}, {acceptableIssuers?.Length})", this._context);
return this._hostSettings.TLSSettings.FrameworkTLSSettings.ClientCertificationProvider?.Invoke(targetHost, localCertificates, remoteCertificate, acceptableIssuers);
}
private void OnAuthenticatedAsClient(IAsyncResult ar)
{
HTTPManager.Logger.Information(nameof(FrameworkTLSStream), $"{nameof(OnAuthenticatedAsClient)}()", this._context);
try
{
this._sslStream.EndAuthenticateAsClient(ar);
string alpn = string.Empty;
#if UNITY_2021_2_OR_NEWER
try
{
alpn = this._sslStream.NegotiatedApplicationProtocol.ToString();
}
catch (Exception ex)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(FrameworkTLSStream), $"{nameof(OnAuthenticatedAsClient)}() - NegotiatedApplicationProtocol", ex, this._context);
if (!loggedWarning)
{
loggedWarning = true;
HTTPManager.Logger.Warning(nameof(FrameworkTLSStream), $"{nameof(OnAuthenticatedAsClient)}(): SslStream's NegotiatedApplicationProtocol inaccessible! Using http/1.", this._context);
}
}
#endif
if (string.IsNullOrEmpty(alpn))
alpn = HTTPProtocolFactory.W3C_HTTP1;
CallOnNegotiated(alpn, null);
BeginRead();
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(FrameworkTLSStream), $"{nameof(OnAuthenticatedAsClient)}()", ex, this._context);
CallOnNegotiated(null, ex);
}
}
bool CallOnNegotiated(string alpn, Exception error)
{
HTTPManager.Logger.Verbose(nameof(FrameworkTLSStream), $"CallOnNegotiated(\"{alpn}\", {error})", this._context);
var callback = Interlocked.CompareExchange(ref this.OnNegotiated, null, this.OnNegotiated);
if (callback != null)
{
try
{
callback(this, this._streamer, alpn, error);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(FrameworkTLSStream), "OnContent - OnNegotiated", ex, this._streamer.Context);
}
}
return callback != null;
}
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++];
}
public void OnContent(TCPStreamer streamer)
{
if (this._sslStream.IsAuthenticated)
BeginRead();
}
int _reading;
private void BeginRead()
{
if (Interlocked.CompareExchange(ref _reading, 1, 0) != 0)
{
//HTTPManager.Logger.Warning(nameof(FrameworkTLSStream), $"{nameof(BeginRead)}() - already reading!", this._context);
return;
}
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(FrameworkTLSStream), $"{nameof(BeginRead)}()", this._context);
var buffer = BufferPool.Get(Math.Min(this.MaxBufferSize, 1 * 1024 * 1024), true, this._context);
this._sslStream.ReadAsync(buffer, 0, buffer.Length)
//.AsTask()
.ContinueWith((ti) =>
{
int readCount = 0;
try
{
readCount = ti.Result;
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(FrameworkTLSStream), $"{nameof(OnRead)}({readCount}, {this.Length})", this._context);
if (readCount > 0)
{
lock (locker)
base.Write(buffer.AsBuffer(readCount));
}
this.Consumer?.OnContent();
}
finally
{
Interlocked.Exchange(ref _reading, 0);
if (readCount > 0)
BeginRead();
}
})
.ConfigureAwait(false);
/*IAsyncResult ar = null;
try
{
do
{
var buffer = BufferPool.Get(Math.Min(this.MaxBufferSize, 1 * 1024 * 1024), true, this._context);
ar = this._sslStream.BeginRead(buffer, 0, buffer.Length, OnRead, buffer);
} while (ar != null && ar.CompletedSynchronously);
}
finally
{
Interlocked.Exchange(ref _reading, 0);
//if (ar is not null && ar.CompletedSynchronously)
// BeginRead();
}*/
}
private void OnRead(IAsyncResult ar)
{
try
{
var readCount = this._sslStream.EndRead(ar);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(FrameworkTLSStream), $"{nameof(OnRead)}({readCount}, {ar.CompletedSynchronously})", this._context);
if (readCount > 0)
{
var buffer = ar.AsyncState as byte[];
lock (locker)
base.Write(buffer.AsBuffer(readCount));
this.Consumer?.OnContent();
}
// This call might fail if the read completed synchronously.
BeginRead();
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(FrameworkTLSStream), $"EndRead", ex, this._context);
}
finally
{
}
}
public void OnConnectionClosed(TCPStreamer streamer) => this.Consumer?.OnConnectionClosed();
public void OnError(TCPStreamer streamer, Exception ex) => this.Consumer?.OnError(ex);
public override int Read(byte[] buffer, int offset, int count) { lock (locker) return base.Read(buffer, offset, count); }
public override void Write(byte[] buffer, int offset, int count) => this._sslStream.Write(buffer, offset, count);
public override void Write(BufferSegment bufferSegment)
{
using var _ = new AutoReleaseBuffer(bufferSegment);
this.Write(bufferSegment.Data, bufferSegment.Offset, bufferSegment.Count);
}
public override void Flush() => this._sslStream.Flush();
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._sslStream?.Dispose();
this._sslStream = null;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a8f61b94735e634da61bc8b1324b56f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,300 @@
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
using Best.HTTP.SecureProtocol.Org.BouncyCastle.Tls;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
using Best.HTTP.Shared.TLS;
using System;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams
{
public sealed class NonblockingBCTLSStream : PeekableContentProviderStream, ITCPStreamerContentConsumer
{
public Action<NonblockingBCTLSStream, TCPStreamer, AbstractTls13Client, Exception> OnNegotiated;
public long MaxBufferSize { get => Volatile.Read(ref this._maxBufferSize); set => Interlocked.Exchange(ref this._maxBufferSize, value); }
private long _maxBufferSize;
private TlsClientProtocol _tlsClientProtocol;
private AbstractTls13Client _tlsClient;
//private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
private object locker = new object();
private TCPStreamer _streamer;
private uint _sendBufferSize;
private bool _disposeStreamer;
private int peek_listIdx;
private int peek_pos;
private bool _disposed;
public NonblockingBCTLSStream(TCPStreamer streamer, TlsClientProtocol tlsClientProtocol, AbstractTls13Client tlsClient, bool disposeStreamer, uint maxBufferSize)
{
this._streamer = streamer;
this._streamer.ContentConsumer = this;
this._disposeStreamer = disposeStreamer;
// Maximize buffer use
this._sendBufferSize = this._streamer.MaxBufferedWriteAmount;
this._tlsClientProtocol = tlsClientProtocol;
this._tlsClient = tlsClient;
this.Write(null, 0, 0);
if (streamer.IsConnectionClosed)
CallOnNegotiated(new Exception("Connection closed before TLS negotiation started!"));
this._maxBufferSize = maxBufferSize;
}
public override void BeginPeek()
{
lock (this.locker)
{
peek_listIdx = 0;
peek_pos = base.bufferList.Count > 0 ? base.bufferList[0].Offset : 0;
}
}
public override int PeekByte()
{
lock (this.locker)
{
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++];
}
}
// Called when content from the server is available
public void OnContent(TCPStreamer streamer)
{
lock (this.locker)
{
var socket = this._streamer?.Socket;
// Ignore content after a TLS client protocol closure (it can happen because of an error, but the server still pumping data to the client).
if (this._disposed || socket == null || this._tlsClientProtocol.IsClosed)
{
if (this._tlsClientProtocol.IsHandshaking)
CallOnNegotiated(new Exception("Connection closed while TLS negotiation is in progress!"));
return;
}
try
{
PullContentFromStreamer();
}
catch (Exception ex)
{
if (!CallOnNegotiated(ex))
this.Consumer?.OnError(ex);
}
// There's no read/write when it's still hanshaking, so we have to simulate one.
if (this._tlsClientProtocol.IsHandshaking)
{
this.Write(null, 0, 0);
return;
}
else
CallOnNegotiated(null);
// Call OnContent only if we have something to offer.
if (this.Length > 0)
this.Consumer?.OnContent();
}
}
public void OnConnectionClosed(TCPStreamer streamer)
{
var consumer = this.Consumer;
if (consumer != null)
consumer.OnConnectionClosed();
else
CallOnNegotiated(new Exception("TCP Connection closed during TLS negotiation!"));
}
public void OnError(TCPStreamer streamer, Exception ex)
{
var consumer = this.Consumer;
if (consumer != null)
consumer.OnError(ex);
else
CallOnNegotiated(ex);
}
// TODO: It can throw an exception (for example in case of a bad record mac :/), we have to
// 1.) handle it
// 2.) report to the consumer (through an OnError call)
// 3.) prevent other read/write attempts.
private void PullContentFromStreamer()
{
while (!this._disposed && this._streamer.Length > 0 && this._length < this.MaxBufferSize)
{
var tmp = this._streamer.DequeueReceived();
if (tmp.Count <= 0)
{
BufferPool.Release(tmp);
return;
}
try
{
this._tlsClientProtocol.OfferInput(tmp.Data, tmp.Offset, tmp.Count);
// each call of OfferInput might generate data (for example alerts) to send to the remote peer!
this.Write(null, 0, 0);
}
catch (Exception ex)
{
BufferPool.Release(tmp);
// each call of OfferInput might generate data (for example alerts) to send to the remote peer!
this.Write(null, 0, 0);
CallOnNegotiated(ex);
throw;
}
int available = this._tlsClientProtocol.GetAvailableInputBytes();
byte[] readBuffer = tmp.Data;
while (available > 0)
{
if (readBuffer == null)
readBuffer = BufferPool.Get(available, true, this._streamer.Context);
var readCount = this._tlsClientProtocol.ReadInput(readBuffer, 0, readBuffer.Length);
base.Write(readBuffer.AsBuffer(readCount));
readBuffer = null;
available = this._tlsClientProtocol.GetAvailableInputBytes();
}
BufferPool.Release(readBuffer);
}
}
public override int Read(byte[] buffer, int offset, int count)
{
lock (this.locker)
{
var readCount = base.Read(buffer, offset, count);
// pull content from the streamer, if buffered amount is less then the desired.
if (base.Length <= this.MaxBufferSize)
{
try
{
PullContentFromStreamer();
}
catch (Exception ex)
{
this.Consumer.OnError(ex);
}
}
return readCount;
}
}
// write -> tls encoding -> TCP streamer
public override void Write(byte[] buffer, int offset, int count)
{
lock (this.locker)
{
var streamer = this._streamer;
if (streamer == null)
return;
if (buffer != null && count > 0)
this._tlsClientProtocol.WriteApplicationData(buffer, offset, count);
int available = 0;
available = this._tlsClientProtocol.GetAvailableOutputBytes();
while (available > 0)
{
var tmp = BufferPool.Get(Math.Min(available, this._sendBufferSize), true, streamer.Context);
int readCount = 0;
try
{
readCount = this._tlsClientProtocol.ReadOutput(tmp, 0, tmp.Length);
}
catch
{
BufferPool.Release(tmp);
throw;
}
streamer.EnqueueToSend(tmp.AsBuffer(readCount));
available = this._tlsClientProtocol.GetAvailableOutputBytes();
}
}
}
public override void Write(BufferSegment bufferSegment)
{
lock (this.locker)
{
using var _ = new AutoReleaseBuffer(bufferSegment);
Write(bufferSegment.Data, bufferSegment.Offset, bufferSegment.Count);
}
}
bool CallOnNegotiated(Exception error)
{
var callback = Interlocked.CompareExchange(ref this.OnNegotiated, null, this.OnNegotiated);
if (callback != null)
{
try
{
callback(this, this._streamer, this._tlsClient, error);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(NonblockingBCTLSStream), "CallOnNegotiated", ex, this._streamer.Context);
}
}
return callback != null;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (this._disposed)
return;
HTTPManager.Logger.Verbose(nameof(NonblockingBCTLSStream), "Dispose", this._streamer.Context);
this._disposed = true;
this._tlsClientProtocol?.Close();
if (this._disposeStreamer)
this._streamer?.Dispose();
this._streamer = null;
//this._rwLock?.Dispose();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db49d73dab50cad4a92350516ec20d8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,153 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
using System;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams
{
/// <summary>
/// A non-blocking-read stream over a TCPStreamer that buffers the received bytes from the network in a Peekable stream.
/// </summary>
public sealed class NonblockingTCPStream : PeekableContentProviderStream, ITCPStreamerContentConsumer
{
public long MaxBufferSize { get => Volatile.Read(ref this._maxBufferSize); set => Interlocked.Exchange(ref this._maxBufferSize, value); }
private long _maxBufferSize;
private TCPStreamer _streamer;
private bool _disposeStreamer;
private int peek_listIdx;
private int peek_pos;
private object _locker = new object();
public NonblockingTCPStream(TCPStreamer streamer, bool disposeStreamer, uint maxBufferSize)
{
this._streamer = streamer;
this._streamer.ContentConsumer = this;
this._disposeStreamer = disposeStreamer;
this._maxBufferSize = maxBufferSize;
}
public override void BeginPeek()
{
lock (this._locker)
{
peek_listIdx = 0;
peek_pos = base.bufferList.Count > 0 ? base.bufferList[0].Offset : 0;
}
}
public override int PeekByte()
{
lock (this._locker)
{
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++];
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
lock (this._locker)
{
if (this._streamer != null)
this._streamer.ContentConsumer = null;
if (this._disposeStreamer)
this._streamer?.Dispose();
this._streamer = null;
}
}
// PeekableStream's default implementation of write would place the buffer into its inner segment list,
// but here we want to send it to the server instead.
public override void Write(byte[] buffer, int offset, int count) => this._streamer?.EnqueueToSend(buffer.CopyAsBuffer(offset, count));
public override void Write(BufferSegment buffer) => this._streamer?.EnqueueToSend(buffer);
public override int Read(byte[] buffer, int offset, int count)
{
lock (this._locker)
{
int readCount = base.Read(buffer, offset, count);
// pull content from the streamer, if buffered amount is less then the desired.
if (base.Length <= this.MaxBufferSize)
{
DequeueFromStreamer();
this._streamer.BeginReceive();
}
return readCount;
}
}
public void OnContent(TCPStreamer streamer)
{
lock (this._locker)
{
DequeueFromStreamer();
var consumer = this.Consumer;
if (consumer != null)
consumer?.OnContent();
else
HTTPManager.Logger.Error(nameof(NonblockingTCPStream), $"{nameof(OnContent)}({streamer}) - No consumer to call OnContent on!", streamer.Context);
}
}
public void OnConnectionClosed(TCPStreamer streamer)
{
var consumer = this.Consumer;
if (consumer != null)
consumer?.OnConnectionClosed();
else
HTTPManager.Logger.Error(nameof(NonblockingTCPStream), $"{nameof(OnConnectionClosed)}({streamer}) - No consumer to call OnConnectionClosed on!", streamer.Context);
}
public void OnError(TCPStreamer streamer, Exception ex)
{
var consumer = this.Consumer;
if (consumer != null)
consumer?.OnError(ex);
else
HTTPManager.Logger.Error(nameof(NonblockingTCPStream), $"{nameof(OnError)}({streamer}, {ex}) - No consumer to call OnError on!", streamer.Context);
}
void DequeueFromStreamer()
{
if (this._streamer == null)
return;
while (this._streamer.Length > 0 && this._length < this.MaxBufferSize)
{
var segment = this._streamer.DequeueReceived();
if (segment.Count <= 0)
return;
base.Write(segment);
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e4c36b588df49d147981ba1e73401b7e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,163 @@
using System;
using System.Threading;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Streams;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams
{
public sealed class NonblockingUnderlyingStream : PeekableContentProviderStream
{
private System.IO.Stream _stream;
private int _receiving;
private uint _maxBufferSize;
private LoggingContext _context;
private object _locker = new object();
private int peek_listIdx;
private int peek_pos;
public NonblockingUnderlyingStream(System.IO.Stream stream, uint maxBufferSize, LoggingContext context)
{
this._stream = stream;
this._context = context;
this._maxBufferSize = maxBufferSize;
if (!stream.CanRead)
throw new NotSupportedException("Stream.Read");
}
public override int Read(byte[] buffer, int offset, int count)
{
lock (this._locker)
{
int readCount = base.Read(buffer, offset, count);
if (base.Length <= this._maxBufferSize)
BeginReceive();
return readCount;
}
}
public void BeginReceive()
{
if (base._length < this._maxBufferSize && Interlocked.CompareExchange(ref this._receiving, 1, 0) == 0 && this._stream.CanRead)
{
long readCount = this._maxBufferSize - base._length;
var readBuffer = BufferPool.Get(readCount, true, this._context);
try
{
var ar = this._stream.BeginRead(readBuffer, 0, (int)readCount, OnReceived, readBuffer);
if (ar.CompletedSynchronously)
HTTPManager.Logger.Warning(nameof(NonblockingUnderlyingStream), $"CompletedSynchronously!", this._context);
}
catch (Exception ex)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(NonblockingUnderlyingStream), $"{nameof(this._stream.BeginRead)}", ex, this._context);
BufferPool.Release(Interlocked.Exchange(ref readBuffer, null));
this.Consumer.OnError(ex);
}
}
}
private void OnReceived(IAsyncResult ar)
{
int readCount = 0;
bool isClosed = true;
var readBuffer = ar.AsyncState as byte[];
try
{
readCount = this._stream.EndRead(ar);
isClosed = readCount <= 0;
if (!isClosed)
{
lock (this._locker)
base.Write(readBuffer.AsBuffer(0, readCount));
try
{
this.Consumer?.OnContent();
}
catch (Exception e)
{
HTTPManager.Logger.Exception(nameof(NonblockingUnderlyingStream), "ContentConsumer.OnContent", e, this._context);
}
}
}
catch (Exception ex)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(NonblockingUnderlyingStream), $"{nameof(OnReceived)}", ex, this._context);
}
finally
{
if (!isClosed)
{
Interlocked.Exchange(ref this._receiving, 0);
BeginReceive();
}
else
{
BufferPool.Release(readBuffer);
try
{
this.Consumer?.OnConnectionClosed();
}
catch (Exception e)
{
HTTPManager.Logger.Exception(nameof(NonblockingUnderlyingStream), "Consumer.OnConnectionClosed", e, this._context);
}
}
}
}
public override void BeginPeek()
{
lock (this._locker)
{
peek_listIdx = 0;
peek_pos = base.bufferList.Count > 0 ? base.bufferList[0].Offset : 0;
}
}
public override int PeekByte()
{
lock (this._locker)
{
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++];
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._stream.Dispose();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: efbc3c9e712aef64b8f0de50c6fa0ad5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,451 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using System;
using System.Net.Sockets;
using System.Threading;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Network.DNS.Cache;
using Best.HTTP.Shared.PlatformSupport.Text;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp
{
/// <summary>
/// Contains settings related to TCP Ringmaster, which manages and optimizes TCP connections.
/// </summary>
public sealed class TCPRingmasterSettings
{
/// <summary>
/// The maximum number of simultaneous TCP racers. Racers are used to establish and manage connections.
/// </summary>
public int MaxSimultaneousRacers = 4;
/// <summary>
/// Determines whether to shuffle addresses before assigning racing lanes.
/// </summary>
public bool ShuffleAddresses = true;
/// <summary>
/// Callback to implement a custom address shuffle algorithm. When assigned, no plugin-defined shuffle algorithm will be executed.
/// </summary>
/// <remarks>It must be thread-safe.</remarks>
public Action<TCPRaceParameters> CustomAddressShuffleAlgorithm;
/// <summary>
/// The granularity of cancellation checking for TCP races. It specifies the time interval for checking if a race should be canceled.
/// </summary>
public TimeSpan CancellationCheckingGranularity = TimeSpan.FromMilliseconds(100);
public override string ToString() => $"[{nameof(TCPRingmasterSettings)} {this.MaxSimultaneousRacers}, {this.ShuffleAddresses}, {this.CancellationCheckingGranularity}]";
}
/// <summary>
/// Represents the result of a TCP race competition, including the winning socket or an error.
/// </summary>
public sealed class TCPRaceResult
{
/// <summary>
/// The socket that won the race competition, if available.
/// </summary>
public Socket WinningSocket;
/// <summary>
/// The error encountered during the race competition, if any.
/// </summary>
public Exception Error;
/// <summary>
/// Initializes a new instance of the <see cref="TCPRaceResult"/> class with the winning socket and an error, if any.
/// </summary>
/// <param name="socket">The winning socket of the race competition, if available.</param>
/// <param name="ex">The error encountered during the race competition, if any.</param>
public TCPRaceResult(Socket socket, Exception ex)
{
this.WinningSocket = socket;
this.Error = ex;
}
public override string ToString() => $"[{nameof(TCPRaceResult)} {this.WinningSocket}, {this.Error}]";
}
/// <summary>
/// Represents a TCP race competition with parameters and status.
/// </summary>
sealed class Race
{
/// <summary>
/// The parameters for the TCP race competition.
/// </summary>
public TCPRaceParameters Parameters;
/// <summary>
/// The index of the next address to connect to.
/// </summary>
public int NextAddressIndex;
/// <summary>
/// The number of running lanes in the competition.
/// </summary>
public int RunningLanes;
public override string ToString() => $"[{nameof(Race)} {this.Parameters}, {this.NextAddressIndex}, {this.RunningLanes}]";
}
/// <summary>
/// Represents a racing lane in a TCP race competition.
/// </summary>
sealed class RacingLane
{
/// <summary>
/// The associated race and its parameters.
/// </summary>
public Race Race;
/// <summary>
/// The index of the address to connect to.
/// </summary>
public int AddressIndex;
/// <summary>
/// The index of the racing lane.
/// </summary>
public int LaneIndex;
/// <summary>
/// The socket used for the racing lane.
/// </summary>
public Socket Socket;
public override string ToString() => $"[{nameof(RacingLane)} {this.Race}, {this.AddressIndex}, {this.LaneIndex}, {this.Socket}]";
}
/// <summary>
/// Contains parameters and settings for a TCP race competition to establish connections.
/// </summary>
public sealed class TCPRaceParameters
{
/// <summary>
/// An array of DNS IP addresses to be used for racing to establish a connection.
/// </summary>
public DNSIPAddress[] Addresses;
/// <summary>
/// The hostname to connect to.
/// </summary>
public string Hostname;
/// <summary>
/// The port to connect to.
/// </summary>
public int Port;
/// <summary>
/// The cancellation token used to cancel the TCP race competition.
/// </summary>
public CancellationToken Token;
/// <summary>
/// A callback function to announce the winner of the TCP race competition.
/// </summary>
/// <param name="parameters">The TCPRaceParameters used for the race competition.</param>
/// <param name="result">The result of the race competition, including the winning socket or an error.</param>
public Action<TCPRaceParameters, TCPRaceResult> AnnounceWinnerCallback;
/// <summary>
/// Optional context for logging and tracking purposes.
/// </summary>
public LoggingContext Context;
/// <summary>
/// A user-defined tag associated with the TCP race parameters.
/// </summary>
public object Tag;
public override string ToString()
{
var sb = StringBuilderPool.Get(2 + this.Addresses.Length);
sb.Append('[');
for (int i = 0; i < this.Addresses.Length; ++i)
{
sb.Append(this.Addresses[i]);
if (i < this.Addresses.Length - 1)
sb.Append(", ");
}
sb.Append(']');
var ips = StringBuilderPool.ReleaseAndGrab(sb);
return $"[{nameof(TCPRaceParameters)} \"{this.Hostname}:{this.Port}\" Addresses({this.Addresses.Length}): {ips}]";
}
}
/// <summary>
/// <para>TCPRingmaster provides a method called <see cref="TCPRingmaster.StartCompetion(TCPRaceParameters)"/>, which is used to initiate a competition among multiple TCP connections.
/// Each connection attempt races against the others to establish a connection, and the first successful connection is considered the winner.</para>
/// The class allows to specify a callback function (through <see cref="TCPRaceParameters.AnnounceWinnerCallback"/>) that gets invoked when a winning connection is established or when the competition is canceled.
/// This callback can be used to take action based on the competition outcome.
/// <para>Additionally it includes logic for optimizing the order in which connection attempts are made:
/// <list type="bullet">
/// <item><description>It can shuffle the order of addresses to improve the chances of quickly finding a working address.</description></item>
/// <item><description>It handles scenarios where some addresses may not be working and prioritizes working addresses.</description></item>
/// </list>
/// </para>
/// </summary>
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstruction]
public static class TCPRingmaster
{
/// <summary>
/// Starts a TCP race competition to establish connections based on the provided parameters.
/// </summary>
/// <param name="parameters">The parameters and settings for the TCP race competition.</param>
public static void StartCompetion(TCPRaceParameters parameters)
{
// Initial idea:
// create Min(racers.Addresses.Length, Options.MaxSimultaneousRacers) lanes
// and increase RunningLanes
// randomize adresses
// assign one address to each lane, put the remaining addresses' index to the IndexQueue
// start connecting for each lane
var options = HTTPManager.PerHostSettings.Get(parameters.Hostname);
Race race = new Race
{
Parameters = parameters,
NextAddressIndex = 0,
RunningLanes = Math.Min(parameters.Addresses.Length, options.TCPRingmasterSettings.MaxSimultaneousRacers),
};
// Shuffle addresses?
var algo = options.TCPRingmasterSettings.CustomAddressShuffleAlgorithm;
if (algo != null)
{
try
{
algo(parameters);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(options.TCPRingmasterSettings.CustomAddressShuffleAlgorithm)}({parameters})", ex, parameters.Context);
}
}
else
{
// This helps in cases where the DNS query returns with a lot of addresses whos the first half isn't working.
// In this case, after shuffling there's better chance that a working address can be found in the first MaxSimultaneousRacers lanes.
if (options.TCPRingmasterSettings.ShuffleAddresses)
ShuffleAddresses(parameters.Addresses);
}
// Move non-working addresses to the end of the array, we will give them another chance
// but others will have higher priority
int nonWorkingCount = 0;
for (int i = 0; i < parameters.Addresses.Length - 1; i++)
{
var address = parameters.Addresses[i];
if (!address.IsWorkedLastTime)
{
Array.Copy(parameters.Addresses, i + 1, parameters.Addresses, i, parameters.Addresses.Length - i - 1);
parameters.Addresses[parameters.Addresses.Length - 1] = address;
if (++nonWorkingCount >= parameters.Addresses.Length)
break;
i--;
}
}
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(StartCompetion)}({parameters}) - creating {race.RunningLanes} lane(s)", parameters.Context);
int nextIdx = race.NextAddressIndex;
while (nextIdx < race.RunningLanes)
{
var address = race.Parameters.Addresses[nextIdx];
var socket = new Socket(address.IPAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
var lane = new RacingLane
{
AddressIndex = nextIdx,
LaneIndex = nextIdx,
Race = race,
Socket = socket
};
nextIdx = Interlocked.Increment(ref lane.Race.NextAddressIndex);
var asyncResult = socket.BeginConnect(address.IPAddress, parameters.Port, OnLaneFinished, lane);
// Under Android (and possible under other non-windows platforms) Unity doesn't call the OnLaneFinished callback, only returns with the IAsyncResult instance.
if (asyncResult.CompletedSynchronously && asyncResult.IsCompleted)
OnLaneFinished(asyncResult);
}
if (parameters.Token != CancellationToken.None)
Extensions.Timer.Add(new Extensions.TimerData(options.TCPRingmasterSettings.CancellationCheckingGranularity, race, CheckForCanceled));
}
private static void OnLaneFinished(IAsyncResult ar)
{
var lane = ar.AsyncState as RacingLane;
try
{
// Lane callback logic:
// Increase CompletedRacers
// If there's an error completing the socket, or isn't connected,
// pick the next index and start to connect
// if no more racers (CompletedRacers == racers.Addresses.Length) and no more running lanes, call callback with error
// decrease RunningLanes
// Else If callback isn't null, call callback
lane.Socket.EndConnect(ar);
var callback = lane.Race.Parameters.AnnounceWinnerCallback;
callback = Interlocked.CompareExchange(ref lane.Race.Parameters.AnnounceWinnerCallback, null, callback);
if (callback != null)
{
var runningLanes = Interlocked.Decrement(ref lane.Race.RunningLanes);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) - Winner lane! Address: {lane.Race.Parameters.Addresses[lane.AddressIndex]}, remaining lanes: {runningLanes}", lane.Race.Parameters.Context);
try
{
callback.Invoke(lane.Race.Parameters, new TCPRaceResult(lane.Socket, null));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}({lane.LaneIndex}) - callback", ex, lane.Race.Parameters.Context);
}
}
else
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) - Late lane, disconnecting...", lane.Race.Parameters.Context);
// The callback is already called before this lane finished, we have no use for this socket, have to disconnect.
lane.Socket.Shutdown(SocketShutdown.Both);
lane.Socket.BeginDisconnect(false, OnSocketDisconnect, lane);
}
}
catch (InvalidOperationException)
{
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.endconnect#system-net-sockets-socket-endconnect(system-iasyncresult)
// EndConnect(IAsyncResult) was previously called for the asynchronous connection.
}
catch (Exception ex)
{
lane.Socket.Dispose();
DNSCache.ReportAsNonWorking(lane.Race.Parameters.Hostname, lane.Race.Parameters.Addresses[lane.AddressIndex].IPAddress, lane.Race.Parameters.Context);
var nextIndex = Interlocked.Increment(ref lane.Race.NextAddressIndex);
if (nextIndex < lane.Race.Parameters.Addresses.Length)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) - Couldn't connect to address, grabbing new index: {nextIndex}", lane.Race.Parameters.Context);
lane.AddressIndex = nextIndex;
lane.Socket = new Socket(lane.Race.Parameters.Addresses[lane.AddressIndex].IPAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
var asyncResult = lane.Socket.BeginConnect(lane.Race.Parameters.Addresses[lane.AddressIndex].IPAddress, lane.Race.Parameters.Port, OnLaneFinished, lane);
if (asyncResult.CompletedSynchronously && asyncResult.IsCompleted)
OnLaneFinished(asyncResult);
}
else
{
// no more address to try, decrase the running lanes
var runningLanes = Interlocked.Decrement(ref lane.Race.RunningLanes);
if (runningLanes == 0)
{
// If no more lanes, try to call the callback.
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) Race is over, calling callback if it's still available", lane.Race.Parameters.Context);
var callback = lane.Race.Parameters.AnnounceWinnerCallback;
callback = Interlocked.CompareExchange(ref lane.Race.Parameters.AnnounceWinnerCallback, null, callback);
try
{
callback?.Invoke(lane.Race.Parameters, new TCPRaceResult(null, ex));
}
catch (Exception nex)
{
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) - callback", nex, lane.Race.Parameters.Context);
}
}
else if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnLaneFinished)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex}) Run out of free racers, there are still running racers({runningLanes}), retiring this lane.", lane.Race.Parameters.Context);
}
}
}
private static void OnSocketDisconnect(IAsyncResult ar)
{
var lane = ar.AsyncState as RacingLane;
try
{
lane.Socket.EndDisconnect(ar);
}
catch (Exception ex)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(OnSocketDisconnect)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex})", ex, lane.Race.Parameters.Context);
}
finally
{
Interlocked.Decrement(ref lane.Race.RunningLanes);
lane.Socket.Dispose();
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(OnSocketDisconnect)}(Lane: {lane.LaneIndex}, Addr.: {lane.AddressIndex})", lane.Race.Parameters.Context);
}
}
/// <summary>
/// Inplace shuffles addresses.
/// </summary>
/// <param name="addresses">The array of <see cref="DNSIPAddress"/> to shuffle.</param>
public static void ShuffleAddresses(DNSIPAddress[] addresses)
{
var rand = new Random();
int n = addresses.Length;
while (n > 1)
{
int k = rand.Next(n--);
(addresses[n], addresses[k]) = (addresses[k], addresses[n]);
}
}
private static bool CheckForCanceled(DateTime now, object context)
{
var query = context as Race;
if (query.Parameters.Token.IsCancellationRequested)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPRingmaster), $"{nameof(CheckForCanceled)}({query}) - Token.IsCancellationRequested!", query.Parameters.Context);
var callback = query.Parameters.AnnounceWinnerCallback;
callback = Interlocked.CompareExchange<Action<TCPRaceParameters, TCPRaceResult>>(ref query.Parameters.AnnounceWinnerCallback, null, callback);
try
{
callback?.Invoke(query.Parameters, new TCPRaceResult(null, new TimeoutException("DNS Query Timed Out")));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(TCPRingmaster), $"{nameof(CheckForCanceled)}({query}) - callback", ex, query.Parameters.Context);
}
}
return query.Parameters.AnnounceWinnerCallback != null;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cd27596f35a31d24981e07594bc30a77
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,637 @@
#define _LOG_TCP_STREAMER
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using System;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Network.Tcp
{
/// <summary>
/// The ITCPStreamerContentConsumer interface represents a specialized content consumer for use with <see cref="TCPStreamer"/>. It offers methods for writing data to the streamer and handling content-related events.
/// </summary>
/// <remarks>
/// <para>
/// Key Functions of ITCPStreamerContentConsumer:
/// </para>
/// <list type="bullet">
/// <item>
/// <term>Data Writing</term><description>Provides methods to write data to the associated <see cref="TCPStreamer"/> instance, allowing content to be sent over the TCP connection.
/// </description></item>
/// <item>
/// <term>Content Handling</term><description>Defines event methods for notifying consumers when new content is available, the connection is closed, or errors occur during data transfer.
/// </description></item>
/// </list>
/// </remarks>
/// <seealso cref="TCPStreamer"/>
public interface ITCPStreamerContentConsumer
{
/// <summary>
/// Gets or sets the maximum amount of data bufferable in the consumer.
/// </summary>
long MaxBufferSize { get; set; }
/// <summary>
/// Writes the specified data buffer to the associated <see cref="TCPStreamer"/> instance. The data is copied into a new buffer and passed to the streamer for transmission.
/// </summary>
/// <param name="buffer">The byte array containing the data to be written.</param>
/// <param name="offset">The zero-based byte offset in the buffer from which to begin writing.</param>
/// <param name="count">The number of bytes to write from the buffer.</param>
void Write(byte[] buffer, int offset, int count);
/// <summary>
/// Writes the specified <see cref="BufferSegment"/> directly to the associated <see cref="TCPStreamer"/> instance. The content of the buffer is passed to the streamer for transmission, and the ownership of the buffer is transferred to the <see cref="TCPStreamer"/> too.
/// </summary>
/// <param name="buffer">The <see cref="BufferSegment"/> containing the data to be written.</param>
void Write(BufferSegment buffer);
/// <summary>
/// Called when new content is available from the associated <see cref="TCPStreamer"/> instance.
/// </summary>
/// <param name="streamer">The <see cref="TCPStreamer"/> instance providing the content.</param>
void OnContent(TCPStreamer streamer);
/// <summary>
/// Called when the connection is closed by the remote peer. It notifies the content consumer about the connection closure.
/// </summary>
/// <param name="streamer">The <see cref="TCPStreamer"/> instance for which the connection is closed.</param>
void OnConnectionClosed(TCPStreamer streamer);
/// <summary>
/// Called when an error occurs during content processing or connection handling. It provides the <see cref="TCPStreamer"/> instance and the <see cref="Exception"/> that caused the error.
/// </summary>
/// <param name="streamer">The <see cref="TCPStreamer"/> instance where the error occurred.</param>
/// <param name="ex">The <see cref="Exception"/> that represents the error condition.</param>
void OnError(TCPStreamer streamer, Exception ex);
}
sealed class ReadState
{
public int minReceiveBufferSize;
public byte[] receiveBuffer = null;
public int isReceiving;
public long totalReceived;
public long bufferedLength;
public ConcurrentQueue<BufferSegment> bufferedSegments = new ConcurrentQueue<BufferSegment>();
}
sealed class WriteState
{
public byte[] _writeBuffer = null;
public int _writeInProgress;
public ConcurrentQueue<BufferSegment> _segmentsToWrite = new ConcurrentQueue<BufferSegment>();
public long bufferedLength;
public AutoResetEvent blockEvent = new AutoResetEvent(false);
}
/// <summary>
/// The TCPStreamer class is a versatile component that abstracts the complexities of TCP communication, making it easier to handle data streaming between networked applications or devices. It ensures reliable and efficient data transfer while handling various aspects of network communication and error management.
/// </summary>
/// <remarks>
/// <para>
/// TCPStreamer serves several key functions:
/// </para>
/// <list type="bullet">
/// <item>
/// <term>Data Streaming</term><description>It enables the streaming of data between two endpoints over a TCP connection, ideal for scenarios involving the transfer of large data volumes in manageable chunks.
/// </description></item>
/// <item>
/// <term>Buffer Management</term><description>The class efficiently manages buffering for both incoming and outgoing data, ensuring smooth and efficient data transfer.
/// </description></item>
/// <item>
/// <term>Asynchronous Communication</term><description>Utilizing asynchronous communication patterns, it supports non-blocking operations, essential for applications requiring concurrent data processing.
/// </description></item>
/// <item>
/// <term>Error Handling</term><description>Comprehensive error-handling mechanisms address exceptions that may occur during TCP communication, enhancing robustness in the face of network issues or errors.
/// </description></item>
/// <item>
/// <term>Resource Management</term><description>It handles memory buffer management and resource disposal when the TCP connection is closed or the class is disposed.
/// </description></item>
/// <item>
/// <term>Integration with Heartbeat</term><description>Implementing the <see cref="IHeartbeat"/> interface, it can be seamlessly integrated into systems using heartbeat mechanisms for network connection monitoring and management.
/// </description></item>
/// </list>
/// </remarks>
public sealed class TCPStreamer : IDisposable, IHeartbeat
{
/// <summary>
/// Gets or sets the content consumer that interacts with this <see cref="TCPStreamer"/> instance, allowing data to be written to the streamer for transmission.
/// </summary>
public ITCPStreamerContentConsumer ContentConsumer { get => this._contentConsumer; set { this._contentConsumer = value; } }
private ITCPStreamerContentConsumer _contentConsumer;
/// <summary>
/// Gets the underlying <see cref="Socket"/> associated with this <see cref="TCPStreamer"/> instance.
/// </summary>
public Socket Socket { get => this._socket; }
/// <summary>
/// Gets the optional <see cref="LoggingContext"/> associated with this <see cref="TCPStreamer"/> instance, facilitating logging and diagnostics.
/// </summary>
public LoggingContext Context { get => this._loggingContext; }
/// <summary>
/// Gets a value indicating whether the TCP connection is closed.
/// </summary>
public bool IsConnectionClosed { get => this._disposed || this._closed == 1 || this._closeInitiatedByServer; }
/// <summary>
/// Gets the minimum receive buffer size for the TCP socket.
/// </summary>
public int MinReceiveBufferSize { get => this.readState.minReceiveBufferSize; }
/// <summary>
/// Gets the total length of buffered data for reading from the stream.
/// </summary>
public long Length { get => this.readState.bufferedLength; }
/// <summary>
/// Gets or sets the maximum amount of buffered data allowed for writing to the stream.
/// </summary>
public readonly uint MaxBufferedWriteAmount;
private ReadState readState = new ReadState();
private WriteState writeState = new WriteState();
private Socket _socket;
private LoggingContext _loggingContext;
private bool _disposed;
private int _closed;
private int _isDisconnected;
public bool _closeInitiatedByServer;
/// <summary>
/// Gets or sets the maximum amount of buffered data allowed for reading from the stream.
/// </summary>
private uint MaxBufferedReadAmount;
/// <summary>
/// Initializes a new instance of the TCPStreamer class with the specified <see cref="System.Net.Sockets.Socket"/> and parent <see cref="LoggingContext"/>.
/// </summary>
/// <param name="socket">The underlying <see cref="System.Net.Sockets.Socket"/> representing the TCP connection.</param>
/// <param name="parentLoggingContext">The optional parent <see cref="LoggingContext"/> for logging and diagnostics.</param>
public TCPStreamer(Socket socket, uint maxReadBufferSize, uint maxWriteBufferSize, LoggingContext _parentLoggingContext)
{
this._socket = socket;
this.readState.minReceiveBufferSize = this._socket.ReceiveBufferSize;
this.MaxBufferedReadAmount = maxReadBufferSize;
this.MaxBufferedWriteAmount = maxWriteBufferSize;
this._loggingContext = new LoggingContext(this);
this._loggingContext.Add("Parent", _parentLoggingContext);
HTTPManager.Logger.Verbose(nameof(TCPStreamer), $"Created with minReceiveBufferSize: ({this.readState.minReceiveBufferSize:N0})", this._loggingContext);
BeginReceive();
HTTPManager.Heartbeats.Subscribe(this);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementCurrentConnections();
}
/// <summary>
/// Dequeues received data from the stream's buffer and returns a <see cref="BufferSegment"/> containing the data.
/// </summary>
/// <returns>A <see cref="BufferSegment"/> containing the received data.</returns>
public BufferSegment DequeueReceived()
{
if (this.readState.bufferedSegments.TryDequeue(out var segment))
{
Interlocked.Add(ref this.readState.bufferedLength, -segment.Count);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementReceivedAndUnprocessed(-segment.Count);
}
else
{
if (this.IsConnectionClosed)
return new BufferSegment(null, 0, -1);
}
BeginReceive();
return segment;
}
/// <summary>
/// Begins receiving data from the TCP connection asynchronously. This method ensures that only one receive operation happens at a time.
/// </summary>
/// <remarks>
/// When calling this method, it ensures that there is only one active receive operation at a time, preventing overlapping receives. This optimization helps prevent data loss and improves the reliability of the receive process.
/// </remarks>
public void BeginReceive()
{
var length = this.Length;
var receiving = this.readState.isReceiving;
if (!this.IsConnectionClosed && length < MaxBufferedReadAmount && (receiving = Interlocked.CompareExchange(ref this.readState.isReceiving, 1, 0)) == 0)
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(BeginReceive)}()", this._loggingContext);
#endif
var readBuffer = BufferPool.Get(this.readState.minReceiveBufferSize, true, this._loggingContext);
try
{
Interlocked.Exchange(ref this.readState.receiveBuffer, readBuffer);
this._socket.BeginReceive(
readBuffer, 0, readBuffer.Length,
SocketFlags.None,
OnReceived,
null);
}
catch (Exception e)
{
BufferPool.Release(Interlocked.Exchange(ref this.readState.receiveBuffer, null));
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(TCPStreamer), $"{nameof(this._socket.BeginReceive)}", e, this._loggingContext);
}
}
}
private void OnReceived(IAsyncResult asyncResult)
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnReceived)}()", this._loggingContext);
#endif
bool isClosed = true;
int readCount = 0;
SocketError errorCode = SocketError.Success;
try
{
var socket = this._socket;
isClosed = socket == null || !socket.Connected;
long newLength = this.Length;
if (socket != null)
{
// OnReceived might be still called when we closed&disposed the socket!
readCount = socket.EndReceive(asyncResult, out errorCode);
if (errorCode != SocketError.Success)
isClosed = true;
else
//isClosed = readCount <= 0 && !this._disposed && this._closed == 0;
isClosed = readCount <= 0 || this.IsConnectionClosed;
if (!isClosed)
{
newLength = Interlocked.Add(ref this.readState.bufferedLength, readCount);
this.readState.totalReceived += readCount;
}
}
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementTotalNetworkBytesReceived(readCount);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementReceivedAndUnprocessed(readCount);
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnReceived)}({readCount:N0}, {isClosed}, {errorCode}, {newLength:N0}, {this.readState.totalReceived:N0})", this._loggingContext);
#endif
if (!isClosed)
{
byte[] readBuffer = Interlocked.Exchange(ref this.readState.receiveBuffer, null);
this.readState.bufferedSegments.Enqueue(readBuffer.AsBuffer(readCount));
try
{
this.ContentConsumer?.OnContent(this);
}
catch (Exception e)
{
HTTPManager.Logger.Exception(nameof(TCPStreamer), "ContentConsumer.OnContent", e, this._loggingContext);
}
}
}
catch (Exception ex)
{
if (!Volatile.Read(ref this._disposed))
HTTPManager.Logger.Exception(nameof(TCPStreamer), $"{nameof(OnReceived)}({errorCode})", ex, this._loggingContext);
}
finally
{
if (!isClosed)
{
Interlocked.Exchange(ref this.readState.isReceiving, 0);
BeginReceive();
}
else
{
BufferPool.Release(Interlocked.Exchange(ref this.readState.receiveBuffer, null));
Interlocked.Exchange(ref this.readState.isReceiving, 0);
// Close must be called only when all data read, or initiated by the client too.
this._closeInitiatedByServer = true;
if (this._closed == 0)
this.writeState.blockEvent.Set();
try
{
var consumer = Interlocked.Exchange(ref this._contentConsumer, null);
if (consumer != null)
consumer?.OnConnectionClosed(this);
#if LOG_TCP_STREAMER
else if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Warning(nameof(TCPStreamer), $"{nameof(OnReceived)}({errorCode}) - No consumer to call OnConnectionClosed on!", this._loggingContext);
#endif
}
catch (Exception e)
{
HTTPManager.Logger.Exception(nameof(TCPStreamer), "ContentConsumer.OnConnectionClosed", e, this._loggingContext);
}
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(TCPStreamer), $"{nameof(OnReceived)}({errorCode}) - closed (readCount({readCount}) <= 0, or other issues), not calling BeginReceive", this._loggingContext);
#endif
}
}
}
/// <summary>
/// Enqueues data to be sent over the TCP connection. The data is added to the stream's outgoing buffer for transmission.
/// </summary>
/// <param name="buffer">The <see cref="BufferSegment"/> containing the data to be sent.</param>
public void EnqueueToSend(BufferSegment buffer)
{
if (buffer.Count <= 0)
{
BufferPool.Release(buffer);
return;
}
if (this._closeInitiatedByServer)
{
BufferPool.Release(buffer);
//throw new Exception("TCP connection closed by the server!");
return;
}
if (this.IsConnectionClosed)
{
BufferPool.Release(buffer);
//throw new Exception("TCP connection already closed!");
return;
}
try
{
long buffered = Interlocked.Add(ref this.writeState.bufferedLength, buffer.Count);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementBufferedToSend(buffer.Count);
this.writeState._segmentsToWrite.Enqueue(buffer);
bool allowedToSend = Interlocked.CompareExchange(ref this.writeState._writeInProgress, 1, 0) == 0;
if (!allowedToSend)
{
if (buffered >= MaxBufferedWriteAmount)
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(EnqueueToSend)} - Enqueued({buffer.Count:N0}) & blocking", this._loggingContext);
#endif
this.writeState.blockEvent.Reset();
this.writeState.blockEvent.WaitOne();
}
#if LOG_TCP_STREAMER
else if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(EnqueueToSend)} - Enqueued({buffer.Count:N0})", this._loggingContext);
#endif
}
else
{
if (!SendFromQueue())
Interlocked.Exchange(ref this.writeState._writeInProgress, 0);
}
}
catch
{
Interlocked.Exchange(ref this.writeState._writeInProgress, 0);
BufferPool.Release(Interlocked.Exchange(ref this.writeState._writeBuffer, null));
throw;
}
}
private bool SendFromQueue()
{
var socket = this._socket;
// TODO: merge buffers from the queue into a larger one, to send them at once
if (this.writeState._segmentsToWrite.TryDequeue(out var writeBuffer))
{
Interlocked.Exchange(ref this.writeState._writeBuffer, writeBuffer.Data);
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(SendFromQueue)} - BeginSend({writeBuffer.Count:N0})", this._loggingContext);
#endif
socket.BeginSend(writeBuffer.Data, writeBuffer.Offset, writeBuffer.Count, SocketFlags.None, OnWroteToNetwork, writeBuffer);
return true;
}
return false;
}
private void OnWroteToNetwork(IAsyncResult ar)
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)}()", this._loggingContext);
#endif
var writeBuffer = (BufferSegment)ar.AsyncState;
bool success = false;
try
{
var socket = this._socket;
if (this.IsConnectionClosed)
{
this.writeState.blockEvent.Set();
return;
}
int result = socket.EndSend(ar, out var errorCode);
success = result > 0;
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)} - OnWroteToNetwork({result:N0}, {errorCode})", this._loggingContext);
#endif
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementTotalNetworkBytesSent(result);
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementBufferedToSend(-result);
if (result > 0 && Interlocked.Add(ref this.writeState.bufferedLength, -result) < MaxBufferedWriteAmount)
this.writeState.blockEvent.Set();
if (writeBuffer.Count != result)
{
writeBuffer = writeBuffer.Data.AsBuffer(writeBuffer.Offset + result, writeBuffer.Count - result);
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)} - OnWroteToNetwork({result:N0})", this._loggingContext);
#endif
socket.BeginSend(writeBuffer.Data, writeBuffer.Offset, writeBuffer.Count, SocketFlags.None, OnWroteToNetwork, writeBuffer);
}
else
{
BufferPool.Release(Interlocked.Exchange(ref this.writeState._writeBuffer, null));
if (!SendFromQueue())
{
#if LOG_TCP_STREAMER
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)} - set _writeInProgress = 0", this._loggingContext);
#endif
Interlocked.Exchange(ref this.writeState._writeInProgress, 0);
}
}
}
catch (Exception ex)
{
BufferPool.Release(Interlocked.Exchange(ref this.writeState._writeBuffer, null));
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(TCPStreamer), $"{nameof(OnWroteToNetwork)}({ex.Message})", ex, this._loggingContext);
}
}
/// <summary>
/// Disposes of the <see cref="TCPStreamer"/> instance, releasing associated resources.
/// </summary>
public void Dispose()
{
// If not closed, close
if (Volatile.Read(ref this._closed) == 0)
{
this.Close();
// Close will trigger OnDisconnected that calls Dispose again, but this._closed will be set to 0 this time.
return;
}
// if not disposed, dispose
if (this._disposed)
return;
this._disposed = true;
this._socket?.Dispose();
this._socket = null;
GC.SuppressFinalize(this);
}
void IHeartbeat.OnHeartbeatUpdate(DateTime now, TimeSpan dif)
{
if (this.writeState._segmentsToWrite.Count > 0 && Interlocked.CompareExchange(ref this.writeState._writeInProgress, 1, 0) == 0)
{
if (!SendFromQueue())
Interlocked.Exchange(ref this.writeState._writeInProgress, 0);
}
}
/// <summary>
/// Closes the TCP connection gracefully and performs cleanup operations.
/// </summary>
internal void Close()
{
HTTPManager.Logger.Verbose(nameof(TCPStreamer), $"{nameof(Close)}({this._closed}, {this._socket?.Connected})", this._loggingContext);
if (Interlocked.CompareExchange(ref this._closed, 1, 0) == 1)
return;
try
{
this.writeState.blockEvent.Set();
//this._socket.Shutdown(SocketShutdown.Both);
this._socket.BeginDisconnect(false, OnDisconnected, null);
}
catch
{
OnDisconnected(null);
}
}
private void OnDisconnected(IAsyncResult ar)
{
// TODO: move cleanup code into a separate function and call it when both _writeInProgress & isReceiving are zero
HTTPManager.Logger.Verbose(nameof(TCPStreamer), $"{nameof(OnDisconnected)}()", this._loggingContext);
if (Interlocked.CompareExchange(ref this._isDisconnected, 1, 0) == 1)
return;
Best.HTTP.Profiler.Network.NetworkStatsCollector.DecrementCurrentConnections();
if (ar != null)
{
try
{
this._socket.EndDisconnect(ar);
}
catch { }
}
while (this.readState.bufferedSegments.TryDequeue(out var segment))
{
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementReceivedAndUnprocessed(-segment.Count);
BufferPool.Release(segment);
}
while (this.writeState._segmentsToWrite.TryDequeue(out var segment))
{
Best.HTTP.Profiler.Network.NetworkStatsCollector.IncrementBufferedToSend(-segment.Count);
BufferPool.Release(segment);
}
BufferPool.Release(Interlocked.Exchange(ref this.writeState._writeBuffer, null));
// Don't release the receiveBuffer until lower layer returns with the OnReceived callback because:
// the plugin would reuse it in other parts, while it's unknown what the lower layer is doing and it can
// decide to write into the buffer while we aren't expect it.
if (this.readState.isReceiving == 0)
BufferPool.Release(Interlocked.Exchange(ref this.readState.receiveBuffer, null));
// todo: maybe dispose only if this.writeState._writeInProgress == 0
// otherwise we could dispose it here, and try to use it
this.writeState.blockEvent.Dispose();
HTTPManager.Heartbeats.Unsubscribe(this);
try
{
Interlocked.Exchange(ref this._contentConsumer, null)
?.OnConnectionClosed(this);
}
catch { }
this.Dispose();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90ac6e42bdcbf9e4baa3675a6a2cad75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bc349b02f1bcbf840b2d9201e25322b5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Best.HTTP.Shared.PlatformSupport.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Text
{
/// <summary>
/// Implements pooling logic for <see cref="StringBuilder"/> instances.
/// </summary>
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
public static class StringBuilderPool
{
/// <summary>
/// Setting this property to false the pooling mechanism can be disabled.
/// </summary>
public static bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
// When set to non-enabled remove all stored entries
if (!_isEnabled)
Clear();
}
}
private static volatile bool _isEnabled = true;
/// <summary>
/// Buffer entries that released back to the pool and older than this value are moved when next maintenance is triggered.
/// </summary>
public static TimeSpan RemoveOlderThan = TimeSpan.FromSeconds(10);
/// <summary>
/// How often pool maintenance must run.
/// </summary>
public static TimeSpan RunMaintenanceEvery = TimeSpan.FromSeconds(5);
private static DateTime lastMaintenance = DateTime.MinValue;
private readonly static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
struct BuilderShelf
{
public StringBuilder builder;
public DateTime released;
public BuilderShelf(StringBuilder sb)
{
this.builder = sb;
this.released = DateTime.UtcNow;
}
}
private static List<BuilderShelf> pooledBuilders = new List<BuilderShelf>();
public static StringBuilder Get(int lengthHint)
{
if (!_isEnabled)
return new StringBuilder(lengthHint);
using (new WriteLock(rwLock))
{
for (int i = pooledBuilders.Count - 1; i >= 0; i--)
{
BuilderShelf shelf = pooledBuilders[i];
if (shelf.builder.Capacity >= lengthHint)
{
pooledBuilders.RemoveAt(i);
return shelf.builder;
}
}
// no builder found with lengthHint, take the first available
if (pooledBuilders.Count > 0)
{
BuilderShelf shelf = pooledBuilders[pooledBuilders.Count - 1];
pooledBuilders.RemoveAt(pooledBuilders.Count - 1);
return shelf.builder;
}
}
return new StringBuilder(lengthHint);
}
public static void Release(StringBuilder builder)
{
if (builder == null)
return;
if (!_isEnabled)
return;
builder.Clear();
using (new WriteLock(rwLock))
pooledBuilders.Add(new BuilderShelf(builder));
}
public static string ReleaseAndGrab(StringBuilder builder)
{
if (builder == null)
return null;
var result = builder.ToString();
if (!_isEnabled)
return result;
builder.Clear();
using (new WriteLock(rwLock))
pooledBuilders.Add(new BuilderShelf(builder));
return result;
}
internal static void Maintain()
{
DateTime now = DateTime.UtcNow;
if (!_isEnabled || lastMaintenance + RunMaintenanceEvery > now)
return;
lastMaintenance = now;
DateTime olderThan = now - RemoveOlderThan;
using (new WriteLock(rwLock))
{
for (int i = 0; i < pooledBuilders.Count; i++)
{
BuilderShelf shelf = pooledBuilders[i];
if (shelf.released < olderThan)
pooledBuilders.RemoveAt(i--);
}
}
}
public static void Clear()
{
using (new WriteLock(rwLock))
pooledBuilders.Clear();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 86c608e2d4d42a34283884f8e9eca2f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 44047629f354ef84ea121bd4303299d6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Threading
{
public static class CustomThreadPool
{
public static int BackgroundThreads => activeThreads;
private static int minThreads = 1;
private static int maxThreads = 128;
private static int activeThreads = 0;
private static int workingThreads = 0;
private static ConcurrentQueue<Action> jobQueue = new();
private static AutoResetEvent are = new (false);
//private static List<Thread> threads = new();
public static void SetMinMaxThreads(int min, int max)
{
minThreads = min;
maxThreads = max;
}
public static void QueueUserWorkItem(Action job)
{
jobQueue.Enqueue(job);
are.Set();
if (workingThreads >= (activeThreads * 0.7f) && activeThreads < maxThreads)
{
var id = Interlocked.Increment(ref activeThreads);
var thread = new Thread(ThreadFunc);
thread.Name = $"{nameof(CustomThreadPool)}({id})";
thread.Priority = ThreadPriority.AboveNormal;
thread.IsBackground = true;
thread.Start();
}
}
private static void ThreadFunc()
{
DateTime lastJobProcessed = DateTime.UtcNow;
using var _ = new ThreadedRunner.IncDecLongLiving(true);
try
{
var hasWork = false;
do
{
while (jobQueue.TryDequeue(out var job))
{
try
{
Interlocked.Increment(ref workingThreads);
lastJobProcessed = DateTime.UtcNow;
job?.Invoke();
}
catch (Exception e)
{
UnityEngine.Debug.LogException(e);
}
finally
{
Interlocked.Decrement(ref workingThreads);
}
}
hasWork = are.WaitOne(TimeSpan.FromSeconds(5));
} while (hasWork && DateTime.UtcNow - lastJobProcessed < TimeSpan.FromSeconds(60));
}
finally
{
Interlocked.Decrement(ref activeThreads);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c912644eb3bc48eaa2720bf5c63c5e8d
timeCreated: 1747129961

View File

@@ -0,0 +1,46 @@
using System;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Threading
{
public struct ReadLock : IDisposable
{
private ReaderWriterLockSlim rwLock;
private bool locked;
public ReadLock(ReaderWriterLockSlim rwLock)
{
this.rwLock = rwLock;
this.locked = this.rwLock.IsReadLockHeld;
if (!this.locked)
this.rwLock.EnterReadLock();
}
public void Dispose()
{
if (!this.locked)
this.rwLock.ExitReadLock();
}
}
public struct WriteLock : IDisposable
{
private ReaderWriterLockSlim rwLock;
private bool locked;
public WriteLock(ReaderWriterLockSlim rwLock)
{
this.rwLock = rwLock;
this.locked = rwLock.IsWriteLockHeld;
if (!locked)
this.rwLock.EnterWriteLock();
}
public void Dispose()
{
if (!locked)
this.rwLock.ExitWriteLock();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd25c801571339740b2046ea34732f17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,165 @@
#define _USE_CUSTOM_THREADPOOL
using System;
using System.Threading;
namespace Best.HTTP.Shared.PlatformSupport.Threading
{
[IL2CPP.Il2CppEagerStaticClassConstruction]
public static class ThreadedRunner
{
public static int ShortLivingThreads { get => _shortLivingThreads; }
private static int _shortLivingThreads;
public static int LongLivingThreads { get => _LongLivingThreads; }
private static int _LongLivingThreads;
public static void SetThreadName(string name)
{
try
{
System.Threading.Thread.CurrentThread.Name = name;
}
catch(Exception ex)
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Exception(nameof(ThreadedRunner), nameof(SetThreadName), ex);
}
}
public static void RunShortLiving<T>(Action<T> job, T param)
{
#if USE_CUSTOM_THREADPOOL
CustomThreadPool.QueueUserWorkItem(() =>
#else
ThreadPool.QueueUserWorkItem((state) =>
#endif
{
using var __ = new IncDecShortLiving(true);
job(param);
});
}
public static void RunShortLiving<T1, T2>(Action<T1, T2> job, T1 param1, T2 param2)
{
#if USE_CUSTOM_THREADPOOL
CustomThreadPool.QueueUserWorkItem(() =>
#else
ThreadPool.QueueUserWorkItem((state) =>
#endif
{
using var __ = new IncDecShortLiving(true);
job(param1, param2);
});
}
public static void RunShortLiving<T1, T2, T3>(Action<T1, T2, T3> job, T1 param1, T2 param2, T3 param3)
{
#if USE_CUSTOM_THREADPOOL
CustomThreadPool.QueueUserWorkItem(() =>
#else
ThreadPool.QueueUserWorkItem((state) =>
#endif
{
using var __ = new IncDecShortLiving(true);
job(param1, param2, param3);
});
}
public static void RunShortLiving<T1, T2, T3, T4>(Action<T1, T2, T3, T4> job, T1 param1, T2 param2, T3 param3, T4 param4)
{
#if USE_CUSTOM_THREADPOOL
CustomThreadPool.QueueUserWorkItem(() =>
#else
ThreadPool.QueueUserWorkItem((state) =>
#endif
{
using var __ = new IncDecShortLiving(true);
job(param1, param2, param3, param4);
});
}
public static void RunShortLiving(Action job)
{
#if USE_CUSTOM_THREADPOOL
CustomThreadPool.QueueUserWorkItem(() =>
#else
ThreadPool.QueueUserWorkItem((state) =>
#endif
{
using var __ = new IncDecShortLiving(true);
job();
});
}
public static Thread RunLongLiving(Action job)
{
var thread = new Thread(() =>
{
using var __ = new IncDecLongLiving(true);
job();
});
thread.IsBackground = true;
thread.Start();
return thread;
}
private static int maxLongLiving;
private static int maxShortLiving;
public static void StoreLongLivingThreadUsage(int count)
{
int current;
do
{
current = maxLongLiving;
} while (current < count && Interlocked.CompareExchange(ref maxLongLiving, count, current) != current);
}
public static int GetAndZeroLongLivingThreadUsage()
{
int current = maxLongLiving;
Interlocked.Exchange(ref maxLongLiving, 0);
return current;
}
public static int GetAndZeroShortLivingThreadUsage()
{
int current = maxShortLiving;
Interlocked.Exchange(ref maxShortLiving, 0);
return current;
}
public static void StoreShortLivingThreadUsage(int count)
{
int current;
do
{
current = maxShortLiving;
} while (current < count && Interlocked.CompareExchange(ref maxShortLiving, count, current) != current);
}
public struct IncDecShortLiving : IDisposable
{
public IncDecShortLiving(bool dummy) => Interlocked.Increment(ref ThreadedRunner._shortLivingThreads);
public void Dispose()
{
StoreShortLivingThreadUsage(ThreadedRunner._shortLivingThreads);
Interlocked.Decrement(ref ThreadedRunner._shortLivingThreads);
}
}
public struct IncDecLongLiving : IDisposable
{
public IncDecLongLiving(bool dummy) => Interlocked.Increment(ref ThreadedRunner._LongLivingThreads);
public void Dispose()
{
StoreLongLivingThreadUsage(ThreadedRunner._LongLivingThreads);
Interlocked.Decrement(ref ThreadedRunner._LongLivingThreads);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5b50a35890b2c64c9a36d487c2ca287
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: