This commit is contained in:
PC-20230316NUNE\Administrator
2024-02-01 19:06:51 +08:00
parent aa4d6c3ce2
commit 877dca3b43
7518 changed files with 653768 additions and 162059 deletions

View File

@@ -0,0 +1,110 @@
#if !BESTHTTP_DISABLE_SOCKETIO
namespace BestHTTP.SocketIO
{
/// <summary>
/// Possible event types on the transport level.
/// </summary>
public enum TransportEventTypes : int
{
Unknown = -1,
Open = 0,
Close = 1,
Ping = 2,
Pong = 3,
Message = 4,
Upgrade = 5,
Noop = 6
}
/// <summary>
/// Event types of the SocketIO protocol.
/// </summary>
public enum SocketIOEventTypes : int
{
Unknown = -1,
/// <summary>
/// Connect to a namespace, or we connected to a namespace
/// </summary>
Connect = 0,
/// <summary>
/// Disconnect a namespace, or we disconnected from a namespace.
/// </summary>
Disconnect = 1,
/// <summary>
/// A general event. The event's name is in the payload.
/// </summary>
Event = 2,
/// <summary>
/// Acknowledgment of an event.
/// </summary>
Ack = 3,
/// <summary>
/// Error sent by the server, or by the plugin
/// </summary>
Error = 4,
/// <summary>
/// A general event with binary attached to the packet. The event's name is in the payload.
/// </summary>
BinaryEvent = 5,
/// <summary>
/// Acknowledgment of a binary event.
/// </summary>
BinaryAck = 6
}
/// <summary>
/// Possible error codes that the SocketIO server can send.
/// </summary>
public enum SocketIOErrors
{
/// <summary>
/// Transport unknown
/// </summary>
UnknownTransport = 0,
/// <summary>
/// Session ID unknown
/// </summary>
UnknownSid = 1,
/// <summary>
/// Bad handshake method
/// </summary>
BadHandshakeMethod = 2,
/// <summary>
/// Bad request
/// </summary>
BadRequest = 3,
/// <summary>
/// Tried to access a forbidden resource
/// </summary>
Forbidden = 4,
/// <summary>
/// Plugin internal error!
/// </summary>
Internal = 5,
/// <summary>
/// Exceptions that caught by the plugin but raised in a user code.
/// </summary>
User = 6,
/// <summary>
/// A custom, server sent error, most probably from a Socket.IO middleware.
/// </summary>
Custom = 7,
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 94f15ba2f99e9e148869183ae648e7e6
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
#if !BESTHTTP_DISABLE_SOCKETIO
namespace BestHTTP.SocketIO
{
public sealed class Error
{
public SocketIOErrors Code { get; private set; }
public string Message { get; private set; }
public Error(SocketIOErrors code, string msg)
{
this.Code = code;
this.Message = msg;
}
public override string ToString()
{
return string.Format("Code: {0} Message: \"{1}\"", this.Code.ToString(), this.Message);
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 97e9578ca337d0f42a4145ade32a3a5c
folderAsset: yes
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,97 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System;
using System.Collections.Generic;
namespace BestHTTP.SocketIO.Events
{
public delegate void SocketIOCallback(Socket socket, Packet packet, params object[] args);
public delegate void SocketIOAckCallback(Socket socket, Packet packet, params object[] args);
/// <summary>
/// A class to describe an event, and its metadatas.
/// </summary>
internal sealed class EventDescriptor
{
#region Public Properties
/// <summary>
/// List of callback delegates.
/// </summary>
public List<SocketIOCallback> Callbacks { get; private set; }
/// <summary>
/// If this property is true, callbacks are removed automatically after the event dispatch.
/// </summary>
public bool OnlyOnce { get; private set; }
/// <summary>
/// If this property is true, the dispatching packet's Payload will be decoded using the Manager's Encoder.
/// </summary>
public bool AutoDecodePayload { get; private set; }
#endregion
/// <summary>
/// Cache an array on a hot-path.
/// </summary>
private SocketIOCallback[] CallbackArray;
/// <summary>
/// Constructor to create an EventDescriptor instance and set the meta-datas.
/// </summary>
public EventDescriptor(bool onlyOnce, bool autoDecodePayload, SocketIOCallback callback)
{
this.OnlyOnce = onlyOnce;
this.AutoDecodePayload = autoDecodePayload;
this.Callbacks = new List<SocketIOCallback>(1);
if (callback != null)
Callbacks.Add(callback);
}
/// <summary>
/// Will call the callback delegates with the given parameters and remove the callbacks if this descriptor marked with a true OnlyOnce property.
/// </summary>
public void Call(Socket socket, Packet packet, params object[] args)
{
int callbackCount = Callbacks.Count;
if (CallbackArray == null || CallbackArray.Length < callbackCount)
Array.Resize(ref CallbackArray, callbackCount);
// Copy the callback delegates to an array, because in one of the callbacks we can modify the list(by calling On/Once/Off in an event handler)
// This way we can prevent some strange bug
Callbacks.CopyTo(CallbackArray);
// Go through the delegates and call them
for (int i = 0; i < callbackCount; ++i)
{
try
{
// Call the delegate.
SocketIOCallback callback = CallbackArray[i];
if (callback!= null)
callback(socket, packet, args);
}
catch (Exception ex)
{
// Do not try to emit a new Error when we already tried to deliver an Error, possible causing a
// stack overflow
if (args == null || args.Length == 0 || !(args[0] is Error))
(socket as ISocket).EmitError(SocketIOErrors.User, ex.Message + " " + ex.StackTrace);
HTTPManager.Logger.Exception("EventDescriptor", "Call", ex);
}
// If these callbacks has to be called only once, remove them from the main list
if (this.OnlyOnce)
Callbacks.Remove(CallbackArray[i]);
// Don't keep any reference avoiding memory leaks
CallbackArray[i] = null;
}
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,48 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System;
namespace BestHTTP.SocketIO.Events
{
/// <summary>
/// Helper class to provide functions to an easy Enum->string conversation of the transport and SocketIO evenet types.
/// </summary>
public static class EventNames
{
public const string Connect = "connect";
public const string Disconnect = "disconnect";
public const string Event = "event";
public const string Ack = "ack";
public const string Error = "error";
public const string BinaryEvent = "binaryevent";
public const string BinaryAck = "binaryack";
private static string[] SocketIONames = new string[] { "unknown", "connect", "disconnect", "event", "ack", "error", "binaryevent", "binaryack" };
private static string[] TransportNames = new string[] { "unknown", "open", "close", "ping", "pong", "message", "upgrade", "noop" };
private static string[] BlacklistedEvents = new string[] { "connect", "connect_error", "connect_timeout", "disconnect", "error", "reconnect",
"reconnect_attempt", "reconnect_failed", "reconnect_error", "reconnecting" };
public static string GetNameFor(SocketIOEventTypes type)
{
return SocketIONames[(int)type + 1];
}
public static string GetNameFor(TransportEventTypes transEvent)
{
return TransportNames[(int)transEvent + 1];
}
/// <summary>
/// Checks an event name whether it's blacklisted or not.
/// </summary>
public static bool IsBlacklisted(string eventName)
{
for (int i = 0; i < BlacklistedEvents.Length; ++i)
if (string.Compare(BlacklistedEvents[i], eventName, StringComparison.OrdinalIgnoreCase) == 0)
return true;
return false;
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 3ab78cfee54d71543bec1cdef098cd4b
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,161 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System.Collections.Generic;
namespace BestHTTP.SocketIO.Events
{
/// <summary>
/// This class helps keep track and maintain EventDescriptor instances and dispatching packets to the right delegates.
/// </summary>
internal sealed class EventTable
{
#region Privates
/// <summary>
/// The Socket that this EventTable is bound to.
/// </summary>
private Socket Socket { get; set; }
/// <summary>
/// The 'EventName -> List of events' mapping.
/// </summary>
private Dictionary<string, List<EventDescriptor>> Table = new Dictionary<string, List<EventDescriptor>>();
#endregion
/// <summary>
/// Constructor to create an instance and bind it to a socket.
/// </summary>
public EventTable(Socket socket)
{
this.Socket = socket;
}
/// <summary>
/// Register a callback to a name with the given metadata.
/// </summary>
public void Register(string eventName, SocketIOCallback callback, bool onlyOnce, bool autoDecodePayload)
{
List<EventDescriptor> events;
if (!Table.TryGetValue(eventName, out events))
Table.Add(eventName, events = new List<EventDescriptor>(1));
// Find a matching descriptor
var desc = events.Find((d) => d.OnlyOnce == onlyOnce && d.AutoDecodePayload == autoDecodePayload);
// If not found, create one
if (desc == null)
events.Add(new EventDescriptor(onlyOnce, autoDecodePayload, callback));
else // if found, add the new callback
desc.Callbacks.Add(callback);
}
/// <summary>
/// Removes all events that registered for the given name.
/// </summary>
public void Unregister(string eventName)
{
Table.Remove(eventName);
}
/// <summary>
///
/// </summary>
public void Unregister(string eventName, SocketIOCallback callback)
{
List<EventDescriptor> events;
if (Table.TryGetValue(eventName, out events))
for (int i = 0; i < events.Count; ++i)
events[i].Callbacks.Remove(callback);
}
/// <summary>
/// Will call the delegates that associated to the given eventName.
/// </summary>
public void Call(string eventName, Packet packet, params object[] args)
{
List<EventDescriptor> events;
if (Table.TryGetValue(eventName, out events))
{
if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
HTTPManager.Logger.Verbose("EventTable", string.Format("Call - {0} ({1})", eventName, events.Count));
for (int i = 0; i < events.Count; ++i)
events[i].Call(Socket, packet, args);
}
else
{
if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
HTTPManager.Logger.Verbose("EventTable", string.Format("Call - {0} (0)", eventName));
}
}
/// <summary>
/// This function will get the eventName from the packet's Payload, and optionally will decode it from Json.
/// </summary>
public void Call(Packet packet)
{
string eventName = packet.DecodeEventName();
string typeName = packet.SocketIOEvent != SocketIOEventTypes.Unknown ? EventNames.GetNameFor(packet.SocketIOEvent) : EventNames.GetNameFor(packet.TransportEvent);
object[] args = null;
if (!HasSubsciber(eventName) && !HasSubsciber(typeName))
return;
// If this is an Event or BinaryEvent message, or we have a subscriber with AutoDecodePayload, then
// we have to decode the packet's Payload.
if (packet.TransportEvent == TransportEventTypes.Message && (packet.SocketIOEvent == SocketIOEventTypes.Event || packet.SocketIOEvent == SocketIOEventTypes.BinaryEvent) && ShouldDecodePayload(eventName))
args = packet.Decode(Socket.Manager.Encoder);
// call event callbacks registered for 'eventName'
if (!string.IsNullOrEmpty(eventName))
Call(eventName, packet, args);
if (!packet.IsDecoded && ShouldDecodePayload(typeName))
args = packet.Decode(Socket.Manager.Encoder);
// call event callbacks registered for 'typeName'
if (!string.IsNullOrEmpty(typeName))
Call(typeName, packet, args);
}
/// <summary>
/// Remove all event -> delegate association.
/// </summary>
public void Clear()
{
Table.Clear();
}
#region Private Helpers
/// <summary>
/// Returns true, if for the given event name there are at least one event that needs a decoded
/// </summary>
/// <param name="eventName"></param>
/// <returns></returns>
private bool ShouldDecodePayload(string eventName)
{
List<EventDescriptor> events;
// If we find at least one EventDescriptor with AutoDecodePayload == true, we have to
// decode the whole payload
if (Table.TryGetValue(eventName, out events))
for (int i = 0; i < events.Count; ++i)
if (events[i].AutoDecodePayload && events[i].Callbacks.Count > 0)
return true;
return false;
}
private bool HasSubsciber(string eventName)
{
return Table.ContainsKey(eventName);
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 62b1e6227e8b13446b06cf83b7ab23e2
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,101 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System;
using System.Collections.Generic;
namespace BestHTTP.SocketIO
{
using BestHTTP.JSON;
/// <summary>
/// Helper class to parse and hold handshake information.
/// </summary>
public sealed class HandshakeData
{
#region Public Handshake Data
/// <summary>
/// Session ID of this connection.
/// </summary>
public string Sid { get; private set; }
/// <summary>
/// List of possible upgrades.
/// </summary>
public List<string> Upgrades { get; private set; }
/// <summary>
/// What interval we have to set a ping message.
/// </summary>
public TimeSpan PingInterval { get; private set; }
/// <summary>
/// What time have to pass without an answer to our ping request when we can consider the connection disconnected.
/// </summary>
public TimeSpan PingTimeout { get; private set; }
#endregion
#region Helper Methods
public bool Parse(string str)
{
bool success = false;
Dictionary<string, object> dict = Json.Decode(str, ref success) as Dictionary<string, object>;
if (!success)
return false;
try
{
this.Sid = GetString(dict, "sid");
this.Upgrades = GetStringList(dict, "upgrades");
this.PingInterval = TimeSpan.FromMilliseconds(GetInt(dict, "pingInterval"));
this.PingTimeout = TimeSpan.FromMilliseconds(GetInt(dict, "pingTimeout"));
}
catch (Exception ex)
{
BestHTTP.HTTPManager.Logger.Exception("HandshakeData", "Parse", ex);
return false;
}
return true;
}
private static object Get(Dictionary<string, object> from, string key)
{
object value;
if (!from.TryGetValue(key, out value))
throw new System.Exception(string.Format("Can't get {0} from Handshake data!", key));
return value;
}
private static string GetString(Dictionary<string, object> from, string key)
{
return Get(from, key) as string;
}
private static List<string> GetStringList(Dictionary<string, object> from, string key)
{
List<object> value = Get(from, key) as List<object>;
List<string> result = new List<string>(value.Count);
for (int i = 0; i < value.Count; ++i)
{
string str = value[i] as string;
if (str != null)
result.Add(str);
}
return result;
}
private static int GetInt(Dictionary<string, object> from, string key)
{
return (int)(double)Get(from, key);
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 28457ee0deb7f0e4e8477f231304c601
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,40 @@
#if !BESTHTTP_DISABLE_SOCKETIO
namespace BestHTTP.SocketIO
{
using BestHTTP.SocketIO.Transports;
/// <summary>
/// Interface to hide internal functions from the user by implementing it as an explicit interface.
/// </summary>
public interface IManager
{
void Remove(Socket socket);
void Close(bool removeSockets = true);
void TryToReconnect();
bool OnTransportConnected(ITransport transport);
void OnTransportError(ITransport trans, string err);
void OnTransportProbed(ITransport trans);
void SendPacket(Packet packet);
void OnPacket(Packet packet);
void EmitEvent(string eventName, params object[] args);
void EmitEvent(SocketIOEventTypes type, params object[] args);
void EmitError(SocketIOErrors errCode, string msg);
void EmitAll(string eventName, params object[] args);
}
/// <summary>
/// Interface to hide internal functions from the user by implementing it as an explicit interface.
/// </summary>
public interface ISocket
{
void Open();
void Disconnect(bool remove);
void OnPacket(Packet packet);
void EmitEvent(SocketIOEventTypes type, params object[] args);
void EmitEvent(string eventName, params object[] args);
void EmitError(SocketIOErrors errCode, string msg);
}
}
#endif

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ed82682a337846d47b38e6ca7189a0c8
folderAsset: yes
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System.Collections.Generic;
using BestHTTP.JSON;
namespace BestHTTP.SocketIO.JsonEncoders
{
/// <summary>
/// The default IJsonEncoder implementation. It's uses the Json class from the BestHTTP.JSON namespace to encode and decode.
/// </summary>
public sealed class DefaultJSonEncoder : IJsonEncoder
{
public List<object> Decode(string json)
{
return Json.Decode(json) as List<object>;
}
public string Encode(List<object> obj)
{
return Json.Encode(obj);
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 692eedd1d08951b4fac2cd4447430cf7
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System.Collections.Generic;
namespace BestHTTP.SocketIO.JsonEncoders
{
/// <summary>
/// Interface to be able to write custom Json encoders/decoders.
/// </summary>
public interface IJsonEncoder
{
/// <summary>
/// The Decode function must create a list of objects from the Json formatted string parameter. If the decoding fails, it should return null.
/// </summary>
List<object> Decode(string json);
/// <summary>
/// The Encode function must create a json formatted string from the parameter. If the encoding fails, it should return null.
/// </summary>
string Encode(List<object> obj);
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 68459b55b5787e04186d568f4ec8bbc3
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,611 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System.Text;
namespace BestHTTP.SocketIO
{
using System;
using System.Collections.Generic;
using BestHTTP.JSON;
public sealed class Packet
{
private enum PayloadTypes : byte
{
Textual = 0,
Binary = 1
}
public const string Placeholder = "_placeholder";
#region Public properties
/// <summary>
/// Event type of this packet on the transport layer.
/// </summary>
public TransportEventTypes TransportEvent { get; private set; }
/// <summary>
/// The packet's type in the Socket.IO protocol.
/// </summary>
public SocketIOEventTypes SocketIOEvent { get; private set; }
/// <summary>
/// How many attachment should have this packet.
/// </summary>
public int AttachmentCount { get; private set; }
/// <summary>
/// The internal ack-id of this packet.
/// </summary>
public int Id { get; private set; }
/// <summary>
/// The sender namespace's name.
/// </summary>
public string Namespace { get; private set; }
/// <summary>
/// The payload as a Json string.
/// </summary>
public string Payload { get; private set; }
/// <summary>
/// The decoded event name from the payload string.
/// </summary>
public string EventName { get; private set; }
/// <summary>
/// All binary data attached to this event.
/// </summary>
public List<byte[]> Attachments { get { return attachments; } set { attachments = value; AttachmentCount = attachments != null ? attachments.Count : 0; } }
private List<byte[]> attachments;
/// <summary>
/// Property to check whether all attachments are received to this packet.
/// </summary>
public bool HasAllAttachment { get { return Attachments != null && Attachments.Count == AttachmentCount; } }
/// <summary>
/// True if it's already decoded. The DecodedArgs still can be null after the Decode call.
/// </summary>
public bool IsDecoded { get; private set; }
/// <summary>
/// The decoded arguments from the result of a Json string -> c# object convert.
/// </summary>
public object[] DecodedArgs { get; private set; }
#endregion
#region Constructors
/// <summary>
/// Internal constructor. Don't use it directly!
/// </summary>
internal Packet()
{
this.TransportEvent = TransportEventTypes.Unknown;
this.SocketIOEvent = SocketIOEventTypes.Unknown;
this.Payload = string.Empty;
}
/// <summary>
/// Internal constructor. Don't use it directly!
/// </summary>
internal Packet(string from)
{
this.Parse(from);
}
/// <summary>
/// Internal constructor. Don't use it directly!
/// </summary>
public Packet(TransportEventTypes transportEvent, SocketIOEventTypes packetType, string nsp, string payload, int attachment = 0, int id = 0)
{
this.TransportEvent = transportEvent;
this.SocketIOEvent = packetType;
this.Namespace = nsp;
this.Payload = payload;
this.AttachmentCount = attachment;
this.Id = id;
}
#endregion
#region Public Functions
public object[] Decode(BestHTTP.SocketIO.JsonEncoders.IJsonEncoder encoder)
{
if (IsDecoded || encoder == null)
return DecodedArgs;
IsDecoded = true;
if (string.IsNullOrEmpty(Payload))
return DecodedArgs;
List<object> decoded = encoder.Decode(Payload);
if (decoded != null && decoded.Count > 0)
{
if (this.SocketIOEvent == SocketIOEventTypes.Ack || this.SocketIOEvent == SocketIOEventTypes.BinaryAck)
DecodedArgs = decoded.ToArray();
else
{
decoded.RemoveAt(0);
DecodedArgs = decoded.ToArray();
}
}
return DecodedArgs;
}
/// <summary>
/// Will set and return with the EventName from the packet's Payload string.
/// </summary>
public string DecodeEventName()
{
// Already decoded
if (!string.IsNullOrEmpty(EventName))
return EventName;
// No Payload to decode
if (string.IsNullOrEmpty(Payload))
return string.Empty;
// Not array encoded, we can't decode
if (Payload[0] != '[')
return string.Empty;
int idx = 1;
// Search for the string-begin mark( ' or " chars)
while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'')
idx++;
// Reached the end of the string
if (Payload.Length <= idx)
return string.Empty;
int startIdx = ++idx;
// Search for the trailing mark of the string
while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'')
idx++;
// Reached the end of the string
if (Payload.Length <= idx)
return string.Empty;
return EventName = Payload.Substring(startIdx, idx - startIdx);
}
public string RemoveEventName(bool removeArrayMarks)
{
// No Payload to decode
if (string.IsNullOrEmpty(Payload))
return string.Empty;
// Not array encoded, we can't decode
if (Payload[0] != '[')
return string.Empty;
int idx = 1;
// Search for the string-begin mark( ' or " chars)
while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'')
idx++;
// Reached the end of the string
if (Payload.Length <= idx)
return string.Empty;
int startIdx = idx;
// Search for end of first element, or end of the array marks
while (Payload.Length > idx && Payload[idx] != ',' && Payload[idx] != ']')
idx++;
// Reached the end of the string
if (Payload.Length <= ++idx)
return string.Empty;
string payload = Payload.Remove(startIdx, idx - startIdx);
if (removeArrayMarks)
payload = payload.Substring(1, payload.Length - 2);
return payload;
}
/// <summary>
/// Will switch the "{'_placeholder':true,'num':X}" to a the index num X.
/// </summary>
/// <returns>True if successfully reconstructed, false otherwise.</returns>
public bool ReconstructAttachmentAsIndex()
{
//"452-["multiImage",{"image":true,"buffer1":{"_placeholder":true,"num":0},"buffer2":{"_placeholder":true,"num":1}}]"
return PlaceholderReplacer((json, obj) =>
{
int idx = Convert.ToInt32(obj["num"]);
this.Payload = this.Payload.Replace(json, idx.ToString());
this.IsDecoded = false;
});
}
/// <summary>
/// Will switch the "{'_placeholder':true,'num':X}" to a the data as a base64 encoded string.
/// </summary>
/// <returns>True if successfully reconstructed, false otherwise.</returns>
public bool ReconstructAttachmentAsBase64()
{
//"452-["multiImage",{"image":true,"buffer1":{"_placeholder":true,"num":0},"buffer2":{"_placeholder":true,"num":1}}]"
if (!HasAllAttachment)
return false;
return PlaceholderReplacer((json, obj) =>
{
int idx = Convert.ToInt32(obj["num"]);
this.Payload = this.Payload.Replace(json, string.Format("\"{0}\"", Convert.ToBase64String(this.Attachments[idx])));
this.IsDecoded = false;
});
}
#endregion
#region Internal Functions
/// <summary>
/// Parse the packet from a server sent textual data. The Payload will be the raw json string.
/// </summary>
internal void Parse(string from)
{
int idx = 0;
this.TransportEvent = (TransportEventTypes)ToInt(from[idx++]);
if (from.Length > idx && ToInt(from[idx]) >= 0)
this.SocketIOEvent = (SocketIOEventTypes)ToInt(from[idx++]);
else
this.SocketIOEvent = SocketIOEventTypes.Unknown;
// Parse Attachment
if (this.SocketIOEvent == SocketIOEventTypes.BinaryEvent || this.SocketIOEvent == SocketIOEventTypes.BinaryAck)
{
int endIdx = from.IndexOf('-', idx);
if (endIdx == -1)
endIdx = from.Length;
int attachment = 0;
int.TryParse(from.Substring(idx, endIdx - idx), out attachment);
this.AttachmentCount = attachment;
idx = endIdx + 1;
}
// Parse Namespace
if (from.Length > idx && from[idx] == '/')
{
int endIdx = from.IndexOf(',', idx);
if (endIdx == -1)
endIdx = from.Length;
this.Namespace = from.Substring(idx, endIdx - idx);
idx = endIdx + 1;
}
else
this.Namespace = "/";
// Parse Id
if (from.Length > idx && ToInt(from[idx]) >= 0)
{
int startIdx = idx++;
while (from.Length > idx && ToInt(from[idx]) >= 0)
idx++;
int id = 0;
int.TryParse(from.Substring(startIdx, idx - startIdx), out id);
this.Id = id;
}
// What left is the payload data
if (from.Length > idx)
this.Payload = from.Substring(idx);
else
this.Payload = string.Empty;
}
/// <summary>
/// Custom function instead of char.GetNumericValue, as it throws an error under WebGL using the new 4.x runtime.
/// It will return the value of the char if it's a numeric one, otherwise -1.
/// </summary>
private int ToInt(char ch)
{
int charValue = Convert.ToInt32(ch);
int num = charValue - '0';
if (num < 0 || num > 9)
return -1;
return num;
}
/// <summary>
/// Encodes this packet to a Socket.IO formatted string.
/// </summary>
internal string Encode()
{
StringBuilder builder = new StringBuilder();
// Set to Message if not set, and we are sending attachments
if (this.TransportEvent == TransportEventTypes.Unknown && this.AttachmentCount > 0)
this.TransportEvent = TransportEventTypes.Message;
if (this.TransportEvent != TransportEventTypes.Unknown)
builder.Append(((int)this.TransportEvent).ToString());
// Set to BinaryEvent if not set, and we are sending attachments
if (this.SocketIOEvent == SocketIOEventTypes.Unknown && this.AttachmentCount > 0)
this.SocketIOEvent = SocketIOEventTypes.BinaryEvent;
if (this.SocketIOEvent != SocketIOEventTypes.Unknown)
builder.Append(((int)this.SocketIOEvent).ToString());
if (this.SocketIOEvent == SocketIOEventTypes.BinaryEvent || this.SocketIOEvent == SocketIOEventTypes.BinaryAck)
{
builder.Append(this.AttachmentCount.ToString());
builder.Append("-");
}
// Add the namespace. If there is any other then the root nsp ("/")
// then we have to add a trailing "," if we have more data.
bool nspAdded = false;
if (this.Namespace != "/")
{
builder.Append(this.Namespace);
nspAdded = true;
}
// ack id, if any
if (this.Id != 0)
{
if (nspAdded)
{
builder.Append(",");
nspAdded = false;
}
builder.Append(this.Id.ToString());
}
// payload
if (!string.IsNullOrEmpty(this.Payload))
{
if (nspAdded)
{
builder.Append(",");
nspAdded = false;
}
builder.Append(this.Payload);
}
return builder.ToString();
}
/// <summary>
/// Encodes this packet to a Socket.IO formatted byte array.
/// </summary>
internal byte[] EncodeBinary()
{
if (AttachmentCount != 0 || (Attachments != null && Attachments.Count != 0))
{
if (Attachments == null)
throw new ArgumentException("packet.Attachments are null!");
if (AttachmentCount != Attachments.Count)
throw new ArgumentException("packet.AttachmentCount != packet.Attachments.Count. Use the packet.AddAttachment function to add data to a packet!");
}
// Encode it as usual
string encoded = Encode();
// Convert it to a byte[]
byte[] payload = Encoding.UTF8.GetBytes(encoded);
// Encode it to a message
byte[] buffer = EncodeData(payload, PayloadTypes.Textual, null);
// If there is any attachment, convert them too, and append them after each other
if (AttachmentCount != 0)
{
int idx = buffer.Length;
// List to temporarily hold the converted attachments
List<byte[]> attachmentDatas = new List<byte[]>(AttachmentCount);
// The sum size of the converted attachments to be able to resize our buffer only once. This way we can avoid some GC garbage
int attachmentDataSize = 0;
// Encode our attachments, and store them in our list
for (int i = 0; i < AttachmentCount; i++)
{
byte[] tmpBuff = EncodeData(Attachments[i], PayloadTypes.Binary, new byte[] { 4 });
attachmentDatas.Add(tmpBuff);
attachmentDataSize += tmpBuff.Length;
}
// Resize our buffer once
Array.Resize(ref buffer, buffer.Length + attachmentDataSize);
// And copy all data into it
for (int i = 0; i < AttachmentCount; ++i)
{
byte[] data = attachmentDatas[i];
Array.Copy(data, 0, buffer, idx, data.Length);
idx += data.Length;
}
}
// Return the buffer
return buffer;
}
/// <summary>
/// Will add the byte[] that the server sent to the attachments list.
/// </summary>
internal void AddAttachmentFromServer(byte[] data, bool copyFull)
{
if (data == null || data.Length == 0)
return;
if (this.attachments == null)
this.attachments = new List<byte[]>(this.AttachmentCount);
if (copyFull)
this.Attachments.Add(data);
else
{
byte[] buff = new byte[data.Length - 1];
Array.Copy(data, 1, buff, 0, data.Length - 1);
this.Attachments.Add(buff);
}
}
#endregion
#region Private Helper Functions
/// <summary>
/// Encodes a byte array to a Socket.IO binary encoded message
/// </summary>
private byte[] EncodeData(byte[] data, PayloadTypes type, byte[] afterHeaderData)
{
// Packet binary encoding:
// [ 0|1 ][ length of data ][ FF ][data]
// <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number 255><data>
// Get the length of the payload. Socket.IO uses a wasteful encoding to send the length of the data.
// If the data is 16 bytes we have to send the length as two bytes: byte value of the character '1' and byte value of the character '6'.
// Instead of just one byte: 0xF. If the payload is 123 bytes, we can't send as 0x7B...
int afterHeaderLength = (afterHeaderData != null ? afterHeaderData.Length : 0);
string lenStr = (data.Length + afterHeaderLength).ToString();
byte[] len = new byte[lenStr.Length];
for (int cv = 0; cv < lenStr.Length; ++cv)
len[cv] = (byte)char.GetNumericValue(lenStr[cv]);
// We need another buffer to store the final data
byte[] buffer = new byte[data.Length + len.Length + 2 + afterHeaderLength];
// The payload is textual -> 0
buffer[0] = (byte)type;
// Copy the length of the data
for (int cv = 0; cv < len.Length; ++cv)
buffer[1 + cv] = len[cv];
int idx = 1 + len.Length;
// End of the header data
buffer[idx++] = 0xFF;
if (afterHeaderData != null && afterHeaderData.Length > 0)
{
Array.Copy(afterHeaderData, 0, buffer, idx, afterHeaderData.Length);
idx += afterHeaderData.Length;
}
// Copy our payload data to the buffer
Array.Copy(data, 0, buffer, idx, data.Length);
return buffer;
}
/// <summary>
/// Searches for the "{'_placeholder':true,'num':X}" string, and will call the given action to modify the PayLoad
/// </summary>
private bool PlaceholderReplacer(Action<string, Dictionary<string, object>> onFound)
{
if (string.IsNullOrEmpty(this.Payload))
return false;
// Find the first index of the "_placeholder" str
int placeholderIdx = this.Payload.IndexOf(Placeholder);
while (placeholderIdx >= 0)
{
// Find the object-start token
int startIdx = placeholderIdx;
while (this.Payload[startIdx] != '{')
startIdx--;
// Find the object-end token
int endIdx = placeholderIdx;
while (this.Payload.Length > endIdx && this.Payload[endIdx] != '}')
endIdx++;
// We reached the end
if (this.Payload.Length <= endIdx)
return false;
// Get the object, and decode it
string placeholderJson = this.Payload.Substring(startIdx, endIdx - startIdx + 1);
bool success = false;
Dictionary<string, object> obj = Json.Decode(placeholderJson, ref success) as Dictionary<string, object>;
if (!success)
return false;
// Check for presence and value of _placeholder
object value;
if (!obj.TryGetValue(Placeholder, out value) ||
!(bool)value)
return false;
// Check for presence of num
if (!obj.TryGetValue("num", out value))
return false;
// Let do, what we have to do
onFound(placeholderJson, obj);
// Find the next attachment if there is any
placeholderIdx = this.Payload.IndexOf(Placeholder);
}
return true;
}
#endregion
#region Overrides and Interface Implementations
/// <summary>
/// Returns with the Payload of this packet.
/// </summary>
public override string ToString()
{
return this.Payload;
}
/// <summary>
/// Will clone this packet to an identical packet instance.
/// </summary>
internal Packet Clone()
{
Packet packet = new Packet(this.TransportEvent, this.SocketIOEvent, this.Namespace, this.Payload, 0, this.Id);
packet.EventName = this.EventName;
packet.AttachmentCount = this.AttachmentCount;
packet.attachments = this.attachments;
return packet;
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 907cf4e3f52e02c409084c624e734c7c
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,508 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System;
using System.Collections.Generic;
namespace BestHTTP.SocketIO
{
using BestHTTP;
using BestHTTP.SocketIO.Events;
/// <summary>
/// This class represents a Socket.IO namespace.
/// </summary>
public sealed class Socket : ISocket
{
#region Public Properties
/// <summary>
/// The SocketManager instance that created this socket.
/// </summary>
public SocketManager Manager { get; private set; }
/// <summary>
/// The namespace that this socket is bound to.
/// </summary>
public string Namespace { get; private set; }
/// <summary>
/// Unique Id of the socket.
/// </summary>
public string Id { get; private set; }
/// <summary>
/// True if the socket is connected and open to the server. False otherwise.
/// </summary>
public bool IsOpen { get; private set; }
/// <summary>
/// While this property is True, the socket will decode the Packet's Payload data using the parent SocketManager's Encoder. You must set this property before any event subscription! Its default value is True;
/// </summary>
public bool AutoDecodePayload { get; set; }
#endregion
#region Privates
/// <summary>
/// A table to store acknowledgment callbacks associated to the given ids.
/// </summary>
private Dictionary<int, SocketIOAckCallback> AckCallbacks;
/// <summary>
/// Tha callback table that helps this class to manage event subscription and dispatching events.
/// </summary>
private EventTable EventCallbacks;
/// <summary>
/// Cached list to spare some GC alloc.
/// </summary>
private List<object> arguments = new List<object>();
#endregion
/// <summary>
/// Internal constructor.
/// </summary>
internal Socket(string nsp, SocketManager manager)
{
this.Namespace = nsp;
this.Manager = manager;
this.IsOpen = false;
this.AutoDecodePayload = true;
this.EventCallbacks = new EventTable(this);
}
#region Socket Handling
/// <summary>
/// Internal function to start opening the socket.
/// </summary>
void ISocket.Open()
{
HTTPManager.Logger.Information("Socket", string.Format("Open - Manager.State = {0}", Manager.State));
// The transport already established the connection
if (Manager.State == SocketManager.States.Open)
OnTransportOpen();
else if (Manager.Options.AutoConnect && Manager.State == SocketManager.States.Initial)
Manager.Open();
}
/// <summary>
/// Disconnects this socket/namespace.
/// </summary>
public void Disconnect()
{
(this as ISocket).Disconnect(true);
}
/// <summary>
/// Disconnects this socket/namespace.
/// </summary>
void ISocket.Disconnect(bool remove)
{
// Send a disconnect packet to the server
if (IsOpen)
{
Packet packet = new Packet(TransportEventTypes.Message, SocketIOEventTypes.Disconnect, this.Namespace, string.Empty);
(Manager as IManager).SendPacket(packet);
// IsOpen must be false, because in the OnPacket preprocessing the packet would call this function again
IsOpen = false;
(this as ISocket).OnPacket(packet);
}
if (AckCallbacks != null)
AckCallbacks.Clear();
if (remove)
{
EventCallbacks.Clear();
(Manager as IManager).Remove(this);
}
}
#endregion
#region Emit Implementations
public Socket Emit(string eventName, params object[] args)
{
return Emit(eventName, null, args);
}
public Socket Emit(string eventName, SocketIOAckCallback callback, params object[] args)
{
bool blackListed = EventNames.IsBlacklisted(eventName);
if (blackListed)
throw new ArgumentException("Blacklisted event: " + eventName);
arguments.Clear();
arguments.Add(eventName);
// Find and swap any binary data(byte[]) to a placeholder string.
// Server side these will be swapped back.
List<byte[]> attachments = null;
if (args != null && args.Length > 0)
{
int idx = 0;
for (int i = 0; i < args.Length; ++i)
{
byte[] binData = args[i] as byte[];
if (binData != null)
{
if (attachments == null)
attachments = new List<byte[]>();
Dictionary<string, object> placeholderObj = new Dictionary<string, object>(2);
placeholderObj.Add(Packet.Placeholder, true);
placeholderObj.Add("num", idx++);
arguments.Add(placeholderObj);
attachments.Add(binData);
}
else
arguments.Add(args[i]);
}
}
string payload = null;
try
{
payload = Manager.Encoder.Encode(arguments);
}
catch(Exception ex)
{
(this as ISocket).EmitError(SocketIOErrors.Internal, "Error while encoding payload: " + ex.Message + " " + ex.StackTrace);
return this;
}
// We don't use it further in this function, so we can clear it to not hold any unwanted reference.
arguments.Clear();
if (payload == null)
throw new ArgumentException("Encoding the arguments to JSON failed!");
int id = 0;
if (callback != null)
{
id = Manager.NextAckId;
if (AckCallbacks == null)
AckCallbacks = new Dictionary<int, SocketIOAckCallback>();
AckCallbacks[id] = callback;
}
Packet packet = new Packet(TransportEventTypes.Message,
attachments == null ? SocketIOEventTypes.Event : SocketIOEventTypes.BinaryEvent,
this.Namespace,
payload,
0,
id);
if (attachments != null)
packet.Attachments = attachments; // This will set the AttachmentCount property too.
(Manager as IManager).SendPacket(packet);
return this;
}
public Socket EmitAck(Packet originalPacket, params object[] args)
{
if (originalPacket == null)
throw new ArgumentNullException("originalPacket == null!");
if (/*originalPacket.Id == 0 ||*/
(originalPacket.SocketIOEvent != SocketIOEventTypes.Event && originalPacket.SocketIOEvent != SocketIOEventTypes.BinaryEvent))
throw new ArgumentException("Wrong packet - you can't send an Ack for a packet with id == 0 and SocketIOEvent != Event or SocketIOEvent != BinaryEvent!");
arguments.Clear();
if (args != null && args.Length > 0)
arguments.AddRange(args);
string payload = null;
try
{
payload = Manager.Encoder.Encode(arguments);
}
catch (Exception ex)
{
(this as ISocket).EmitError(SocketIOErrors.Internal, "Error while encoding payload: " + ex.Message + " " + ex.StackTrace);
return this;
}
if (payload == null)
throw new ArgumentException("Encoding the arguments to JSON failed!");
Packet packet = new Packet(TransportEventTypes.Message,
originalPacket.SocketIOEvent == SocketIOEventTypes.Event ? SocketIOEventTypes.Ack : SocketIOEventTypes.BinaryAck,
this.Namespace,
payload,
0,
originalPacket.Id);
(Manager as IManager).SendPacket(packet);
return this;
}
#endregion
#region On Implementations
/// <summary>
/// Register a callback for a given name
/// </summary>
public void On(string eventName, SocketIOCallback callback)
{
EventCallbacks.Register(eventName, callback, false, this.AutoDecodePayload);
}
public void On(SocketIOEventTypes type, SocketIOCallback callback)
{
string eventName = EventNames.GetNameFor(type);
EventCallbacks.Register(eventName, callback, false, this.AutoDecodePayload);
}
public void On(string eventName, SocketIOCallback callback, bool autoDecodePayload)
{
EventCallbacks.Register(eventName, callback, false, autoDecodePayload);
}
public void On(SocketIOEventTypes type, SocketIOCallback callback, bool autoDecodePayload)
{
string eventName = EventNames.GetNameFor(type);
EventCallbacks.Register(eventName, callback, false, autoDecodePayload);
}
#endregion
#region Once Implementations
public void Once(string eventName, SocketIOCallback callback)
{
EventCallbacks.Register(eventName, callback, true, this.AutoDecodePayload);
}
public void Once(SocketIOEventTypes type, SocketIOCallback callback)
{
EventCallbacks.Register(EventNames.GetNameFor(type), callback, true, this.AutoDecodePayload);
}
public void Once(string eventName, SocketIOCallback callback, bool autoDecodePayload)
{
EventCallbacks.Register(eventName, callback, true, autoDecodePayload);
}
public void Once(SocketIOEventTypes type, SocketIOCallback callback, bool autoDecodePayload)
{
EventCallbacks.Register(EventNames.GetNameFor(type), callback, true, autoDecodePayload);
}
#endregion
#region Off Implementations
/// <summary>
/// Remove all callbacks for all events.
/// </summary>
public void Off()
{
EventCallbacks.Clear();
}
/// <summary>
/// Removes all callbacks to the given event.
/// </summary>
public void Off(string eventName)
{
EventCallbacks.Unregister(eventName);
}
/// <summary>
/// Removes all callbacks to the given event.
/// </summary>
public void Off(SocketIOEventTypes type)
{
Off(EventNames.GetNameFor(type));
}
/// <summary>
/// Remove the specified callback.
/// </summary>
public void Off(string eventName, SocketIOCallback callback)
{
EventCallbacks.Unregister(eventName, callback);
}
/// <summary>
/// Remove the specified callback.
/// </summary>
public void Off(SocketIOEventTypes type, SocketIOCallback callback)
{
EventCallbacks.Unregister(EventNames.GetNameFor(type), callback);
}
#endregion
#region Packet Handling
/// <summary>
/// Last call of the OnPacket chain(Transport -> Manager -> Socket), we will dispatch the event if there is any callback
/// </summary>
void ISocket.OnPacket(Packet packet)
{
// Some preprocessing of the packet
switch(packet.SocketIOEvent)
{
case SocketIOEventTypes.Connect:
if (this.Manager.Options.ServerVersion != SupportedSocketIOVersions.v3)
{
this.Id = this.Namespace != "/" ? this.Namespace + "#" + this.Manager.Handshake.Sid : this.Manager.Handshake.Sid;
}
else
{
var data = JSON.Json.Decode(packet.Payload) as Dictionary<string, object>;
this.Id = data["sid"].ToString();
}
this.IsOpen = true;
break;
case SocketIOEventTypes.Disconnect:
if (IsOpen)
{
IsOpen = false;
EventCallbacks.Call(EventNames.GetNameFor(SocketIOEventTypes.Disconnect), packet);
Disconnect();
}
break;
// Create an Error object from the server-sent json string
case SocketIOEventTypes.Error:
bool success = false;
object result = JSON.Json.Decode(packet.Payload, ref success);
if (success)
{
var errDict = result as Dictionary<string, object>;
Error err = null;
if (errDict != null)
{
object tmpObject = null;
string code = null;
if (errDict.TryGetValue("code", out tmpObject))
code = tmpObject.ToString();
int errorCode;
if (code != null && int.TryParse(code, out errorCode) && errorCode >= 0 && errorCode <= 7)
{
errDict.TryGetValue("message", out tmpObject);
err = new Error((SocketIOErrors)errorCode, tmpObject != null ? tmpObject.ToString() : string.Empty);
}
}
if (err == null)
err = new Error(SocketIOErrors.Custom, packet.Payload);
EventCallbacks.Call(EventNames.GetNameFor(SocketIOEventTypes.Error), packet, err);
return;
}
break;
}
// Dispatch the event to all subscriber
EventCallbacks.Call(packet);
// call Ack callbacks
if ((packet.SocketIOEvent == SocketIOEventTypes.Ack || packet.SocketIOEvent == SocketIOEventTypes.BinaryAck) && AckCallbacks != null)
{
SocketIOAckCallback ackCallback = null;
if (AckCallbacks.TryGetValue(packet.Id, out ackCallback) &&
ackCallback != null)
{
try
{
ackCallback(this, packet, this.AutoDecodePayload ? packet.Decode(Manager.Encoder) : null);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("Socket", "ackCallback", ex);
}
}
AckCallbacks.Remove(packet.Id);
}
}
#endregion
/// <summary>
/// Emits an internal packet-less event to the user level.
/// </summary>
void ISocket.EmitEvent(SocketIOEventTypes type, params object[] args)
{
(this as ISocket).EmitEvent(EventNames.GetNameFor(type), args);
}
/// <summary>
/// Emits an internal packet-less event to the user level.
/// </summary>
void ISocket.EmitEvent(string eventName, params object[] args)
{
if (!string.IsNullOrEmpty(eventName))
EventCallbacks.Call(eventName, null, args);
}
void ISocket.EmitError(SocketIOErrors errCode, string msg)
{
(this as ISocket).EmitEvent(SocketIOEventTypes.Error, new Error(errCode, msg));
}
#region Private Helper Functions
/// <summary>
/// Called when the underlying transport is connected
/// </summary>
internal void OnTransportOpen()
{
HTTPManager.Logger.Information("Socket", "OnTransportOpen - IsOpen: " + this.IsOpen);
if (this.IsOpen)
return;
if (this.Namespace != "/" || this.Manager.Options.ServerVersion == SupportedSocketIOVersions.v3)
{
try
{
string authData = null;
if (this.Manager.Options.ServerVersion == SupportedSocketIOVersions.v3)
authData = this.Manager.Options.Auth != null ? this.Manager.Options.Auth(this.Manager, this) : "{}";
(Manager as IManager).SendPacket(new Packet(TransportEventTypes.Message, SocketIOEventTypes.Connect, this.Namespace, authData));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("Socket", "OnTransportOpen", ex);
}
}
else
this.IsOpen = true;
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 2087171a4dffa4f49a83c2c61d3debba
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,749 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System;
using System.Collections.Generic;
using BestHTTP.SocketIO.Transports;
using BestHTTP.Extensions;
using BestHTTP.SocketIO.JsonEncoders;
using BestHTTP.SocketIO.Events;
namespace BestHTTP.SocketIO
{
public sealed class SocketManager : IHeartbeat, IManager
{
/// <summary>
/// Possible states of a SocketManager instance.
/// </summary>
public enum States
{
/// <summary>
/// Initial state of the SocketManager
/// </summary>
Initial,
/// <summary>
/// The SocketManager is currently opening.
/// </summary>
Opening,
/// <summary>
/// The SocketManager is open, events can be sent to the server.
/// </summary>
Open,
/// <summary>
/// Paused for transport upgrade
/// </summary>
Paused,
/// <summary>
/// An error occurred, the SocketManager now trying to connect again to the server.
/// </summary>
Reconnecting,
/// <summary>
/// The SocketManager is closed, initiated by the user or by the server
/// </summary>
Closed
}
/// <summary>
/// The default Json encode/decoder that will be used to encode/decode the event arguments.
/// </summary>
public static IJsonEncoder DefaultEncoder = new DefaultJSonEncoder();
/// <summary>
/// Supported Socket.IO protocol version
/// </summary>
public int ProtocolVersion { get { return this.Options.ServerVersion == SupportedSocketIOVersions.v3 ? 4 : 3; } }
#region Public Properties
/// <summary>
/// The current state of this Socket.IO manager.
/// </summary>
public States State { get { return state; } private set { PreviousState = state; state = value; } }
private States state;
/// <summary>
/// The SocketOptions instance that this manager will use.
/// </summary>
public SocketOptions Options { get; private set; }
/// <summary>
/// The Uri to the Socket.IO endpoint.
/// </summary>
public Uri Uri { get; private set; }
/// <summary>
/// The server sent and parsed Handshake data.
/// </summary>
public HandshakeData Handshake { get; private set; }
/// <summary>
/// The currently used main transport instance.
/// </summary>
public ITransport Transport { get; private set; }
/// <summary>
/// The Request counter for request-based transports.
/// </summary>
public ulong RequestCounter { get; internal set; }
/// <summary>
/// The root("/") Socket.
/// </summary>
public Socket Socket { get { return GetSocket(); } }
/// <summary>
/// Indexer to access socket associated to the given namespace.
/// </summary>
public Socket this[string nsp] { get { return GetSocket(nsp); } }
/// <summary>
/// How many reconnect attempts made.
/// </summary>
public int ReconnectAttempts { get; private set; }
/// <summary>
/// The JSon encoder that will be used to encode the sent data to json and decode the received json to an object list.
/// </summary>
public IJsonEncoder Encoder { get; set; }
#endregion
#region Internal Properties
/// <summary>
/// Timestamp support to the request based transports.
/// </summary>
internal UInt64 Timestamp { get { return (UInt64)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalMilliseconds; } }
/// <summary>
/// Auto-incrementing property to return Ack ids.
/// </summary>
internal int NextAckId { get { return System.Threading.Interlocked.Increment(ref nextAckId); } }
private int nextAckId;
/// <summary>
/// Internal property to store the previous state of the manager.
/// </summary>
internal States PreviousState { get; private set; }
/// <summary>
/// Transport currently upgrading.
/// </summary>
internal ITransport UpgradingTransport { get; set; }
#endregion
#region Privates
/// <summary>
/// Namespace name -> Socket mapping
/// </summary>
private Dictionary<string, Socket> Namespaces = new Dictionary<string, Socket>();
/// <summary>
/// List of the sockets to able to iterate over them easily.
/// </summary>
private List<Socket> Sockets = new List<Socket>();
/// <summary>
/// List of unsent packets. Only instantiated when we have to use it.
/// </summary>
private List<Packet> OfflinePackets;
/// <summary>
/// When we sent out the last heartbeat(Ping) message.
/// </summary>
private DateTime LastHeartbeat = DateTime.MinValue;
/// <summary>
/// When we have to try to do a reconnect attempt
/// </summary>
private DateTime ReconnectAt;
/// <summary>
/// When we started to connect to the server.
/// </summary>
private DateTime ConnectionStarted;
/// <summary>
/// Private flag to avoid multiple Close call
/// </summary>
private bool closing;
/// <summary>
/// Whether the connection is waiting for a ping response.
/// </summary>
private bool IsWaitingPong;
/// <summary>
/// In Engine.io v4 / socket.io v3 the server sends the ping messages, not the client.
/// </summary>
private DateTime lastPingReceived;
#endregion
#region Constructors
/// <summary>
/// Constructor to create a SocketManager instance that will connect to the given uri.
/// </summary>
public SocketManager(Uri uri)
:this(uri, new SocketOptions())
{
}
/// <summary>
/// Constructor to create a SocketManager instance.
/// </summary>
public SocketManager(Uri uri, SocketOptions options)
{
Uri = uri;
Options = options ?? new SocketOptions();
State = States.Initial;
PreviousState = States.Initial;
Encoder = SocketManager.DefaultEncoder;
}
#endregion
/// <summary>
/// Returns with the "/" namespace, the same as the Socket property.
/// </summary>
public Socket GetSocket()
{
return GetSocket("/");
}
/// <summary>
/// Returns with the specified namespace
/// </summary>
public Socket GetSocket(string nsp)
{
if (string.IsNullOrEmpty(nsp))
throw new ArgumentNullException("Namespace parameter is null or empty!");
/*if (nsp[0] != '/')
nsp = "/" + nsp;*/
Socket socket = null;
if (!Namespaces.TryGetValue(nsp, out socket))
{
// No socket found, create one
socket = new Socket(nsp, this);
Namespaces.Add(nsp, socket);
Sockets.Add(socket);
(socket as ISocket).Open();
}
return socket;
}
/// <summary>
/// Internal function to remove a Socket instance from this manager.
/// </summary>
/// <param name="socket"></param>
void IManager.Remove(Socket socket)
{
Namespaces.Remove(socket.Namespace);
Sockets.Remove(socket);
if (Sockets.Count == 0)
Close();
}
#region Connection to the server, and upgrading
/// <summary>
/// This function will begin to open the Socket.IO connection by sending out the handshake request.
/// If the Options' AutoConnect is true, it will be called automatically.
/// </summary>
public void Open()
{
if (State != States.Initial &&
State != States.Closed &&
State != States.Reconnecting)
return;
HTTPManager.Logger.Information("SocketManager", "Opening");
ReconnectAt = DateTime.MinValue;
switch (Options.ConnectWith)
{
case TransportTypes.Polling: Transport = new PollingTransport(this); break;
#if !BESTHTTP_DISABLE_WEBSOCKET
case TransportTypes.WebSocket:
Transport = new WebSocketTransport(this);
break;
#endif
}
Transport.Open();
(this as IManager).EmitEvent("connecting");
State = States.Opening;
ConnectionStarted = DateTime.UtcNow;
HTTPManager.Heartbeats.Subscribe(this);
// The root namespace will be opened by default
GetSocket("/");
}
/// <summary>
/// Closes this Socket.IO connection.
/// </summary>
public void Close()
{
(this as IManager).Close(true);
}
/// <summary>
/// Closes this Socket.IO connection.
/// </summary>
void IManager.Close(bool removeSockets)
{
if (State == States.Closed || closing)
return;
closing = true;
HTTPManager.Logger.Information("SocketManager", "Closing");
HTTPManager.Heartbeats.Unsubscribe(this);
// Disconnect the sockets. The Disconnect function will call the Remove function to remove it from the Sockets list.
if (removeSockets)
while (Sockets.Count > 0)
(Sockets[Sockets.Count - 1] as ISocket).Disconnect(removeSockets);
else
for (int i = 0; i < Sockets.Count; ++i)
(Sockets[i] as ISocket).Disconnect(removeSockets);
// Set to Closed after Socket's Disconnect. This way we can send the disconnect events to the server.
State = States.Closed;
LastHeartbeat = DateTime.MinValue;
IsWaitingPong = false;
lastPingReceived = DateTime.MinValue;
if (removeSockets && OfflinePackets != null)
OfflinePackets.Clear();
// Remove the references from the dictionary too.
if (removeSockets)
Namespaces.Clear();
Handshake = null;
if (Transport != null)
Transport.Close();
Transport = null;
if (UpgradingTransport != null)
UpgradingTransport.Close();
UpgradingTransport = null;
closing = false;
}
/// <summary>
/// Called from a ITransport implementation when an error occurs and we may have to try to reconnect.
/// </summary>
void IManager.TryToReconnect()
{
if (State == States.Reconnecting ||
State == States.Closed)
return;
if (!Options.Reconnection || HTTPManager.IsQuitting)
{
Close();
return;
}
if (++ReconnectAttempts >= Options.ReconnectionAttempts)
{
(this as IManager).EmitEvent("reconnect_failed");
Close();
return;
}
Random rand = new Random();
int delay = (int)Options.ReconnectionDelay.TotalMilliseconds * ReconnectAttempts;
ReconnectAt = DateTime.UtcNow +
TimeSpan.FromMilliseconds(Math.Min(rand.Next(/*rand min:*/(int)(delay - (delay * Options.RandomizationFactor)),
/*rand max:*/(int)(delay + (delay * Options.RandomizationFactor))),
(int)Options.ReconnectionDelayMax.TotalMilliseconds));
(this as IManager).Close(false);
State = States.Reconnecting;
for (int i = 0; i < Sockets.Count; ++i)
(Sockets[i] as ISocket).Open();
// In the Close() function we unregistered
HTTPManager.Heartbeats.Subscribe(this);
HTTPManager.Logger.Information("SocketManager", "Reconnecting");
}
/// <summary>
/// Called by transports when they are connected to the server.
/// </summary>
bool IManager.OnTransportConnected(ITransport trans)
{
HTTPManager.Logger.Information("SocketManager", string.Format("OnTransportConnected State: {0}, PreviousState: {1}, Current Transport: {2}, Upgrading Transport: {3}", this.State, this.PreviousState, trans.Type, UpgradingTransport != null ? UpgradingTransport.Type.ToString() : "null"));
if (State != States.Opening)
return false;
if (PreviousState == States.Reconnecting)
(this as IManager).EmitEvent("reconnect");
State = States.Open;
if (PreviousState == States.Reconnecting)
(this as IManager).EmitEvent("reconnect_before_offline_packets");
for (int i = 0; i < Sockets.Count; ++i)
{
var socket = Sockets[i];
if (socket != null)
socket.OnTransportOpen();
}
ReconnectAttempts = 0;
// Send out packets that we collected while there were no available transport.
SendOfflinePackets();
#if !BESTHTTP_DISABLE_WEBSOCKET
// Can we upgrade to WebSocket transport?
if (Transport.Type != TransportTypes.WebSocket &&
Handshake.Upgrades.Contains("websocket"))
{
UpgradingTransport = new WebSocketTransport(this);
UpgradingTransport.Open();
}
#endif
return true;
}
void IManager.OnTransportError(ITransport trans, string err)
{
(this as IManager).EmitError(SocketIOErrors.Internal, err);
trans.Close();
(this as IManager).TryToReconnect();
}
void IManager.OnTransportProbed(ITransport trans)
{
HTTPManager.Logger.Information("SocketManager", "\"probe\" packet received");
// If we have to reconnect, we will go straight with the transport we were able to upgrade
Options.ConnectWith = trans.Type;
// Pause ourself to wait for any send and receive turn to finish.
State = States.Paused;
}
#endregion
#region Packet Handling
/// <summary>
/// Select the best transport to send out packets.
/// </summary>
private ITransport SelectTransport()
{
if (State != States.Open || Transport == null)
return null;
return Transport.IsRequestInProgress ? null : Transport;
}
/// <summary>
/// Will select the best transport and sends out all packets that are in the OfflinePackets list.
/// </summary>
private void SendOfflinePackets()
{
ITransport trans = SelectTransport();
// Send out packets that we not sent while no transport was available.
// This function is called before the event handlers get the 'connected' event, so
// theoretically the packet orders are remains.
if (OfflinePackets != null && OfflinePackets.Count > 0 && trans != null)
{
trans.Send(OfflinePackets);
OfflinePackets.Clear();
}
}
/// <summary>
/// Internal function that called from the Socket class. It will send out the packet instantly, or if no transport is available it will store
/// the packet in the OfflinePackets list.
/// </summary>
void IManager.SendPacket(Packet packet)
{
HTTPManager.Logger.Information("SocketManager", "SendPacket " + packet.ToString());
ITransport trans = SelectTransport();
if (trans != null)
{
try
{
trans.Send(packet);
}
catch(Exception ex)
{
(this as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
}
}
else
{
HTTPManager.Logger.Information("SocketManager", "SendPacket - Offline stashing packet");
if (OfflinePackets == null)
OfflinePackets = new List<Packet>();
// The same packet can be sent through multiple Sockets.
OfflinePackets.Add(packet.Clone());
}
}
/// <summary>
/// Called from the currently operating Transport. Will pass forward to the Socket that has to call the callbacks.
/// </summary>
void IManager.OnPacket(Packet packet)
{
if (State == States.Closed)
{
HTTPManager.Logger.Information("SocketManager", "OnPacket - State == States.Closed");
return;
}
switch(packet.TransportEvent)
{
case TransportEventTypes.Open:
if (Handshake == null)
{
Handshake = new HandshakeData();
if (!Handshake.Parse(packet.Payload))
HTTPManager.Logger.Warning("SocketManager", "Expected handshake data, but wasn't able to parse. Payload: " + packet.Payload);
(this as IManager).OnTransportConnected(Transport);
return;
}
else
HTTPManager.Logger.Information("SocketManager", "OnPacket - Already received handshake data!");
break;
case TransportEventTypes.Ping:
if (this.Options.ServerVersion == SupportedSocketIOVersions.Unknown)
{
HTTPManager.Logger.Information("SocketManager", "Received Ping packet from server, setting ServerVersion to v3!");
this.Options.ServerVersion = SupportedSocketIOVersions.v3;
}
lastPingReceived = DateTime.UtcNow;
(this as IManager).SendPacket(new Packet(TransportEventTypes.Pong, SocketIOEventTypes.Unknown, "/", string.Empty));
break;
case TransportEventTypes.Pong:
IsWaitingPong = false;
break;
}
Socket socket = null;
if (Namespaces.TryGetValue(packet.Namespace, out socket))
(socket as ISocket).OnPacket(packet);
else
HTTPManager.Logger.Warning("SocketManager", "Namespace \"" + packet.Namespace + "\" not found!");
}
#endregion
/// <summary>
/// Sends an event to all available namespaces.
/// </summary>
public void EmitAll(string eventName, params object[] args)
{
for (int i = 0; i < Sockets.Count; ++i)
Sockets[i].Emit(eventName, args);
}
/// <summary>
/// Emits an internal packet-less event to the root namespace without creating it if it isn't exists yet.
/// </summary>
void IManager.EmitEvent(string eventName, params object[] args)
{
Socket socket = null;
if (Namespaces.TryGetValue("/", out socket))
(socket as ISocket).EmitEvent(eventName, args);
}
/// <summary>
/// Emits an internal packet-less event to the root namespace without creating it if it isn't exists yet.
/// </summary>
void IManager.EmitEvent(SocketIOEventTypes type, params object[] args)
{
(this as IManager).EmitEvent(EventNames.GetNameFor(type), args);
}
void IManager.EmitError(SocketIOErrors errCode, string msg)
{
(this as IManager).EmitEvent(SocketIOEventTypes.Error, new Error(errCode, msg));
}
void IManager.EmitAll(string eventName, params object[] args)
{
for (int i = 0; i < Sockets.Count; ++i)
(Sockets[i] as ISocket).EmitEvent(eventName, args);
}
#region IHeartbeat Implementation
/// <summary>
/// Called from the HTTPManager's OnUpdate function every frame. It's main function is to send out heartbeat messages.
/// </summary>
void IHeartbeat.OnHeartbeatUpdate(TimeSpan dif)
{
switch (State)
{
case States.Paused:
// To ensure no messages are lost, the upgrade packet will only be sent once all the buffers of the existing transport are flushed and the transport is considered paused.
if (!Transport.IsRequestInProgress &&
!Transport.IsPollingInProgress)
{
State = States.Open;
// Close the current transport
Transport.Close();
// and switch to the newly upgraded one
Transport = UpgradingTransport;
UpgradingTransport = null;
// We will send an Upgrade("5") packet.
Transport.Send(new Packet(TransportEventTypes.Upgrade, SocketIOEventTypes.Unknown, "/", string.Empty));
goto case States.Open;
}
break;
case States.Opening:
if (DateTime.UtcNow - ConnectionStarted >= Options.Timeout)
{
(this as IManager).EmitError(SocketIOErrors.Internal, "Connection timed out!");
(this as IManager).EmitEvent("connect_error");
(this as IManager).EmitEvent("connect_timeout");
(this as IManager).TryToReconnect();
}
break;
case States.Reconnecting:
if (ReconnectAt != DateTime.MinValue && DateTime.UtcNow >= ReconnectAt)
{
(this as IManager).EmitEvent("reconnect_attempt");
(this as IManager).EmitEvent("reconnecting");
Open();
}
break;
case States.Open:
ITransport trans = null;
// Select transport to use
if (Transport != null && Transport.State == TransportStates.Open)
trans = Transport;
// not yet open?
if (trans == null || trans.State != TransportStates.Open)
return;
// Start to poll the server for events
trans.Poll();
// Start to send out unsent packets
SendOfflinePackets();
// First time we reached this point. Set the LastHeartbeat to the current time, 'cause we are just opened.
if (LastHeartbeat == DateTime.MinValue)
{
LastHeartbeat = DateTime.UtcNow;
lastPingReceived = DateTime.UtcNow;
if (this.Options.ServerVersion == SupportedSocketIOVersions.Unknown) {
(this as IManager).SendPacket(new Packet(TransportEventTypes.Ping, SocketIOEventTypes.Unknown, "/", string.Empty));
IsWaitingPong = true;
}
return;
}
switch (this.Options.ServerVersion)
{
case SupportedSocketIOVersions.v2:
// It's time to send out a ping event to the server
if (!IsWaitingPong && DateTime.UtcNow - LastHeartbeat > Handshake.PingInterval)
{
(this as IManager).SendPacket(new Packet(TransportEventTypes.Ping, SocketIOEventTypes.Unknown, "/", string.Empty));
LastHeartbeat = DateTime.UtcNow;
IsWaitingPong = true;
}
// No pong event received in the given time, we are disconnected.
if (IsWaitingPong && DateTime.UtcNow - LastHeartbeat > Handshake.PingTimeout)
{
IsWaitingPong = false;
(this as IManager).TryToReconnect();
}
break;
case SupportedSocketIOVersions.v3:
if (DateTime.UtcNow - lastPingReceived > Handshake.PingInterval + Handshake.PingTimeout)
{
(this as IManager).TryToReconnect();
}
break;
case SupportedSocketIOVersions.Unknown:
var diff = DateTime.UtcNow - LastHeartbeat;
if (diff > Handshake.PingTimeout)
{
this.Options.ServerVersion = IsWaitingPong ? SupportedSocketIOVersions.v3 : SupportedSocketIOVersions.v2;
}
break;
}
break; // case States.Open:
}
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 71662b80689f3cc479fbd747b6b05ed4
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,184 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System;
using System.Text;
using BestHTTP.PlatformSupport.Text;
using PlatformSupport.Collections.ObjectModel;
#if !NETFX_CORE
using PlatformSupport.Collections.Specialized;
#else
using System.Collections.Specialized;
#endif
namespace BestHTTP.SocketIO
{
public delegate void HTTPRequestCallbackDelegate(SocketManager manager, HTTPRequest request);
public enum SupportedSocketIOVersions
{
Unknown,
v2,
v3
}
public sealed class SocketOptions
{
#region Properties
/// <summary>
/// The SocketManager will try to connect with this transport.
/// </summary>
public Transports.TransportTypes ConnectWith { get; set; }
/// <summary>
/// Whether to reconnect automatically after a disconnect (default true)
/// </summary>
public bool Reconnection { get; set; }
/// <summary>
/// Number of attempts before giving up (default Int.MaxValue)
/// </summary>
public int ReconnectionAttempts { get; set; }
/// <summary>
/// How long to initially wait before attempting a new reconnection (default 1000ms).
/// Affected by +/- RandomizationFactor, for example the default initial delay will be between 500ms to 1500ms.
/// </summary>
public TimeSpan ReconnectionDelay { get; set; }
/// <summary>
/// Maximum amount of time to wait between reconnections (default 5000ms).
/// Each attempt increases the reconnection delay along with a randomization as above.
/// </summary>
public TimeSpan ReconnectionDelayMax { get; set; }
/// <summary>
/// (default 0.5`), [0..1]
/// </summary>
public float RandomizationFactor { get { return randomizationFactor; } set { randomizationFactor = Math.Min(1.0f, Math.Max(0.0f, value)); } }
private float randomizationFactor;
/// <summary>
/// Connection timeout before a connect_error and connect_timeout events are emitted (default 20000ms)
/// </summary>
public TimeSpan Timeout { get; set; }
/// <summary>
/// By setting this false, you have to call SocketManager's Open() whenever you decide it's appropriate.
/// </summary>
public bool AutoConnect { get; set; }
/// <summary>
/// Additional query parameters that will be passed for accessed uris. If the value is null, or an empty string it will be not appended to the query only the key.
/// <remarks>The keys and values must be escaped properly, as the plugin will not escape these. </remarks>
/// </summary>
public ObservableDictionary<string, string> AdditionalQueryParams
{
get { return additionalQueryParams; }
set
{
// Unsubscribe from previous dictionary's events
if (additionalQueryParams != null)
additionalQueryParams.CollectionChanged -= AdditionalQueryParams_CollectionChanged;
additionalQueryParams = value;
// Clear out the cached value
BuiltQueryParams = null;
// Subscribe to the collection changed event
if (value != null)
value.CollectionChanged += AdditionalQueryParams_CollectionChanged;
}
}
private ObservableDictionary<string, string> additionalQueryParams;
/// <summary>
/// If it's false, the parameters in the AdditionalQueryParams will be passed for all HTTP requests. Its default value is true.
/// </summary>
public bool QueryParamsOnlyForHandshake { get; set; }
/// <summary>
/// A callback that called for every HTTPRequest the socket.io protocol sends out. It can be used to further customize (add additional request for example) requests.
/// </summary>
public HTTPRequestCallbackDelegate HTTPRequestCustomizationCallback { get; set; }
/// <summary>
/// Socket.IO protocol version of the server. If left as default (Unknown) the plugin tries to detect the server version.
/// </summary>
public SupportedSocketIOVersions ServerVersion { get; set; }
/// <summary>
/// Starting with Socket.IO v3, connecting to a namespace a client can send payload data. When the Auth callback function is set, the plugin going to call it when connecting to a namespace. Its return value must be a json string!
/// </summary>
public Func<SocketManager, Socket, string> Auth;
#endregion
/// <summary>
/// The cached value of the result of the BuildQueryParams() call.
/// </summary>
private string BuiltQueryParams;
/// <summary>
/// Constructor, setting the default option values.
/// </summary>
public SocketOptions()
{
ConnectWith = Transports.TransportTypes.Polling;
Reconnection = true;
ReconnectionAttempts = int.MaxValue;
ReconnectionDelay = TimeSpan.FromMilliseconds(1000);
ReconnectionDelayMax = TimeSpan.FromMilliseconds(5000);
RandomizationFactor = 0.5f;
Timeout = TimeSpan.FromMilliseconds(20000);
AutoConnect = true;
QueryParamsOnlyForHandshake = true;
}
#region Helper Functions
/// <summary>
/// Builds the keys and values from the AdditionalQueryParams to an key=value form. If AdditionalQueryParams is null or empty, it will return an empty string.
/// </summary>
internal string BuildQueryParams()
{
if (AdditionalQueryParams == null || AdditionalQueryParams.Count == 0)
return string.Empty;
if (!string.IsNullOrEmpty(BuiltQueryParams))
return BuiltQueryParams;
StringBuilder sb = StringBuilderPool.Get(AdditionalQueryParams.Count * 4); //new StringBuilder(AdditionalQueryParams.Count * 4);
foreach (var kvp in AdditionalQueryParams)
{
sb.Append("&");
sb.Append(kvp.Key);
if (!string.IsNullOrEmpty(kvp.Value))
{
sb.Append("=");
sb.Append(kvp.Value);
}
}
return BuiltQueryParams = StringBuilderPool.ReleaseAndGrab(sb);
}
/// <summary>
/// This event will be called when the AdditonalQueryPrams dictionary changed. We have to reset the cached values.
/// </summary>
private void AdditionalQueryParams_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
BuiltQueryParams = null;
}
#endregion
}
}
#endif

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: eaefe2a013280de43aa61175fa8c119a
folderAsset: yes
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,104 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System.Collections.Generic;
namespace BestHTTP.SocketIO.Transports
{
public enum TransportTypes
{
Polling,
#if !BESTHTTP_DISABLE_WEBSOCKET
WebSocket
#endif
}
/// <summary>
/// Possible states of an ITransport implementation.
/// </summary>
public enum TransportStates : int
{
/// <summary>
/// The transport is connecting to the server.
/// </summary>
Connecting = 0,
/// <summary>
/// The transport is connected, and started the opening process.
/// </summary>
Opening = 1,
/// <summary>
/// The transport is open, can send and receive packets.
/// </summary>
Open = 2,
/// <summary>
/// The transport is closed.
/// </summary>
Closed = 3,
/// <summary>
/// The transport is paused.
/// </summary>
Paused = 4
}
/// <summary>
/// An interface that a Socket.IO transport must implement.
/// </summary>
public interface ITransport
{
/// <summary>
/// Type of this transport.
/// </summary>
TransportTypes Type { get; }
/// <summary>
/// Current state of the transport
/// </summary>
TransportStates State { get; }
/// <summary>
/// SocketManager instance that this transport is bound to.
/// </summary>
SocketManager Manager { get; }
/// <summary>
/// True if the transport is busy with sending messages.
/// </summary>
bool IsRequestInProgress { get; }
/// <summary>
/// True if the transport is busy with a poll request.
/// </summary>
bool IsPollingInProgress { get; }
/// <summary>
/// Start open/upgrade the transport.
/// </summary>
void Open();
/// <summary>
/// Do a poll for available messages on the server.
/// </summary>
void Poll();
/// <summary>
/// Send a single packet to the server.
/// </summary>
void Send(Packet packet);
/// <summary>
/// Send a list of packets to the server.
/// </summary>
void Send(List<Packet> packets);
/// <summary>
/// Close this transport.
/// </summary>
void Close();
}
}
#endif

View File

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

View File

@@ -0,0 +1,638 @@
#if !BESTHTTP_DISABLE_SOCKETIO
using System;
using System.Linq;
using System.Text;
using BestHTTP.Extensions;
namespace BestHTTP.SocketIO.Transports
{
public sealed class PollingTransport : ITransport
{
#region Public (ITransport) Properties
public TransportTypes Type { get { return TransportTypes.Polling; } }
public TransportStates State { get; private set; }
public SocketManager Manager { get; private set; }
public bool IsRequestInProgress { get { return LastRequest != null; } }
public bool IsPollingInProgress { get { return PollRequest != null; } }
#endregion
#region Private Fields
/// <summary>
/// The last POST request we sent to the server.
/// </summary>
private HTTPRequest LastRequest;
/// <summary>
/// Last GET request we sent to the server.
/// </summary>
private HTTPRequest PollRequest;
/// <summary>
/// The last packet with expected binary attachments
/// </summary>
private Packet PacketWithAttachment;
#endregion
public enum PayloadTypes : byte
{
Text,
Binary
}
public PollingTransport(SocketManager manager)
{
Manager = manager;
}
public void Open()
{
string format = "{0}?EIO={1}&transport=polling&t={2}-{3}{5}";
if (Manager.Handshake != null)
format += "&sid={4}";
bool sendAdditionalQueryParams = !Manager.Options.QueryParamsOnlyForHandshake || (Manager.Options.QueryParamsOnlyForHandshake && Manager.Handshake == null);
HTTPRequest request = new HTTPRequest(new Uri(string.Format(format,
Manager.Uri.ToString(),
Manager.ProtocolVersion,
Manager.Timestamp.ToString(),
Manager.RequestCounter++.ToString(),
Manager.Handshake != null ? Manager.Handshake.Sid : string.Empty,
sendAdditionalQueryParams ? Manager.Options.BuildQueryParams() : string.Empty)),
OnRequestFinished);
#if !BESTHTTP_DISABLE_CACHING
// Don't even try to cache it
request.DisableCache = true;
#endif
request.MaxRetries = 0;
if (this.Manager.Options.HTTPRequestCustomizationCallback != null)
this.Manager.Options.HTTPRequestCustomizationCallback(this.Manager, request);
request.Send();
State = TransportStates.Opening;
}
/// <summary>
/// Closes the transport and cleans up resources.
/// </summary>
public void Close()
{
if (State == TransportStates.Closed)
return;
State = TransportStates.Closed;
/*
if (LastRequest != null)
LastRequest.Abort();
if (PollRequest != null)
PollRequest.Abort();*/
}
#region Packet Sending Implementation
private System.Collections.Generic.List<Packet> lonelyPacketList = new System.Collections.Generic.List<Packet>(1);
public void Send(Packet packet)
{
try
{
lonelyPacketList.Add(packet);
Send(lonelyPacketList);
}
finally
{
lonelyPacketList.Clear();
}
}
public void Send(System.Collections.Generic.List<Packet> packets)
{
if (State != TransportStates.Opening && State != TransportStates.Open)
return;
if (IsRequestInProgress)
throw new Exception("Sending packets are still in progress!");
LastRequest = new HTTPRequest(new Uri(string.Format("{0}?EIO={1}&transport=polling&t={2}-{3}&sid={4}{5}",
Manager.Uri.ToString(),
Manager.ProtocolVersion,
Manager.Timestamp.ToString(),
Manager.RequestCounter++.ToString(),
Manager.Handshake.Sid,
!Manager.Options.QueryParamsOnlyForHandshake ? Manager.Options.BuildQueryParams() : string.Empty)),
HTTPMethods.Post,
OnRequestFinished);
#if !BESTHTTP_DISABLE_CACHING
// Don't even try to cache it
LastRequest.DisableCache = true;
#endif
if (this.Manager.Options.ServerVersion == SupportedSocketIOVersions.v2)
SendV2(packets, LastRequest);
else
SendV3(packets, LastRequest);
if (this.Manager.Options.HTTPRequestCustomizationCallback != null)
this.Manager.Options.HTTPRequestCustomizationCallback(this.Manager, LastRequest);
LastRequest.Send();
}
StringBuilder sendBuilder = new StringBuilder();
private void SendV3(System.Collections.Generic.List<Packet> packets, HTTPRequest request)
{
sendBuilder.Length = 0;
try
{
for (int i = 0; i < packets.Count; ++i)
{
var packet = packets[i];
if (i > 0)
sendBuilder.Append((char)0x1E);
sendBuilder.Append(packet.Encode());
if (packet.Attachments != null && packet.Attachments.Count > 0)
for(int cv = 0; cv < packet.Attachments.Count; ++cv)
{
sendBuilder.Append((char)0x1E);
sendBuilder.Append('b');
sendBuilder.Append(Convert.ToBase64String(packet.Attachments[i]));
}
}
packets.Clear();
}
catch (Exception ex)
{
(Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
return;
}
var str = sendBuilder.ToString();
request.RawData = System.Text.Encoding.UTF8.GetBytes(str);
request.SetHeader("Content-Type", "text/plain; charset=UTF-8");
}
private void SendV2(System.Collections.Generic.List<Packet> packets, HTTPRequest request)
{
byte[] buffer = null;
try
{
buffer = packets[0].EncodeBinary();
for (int i = 1; i < packets.Count; ++i)
{
byte[] tmpBuffer = packets[i].EncodeBinary();
Array.Resize(ref buffer, buffer.Length + tmpBuffer.Length);
Array.Copy(tmpBuffer, 0, buffer, buffer.Length - tmpBuffer.Length, tmpBuffer.Length);
}
packets.Clear();
}
catch (Exception ex)
{
(Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
return;
}
request.SetHeader("Content-Type", "application/octet-stream");
request.RawData = buffer;
}
private void OnRequestFinished(HTTPRequest req, HTTPResponse resp)
{
// Clear out the LastRequest variable, so we can start sending out new packets
LastRequest = null;
if (State == TransportStates.Closed)
return;
string errorString = null;
switch (req.State)
{
// The request finished without any problem.
case HTTPRequestStates.Finished:
if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
HTTPManager.Logger.Verbose("PollingTransport", "OnRequestFinished: " + resp.DataAsText);
if (resp.IsSuccess)
{
// When we are sending data, the response is an 'ok' string
if (req.MethodType != HTTPMethods.Post)
ParseResponse(resp);
}
else
errorString = string.Format("Polling - Request finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2} Uri: {3}",
resp.StatusCode,
resp.Message,
resp.DataAsText,
req.CurrentUri);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
errorString = (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception");
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
errorString = string.Format("Polling - Request({0}) Aborted!", req.CurrentUri);
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
errorString = string.Format("Polling - Connection Timed Out! Uri: {0}", req.CurrentUri);
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
errorString = string.Format("Polling - Processing the request({0}) Timed Out!", req.CurrentUri);
break;
}
if (!string.IsNullOrEmpty(errorString))
(Manager as IManager).OnTransportError(this, errorString);
}
#endregion
#region Polling Implementation
public void Poll()
{
if (PollRequest != null || State == TransportStates.Paused)
return;
PollRequest = new HTTPRequest(new Uri(string.Format("{0}?EIO={1}&transport=polling&t={2}-{3}&sid={4}{5}",
Manager.Uri.ToString(),
Manager.ProtocolVersion,
Manager.Timestamp.ToString(),
Manager.RequestCounter++.ToString(),
Manager.Handshake.Sid,
!Manager.Options.QueryParamsOnlyForHandshake ? Manager.Options.BuildQueryParams() : string.Empty)),
HTTPMethods.Get,
OnPollRequestFinished);
#if !BESTHTTP_DISABLE_CACHING
// Don't even try to cache it
PollRequest.DisableCache = true;
#endif
PollRequest.MaxRetries = 0;
if (this.Manager.Options.HTTPRequestCustomizationCallback != null)
this.Manager.Options.HTTPRequestCustomizationCallback(this.Manager, PollRequest);
PollRequest.Send();
}
private void OnPollRequestFinished(HTTPRequest req, HTTPResponse resp)
{
// Clear the PollRequest variable, so we can start a new poll.
PollRequest = null;
if (State == TransportStates.Closed)
return;
string errorString = null;
switch (req.State)
{
// The request finished without any problem.
case HTTPRequestStates.Finished:
if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
HTTPManager.Logger.Verbose("PollingTransport", "OnPollRequestFinished: " + resp.DataAsText);
if (resp.IsSuccess)
ParseResponse(resp);
else
errorString = string.Format("Polling - Request finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2} Uri: {3}",
resp.StatusCode,
resp.Message,
resp.DataAsText,
req.CurrentUri);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
errorString = req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception";
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
errorString = string.Format("Polling - Request({0}) Aborted!", req.CurrentUri);
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
errorString = string.Format("Polling - Connection Timed Out! Uri: {0}", req.CurrentUri);
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
errorString = string.Format("Polling - Processing the request({0}) Timed Out!", req.CurrentUri);
break;
}
if (!string.IsNullOrEmpty(errorString))
(Manager as IManager).OnTransportError(this, errorString);
}
#endregion
#region Packet Parsing and Handling
/// <summary>
/// Preprocessing and sending out packets to the manager.
/// </summary>
private void OnPacket(Packet packet)
{
if (packet.AttachmentCount != 0 && !packet.HasAllAttachment)
{
PacketWithAttachment = packet;
return;
}
switch (packet.TransportEvent)
{
case TransportEventTypes.Open:
if (this.State != TransportStates.Opening)
HTTPManager.Logger.Warning("PollingTransport", "Received 'Open' packet while state is '" + State.ToString() + "'");
else
State = TransportStates.Open;
goto default;
case TransportEventTypes.Message:
if (packet.SocketIOEvent == SocketIOEventTypes.Connect) //2:40
this.State = TransportStates.Open;
goto default;
default:
(Manager as IManager).OnPacket(packet);
break;
}
}
private SupportedSocketIOVersions GetServerVersion(HTTPResponse resp)
{
string contentTypeValue = resp.GetFirstHeaderValue("content-type");
if (string.IsNullOrEmpty(contentTypeValue))
return SupportedSocketIOVersions.v2;
HeaderParser contentType = new HeaderParser(contentTypeValue);
PayloadTypes type = contentType.Values.FirstOrDefault().Key == "text/plain" ? PayloadTypes.Text : PayloadTypes.Binary;
if (type != PayloadTypes.Text)
return SupportedSocketIOVersions.v2;
// https://github.com/socketio/engine.io-protocol/issues/35
// v3: 96:0{ "sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}
// v4: 0{ "sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}
for (int i = 0; i< resp.Data.Length; ++i)
{
if (resp.Data[i] == ':')
return SupportedSocketIOVersions.v2;
if (resp.Data[i] == '{')
return SupportedSocketIOVersions.v3;
}
return SupportedSocketIOVersions.Unknown;
}
private void ParseResponse(HTTPResponse resp)
{
if (this.Manager.Options.ServerVersion == SupportedSocketIOVersions.Unknown)
this.Manager.Options.ServerVersion = GetServerVersion(resp);
if (this.Manager.Options.ServerVersion == SupportedSocketIOVersions.v2)
this.ParseResponseV2(resp);
else
this.ParseResponseV3(resp);
}
private void ParseResponseV3(HTTPResponse resp)
{
try
{
if (resp == null || resp.Data == null || resp.Data.Length < 1)
return;
//HeaderParser contentType = new HeaderParser(resp.GetFirstHeaderValue("content-type"));
//PayloadTypes type = contentType.Values.FirstOrDefault().Key == "text/plain" ? PayloadTypes.Text : PayloadTypes.Binary;
int idx = 0;
while (idx < resp.Data.Length)
{
int endIdx = FindNextRecordSeparator(resp.Data, idx);
int length = endIdx - idx;
if (length <= 0)
break;
Packet packet = null;
if (resp.Data[idx] == 'b')
{
if (PacketWithAttachment != null)
{
// First byte is the binary indicator('b'). We must skip it, so we advance our idx and also have to decrease length
idx++;
length--;
var base64Encoded = System.Text.Encoding.UTF8.GetString(resp.Data, idx, length);
PacketWithAttachment.AddAttachmentFromServer(Convert.FromBase64String(base64Encoded), true);
if (PacketWithAttachment.HasAllAttachment)
{
packet = PacketWithAttachment;
PacketWithAttachment = null;
}
}
else
HTTPManager.Logger.Warning("PollingTransport", "Received binary but no packet to attach to!");
}
else
{
packet = new Packet(Encoding.UTF8.GetString(resp.Data, idx, length));
}
if (packet != null)
{
try
{
OnPacket(packet);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("PollingTransport", "ParseResponseV3 - OnPacket", ex);
(Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
}
}
idx = endIdx + 1;
}
}
catch (Exception ex)
{
(Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
HTTPManager.Logger.Exception("PollingTransport", "ParseResponseV3", ex);
}
}
private int FindNextRecordSeparator(byte[] data, int startIdx)
{
for (int i = startIdx; i < data.Length; ++i)
{
if (data[i] == 0x1E)
return i;
}
return data.Length;
}
/// <summary>
/// Will parse the response, and send out the parsed packets.
/// </summary>
private void ParseResponseV2(HTTPResponse resp)
{
try
{
if (resp != null && resp.Data != null && resp.Data.Length >= 1)
{
int idx = 0;
while (idx < resp.Data.Length)
{
PayloadTypes type = PayloadTypes.Text;
int length = 0;
if (resp.Data[idx] < '0')
{
type = (PayloadTypes)resp.Data[idx++];
byte num = resp.Data[idx++];
while (num != 0xFF)
{
length = (length * 10) + num;
num = resp.Data[idx++];
}
}
else
{
byte next = resp.Data[idx++];
while (next != ':')
{
length = (length * 10) + (next - '0');
next = resp.Data[idx++];
}
// Because length can be different from the byte length, we have to do a little post-processing to support unicode characters.
int brackets = 0;
int tmpIdx = idx;
while (tmpIdx < idx + length)
{
if (resp.Data[tmpIdx] == '[')
brackets++;
else if (resp.Data[tmpIdx] == ']')
brackets--;
tmpIdx++;
}
if (brackets > 0)
{
while (brackets > 0)
{
if (resp.Data[tmpIdx] == '[')
brackets++;
else if (resp.Data[tmpIdx] == ']')
brackets--;
tmpIdx++;
}
length = tmpIdx - idx;
}
}
Packet packet = null;
switch (type)
{
case PayloadTypes.Text:
packet = new Packet(Encoding.UTF8.GetString(resp.Data, idx, length));
break;
case PayloadTypes.Binary:
if (PacketWithAttachment != null)
{
// First byte is the packet type. We can skip it, so we advance our idx and we also have
// to decrease length
idx++;
length--;
byte[] buffer = new byte[length];
Array.Copy(resp.Data, idx, buffer, 0, length);
PacketWithAttachment.AddAttachmentFromServer(buffer, true);
if (PacketWithAttachment.HasAllAttachment)
{
packet = PacketWithAttachment;
PacketWithAttachment = null;
}
}
break;
} // switch
if (packet != null)
{
try
{
OnPacket(packet);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("PollingTransport", "ParseResponseV2 - OnPacket", ex);
(Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
}
}
idx += length;
}// while
}
}
catch (Exception ex)
{
(Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
HTTPManager.Logger.Exception("PollingTransport", "ParseResponseV2", ex);
}
}
#endregion
}
}
#endif

View File

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

View File

@@ -0,0 +1,386 @@
#if !BESTHTTP_DISABLE_SOCKETIO
#if !BESTHTTP_DISABLE_WEBSOCKET
using System;
using System.Collections.Generic;
namespace BestHTTP.SocketIO.Transports
{
using BestHTTP.Connections;
using BestHTTP.WebSocket;
using Extensions;
/// <summary>
/// A transport implementation that can communicate with a SocketIO server.
/// </summary>
internal sealed class WebSocketTransport : ITransport
{
public TransportTypes Type { get { return TransportTypes.WebSocket; } }
public TransportStates State { get; private set; }
public SocketManager Manager { get; private set; }
public bool IsRequestInProgress { get { return false; } }
public bool IsPollingInProgress { get { return false; } }
public WebSocket Implementation { get; private set; }
private Packet PacketWithAttachment;
private byte[] Buffer;
public WebSocketTransport(SocketManager manager)
{
State = TransportStates.Closed;
Manager = manager;
}
#region Some ITransport Implementation
public void Open()
{
if (State != TransportStates.Closed)
return;
Uri uri = null;
string baseUrl = new UriBuilder(HTTPProtocolFactory.IsSecureProtocol(Manager.Uri) ? "wss" : "ws",
Manager.Uri.Host,
Manager.Uri.Port,
Manager.Uri.GetRequestPathAndQueryURL()).Uri.ToString();
string format = "{0}?EIO={1}&transport=websocket{3}";
if (Manager.Handshake != null)
format += "&sid={2}";
bool sendAdditionalQueryParams = !Manager.Options.QueryParamsOnlyForHandshake || (Manager.Options.QueryParamsOnlyForHandshake && Manager.Handshake == null);
uri = new Uri(string.Format(format,
baseUrl,
Manager.ProtocolVersion,
Manager.Handshake != null ? Manager.Handshake.Sid : string.Empty,
sendAdditionalQueryParams ? Manager.Options.BuildQueryParams() : string.Empty));
Implementation = new WebSocket(uri);
#if !UNITY_WEBGL || UNITY_EDITOR
Implementation.StartPingThread = true;
if (this.Manager.Options.HTTPRequestCustomizationCallback != null)
Implementation.OnInternalRequestCreated = (ws, internalRequest) => this.Manager.Options.HTTPRequestCustomizationCallback(this.Manager, internalRequest);
#endif
Implementation.OnOpen = OnOpen;
Implementation.OnMessage = OnMessage;
Implementation.OnBinary = OnBinary;
Implementation.OnError = OnError;
Implementation.OnClosed = OnClosed;
Implementation.Open();
State = TransportStates.Connecting;
}
/// <summary>
/// Closes the transport and cleans up resources.
/// </summary>
public void Close()
{
if (State == TransportStates.Closed)
return;
State = TransportStates.Closed;
if (Implementation != null)
Implementation.Close();
else
HTTPManager.Logger.Warning("WebSocketTransport", "Close - WebSocket Implementation already null!");
Implementation = null;
}
/// <summary>
/// Polling implementation. With WebSocket it's just a skeleton.
/// </summary>
public void Poll()
{
}
#endregion
#region WebSocket Events
/// <summary>
/// WebSocket implementation OnOpen event handler.
/// </summary>
private void OnOpen(WebSocket ws)
{
if (ws != Implementation)
return;
HTTPManager.Logger.Information("WebSocketTransport", "OnOpen");
State = TransportStates.Opening;
// Send a Probe packet to test the transport. If we receive back a pong with the same payload we can upgrade
if (Manager.UpgradingTransport == this)
Send(new Packet(TransportEventTypes.Ping, SocketIOEventTypes.Unknown, "/", "probe"));
}
/// <summary>
/// WebSocket implementation OnMessage event handler.
/// </summary>
private void OnMessage(WebSocket ws, string message)
{
if (ws != Implementation)
return;
if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
HTTPManager.Logger.Verbose("WebSocketTransport", "OnMessage: " + message);
Packet packet = null;
try
{
packet = new Packet(message);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocketTransport", "OnMessage Packet parsing", ex);
}
if (packet == null)
{
HTTPManager.Logger.Error("WebSocketTransport", "Message parsing failed. Message: " + message);
return;
}
try
{
if (packet.AttachmentCount == 0)
OnPacket(packet);
else
PacketWithAttachment = packet;
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocketTransport", "OnMessage OnPacket", ex);
}
}
/// <summary>
/// WebSocket implementation OnBinary event handler.
/// </summary>
private void OnBinary(WebSocket ws, byte[] data)
{
if (ws != Implementation)
return;
if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
HTTPManager.Logger.Verbose("WebSocketTransport", "OnBinary");
if (PacketWithAttachment != null)
{
switch(this.Manager.Options.ServerVersion)
{
case SupportedSocketIOVersions.v2: PacketWithAttachment.AddAttachmentFromServer(data, false); break;
case SupportedSocketIOVersions.v3: PacketWithAttachment.AddAttachmentFromServer(data, true); break;
default:
HTTPManager.Logger.Warning("WebSocketTransport", "Binary packet received while the server's version is Unknown. Set SocketOption's ServerVersion to the correct value to avoid packet mishandling!");
// Fall back to V2 by default.
this.Manager.Options.ServerVersion = SupportedSocketIOVersions.v2;
goto case SupportedSocketIOVersions.v2;
}
if (PacketWithAttachment.HasAllAttachment)
{
try
{
OnPacket(PacketWithAttachment);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocketTransport", "OnBinary", ex);
}
finally
{
PacketWithAttachment = null;
}
}
}
else
{
// Room for improvement: we received an unwanted binary message?
}
}
/// <summary>
/// WebSocket implementation OnError event handler.
/// </summary>
private void OnError(WebSocket ws, string error)
{
if (ws != Implementation)
return;
#if !UNITY_WEBGL || UNITY_EDITOR
if (string.IsNullOrEmpty(error))
{
switch (ws.InternalRequest.State)
{
// The request finished without any problem.
case HTTPRequestStates.Finished:
if (ws.InternalRequest.Response.IsSuccess || ws.InternalRequest.Response.StatusCode == 101)
error = string.Format("Request finished. Status Code: {0} Message: {1}", ws.InternalRequest.Response.StatusCode.ToString(), ws.InternalRequest.Response.Message);
else
error = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
ws.InternalRequest.Response.StatusCode,
ws.InternalRequest.Response.Message,
ws.InternalRequest.Response.DataAsText);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
error = "Request Finished with Error! : " + ws.InternalRequest.Exception != null ? (ws.InternalRequest.Exception.Message + " " + ws.InternalRequest.Exception.StackTrace) : string.Empty;
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
error = "Request Aborted!";
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
error = "Connection Timed Out!";
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
error = "Processing the request Timed Out!";
break;
}
}
#endif
if (Manager.UpgradingTransport != this)
(Manager as IManager).OnTransportError(this, error);
else
Manager.UpgradingTransport = null;
}
/// <summary>
/// WebSocket implementation OnClosed event handler.
/// </summary>
private void OnClosed(WebSocket ws, ushort code, string message)
{
if (ws != Implementation)
return;
HTTPManager.Logger.Information("WebSocketTransport", "OnClosed");
Close();
if (Manager.UpgradingTransport != this)
(Manager as IManager).TryToReconnect();
else
Manager.UpgradingTransport = null;
}
#endregion
#region Packet Sending Implementation
/// <summary>
/// A WebSocket implementation of the packet sending.
/// </summary>
public void Send(Packet packet)
{
if (State == TransportStates.Closed ||
State == TransportStates.Paused)
{
HTTPManager.Logger.Information("WebSocketTransport", string.Format("Send - State == {0}, skipping packet sending!", State));
return;
}
string encoded = packet.Encode();
if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
HTTPManager.Logger.Verbose("WebSocketTransport", "Send: " + encoded);
if (packet.AttachmentCount != 0 || (packet.Attachments != null && packet.Attachments.Count != 0))
{
if (packet.Attachments == null)
throw new ArgumentException("packet.Attachments are null!");
if (packet.AttachmentCount != packet.Attachments.Count)
throw new ArgumentException("packet.AttachmentCount != packet.Attachments.Count. Use the packet.AddAttachment function to add data to a packet!");
}
Implementation.Send(encoded);
if (packet.AttachmentCount != 0)
{
int maxLength = packet.Attachments[0].Length + 1;
for (int cv = 1; cv < packet.Attachments.Count; ++cv)
if ((packet.Attachments[cv].Length + 1) > maxLength)
maxLength = packet.Attachments[cv].Length + 1;
if (Buffer == null || Buffer.Length < maxLength)
Array.Resize(ref Buffer, maxLength);
for (int i = 0; i < packet.AttachmentCount; i++)
{
Buffer[0] = (byte)TransportEventTypes.Message;
Array.Copy(packet.Attachments[i], 0, Buffer, 1, packet.Attachments[i].Length);
Implementation.Send(Buffer, 0, (ulong)packet.Attachments[i].Length + 1UL);
}
}
}
/// <summary>
/// A WebSocket implementation of the packet sending.
/// </summary>
public void Send(List<Packet> packets)
{
for (int i = 0; i < packets.Count; ++i)
Send(packets[i]);
packets.Clear();
}
#endregion
#region Packet Handling
/// <summary>
/// Will only process packets that need to upgrade. All other packets are passed to the Manager.
/// </summary>
private void OnPacket(Packet packet)
{
switch (packet.TransportEvent)
{
case TransportEventTypes.Open:
if (this.State != TransportStates.Opening)
HTTPManager.Logger.Warning("WebSocketTransport", "Received 'Open' packet while state is '" + State.ToString() + "'");
else
State = TransportStates.Open;
goto default;
case TransportEventTypes.Pong:
// Answer for a Ping Probe.
if (packet.Payload == "probe")
{
State = TransportStates.Open;
(Manager as IManager).OnTransportProbed(this);
}
goto default;
default:
if (Manager.UpgradingTransport != this)
(Manager as IManager).OnPacket(packet);
break;
}
}
#endregion
}
}
#endif
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 28075d7868e0f0845ab98be53d258c85
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: