#if !BESTHTTP_DISABLE_WEBSOCKET
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using BestHTTP.Extensions;
#if UNITY_WEBGL && !UNITY_EDITOR
using System.Runtime.InteropServices;
#else
using BestHTTP.WebSocket.Frames;
using BestHTTP.WebSocket.Extensions;
#endif
namespace BestHTTP.WebSocket
{
///
/// States of the underlying browser's WebSocket implementation's state.
///
public enum WebSocketStates : byte
{
Connecting = 0,
Open = 1,
Closing = 2,
Closed = 3,
Unknown
};
public delegate void OnWebSocketOpenDelegate(WebSocket webSocket);
public delegate void OnWebSocketMessageDelegate(WebSocket webSocket, string message);
public delegate void OnWebSocketBinaryDelegate(WebSocket webSocket, byte[] data);
public delegate void OnWebSocketClosedDelegate(WebSocket webSocket, UInt16 code, string message);
public delegate void OnWebSocketErrorDelegate(WebSocket webSocket, Exception ex);
public delegate void OnWebSocketErrorDescriptionDelegate(WebSocket webSocket, string reason);
#if (!UNITY_WEBGL || UNITY_EDITOR)
public delegate void OnWebSocketIncompleteFrameDelegate(WebSocket webSocket, WebSocketFrameReader frame);
#else
delegate void OnWebGLWebSocketOpenDelegate(uint id);
delegate void OnWebGLWebSocketTextDelegate(uint id, string text);
delegate void OnWebGLWebSocketBinaryDelegate(uint id, IntPtr pBuffer, int length);
delegate void OnWebGLWebSocketErrorDelegate(uint id, string error);
delegate void OnWebGLWebSocketCloseDelegate(uint id, int code, string reason);
#endif
public sealed class WebSocket
{
#region Properties
#if !UNITY_WEBGL || UNITY_EDITOR
public WebSocketStates State { get; private set; }
#else
public WebSocketStates State { get { return ImplementationId != 0 ? WS_GetState(ImplementationId) : WebSocketStates.Unknown; } }
#endif
///
/// The connection to the WebSocket server is open.
///
public bool IsOpen
{
get
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
return webSocket != null && !webSocket.IsClosed;
#else
return ImplementationId != 0 && WS_GetState(ImplementationId) == WebSocketStates.Open;
#endif
}
}
public int BufferedAmount
{
get
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
return webSocket.BufferedAmount;
#else
return WS_GetBufferedAmount(ImplementationId);
#endif
}
}
#if (!UNITY_WEBGL || UNITY_EDITOR)
///
/// Set to true to start a new thread to send Pings to the WebSocket server
///
public bool StartPingThread { get; set; }
///
/// The delay between two Pings in millisecs. Minimum value is 100, default is 1000.
///
public int PingFrequency { get; set; }
///
/// If StartPingThread set to true, the plugin will close the connection and emit an OnError/OnErrorDesc event if no
/// message is received from the server in the given time. Its default value is 10 sec.
///
public TimeSpan CloseAfterNoMesssage { get; set; }
///
/// The internal HTTPRequest object.
///
public HTTPRequest InternalRequest { get; private set; }
///
/// IExtension implementations the plugin will negotiate with the server to use.
///
public IExtension[] Extensions { get; private set; }
///
/// Latency calculated from the ping-pong message round-trip times.
///
public int Latency { get { return webSocket.Latency; } }
#endif
///
/// Called when the connection to the WebSocket server is established.
///
public OnWebSocketOpenDelegate OnOpen;
///
/// Called when a new textual message is received from the server.
///
public OnWebSocketMessageDelegate OnMessage;
///
/// Called when a new binary message is received from the server.
///
public OnWebSocketBinaryDelegate OnBinary;
///
/// Called when the WebSocket connection is closed.
///
public OnWebSocketClosedDelegate OnClosed;
///
/// Called when an error is encountered. The Exception parameter may be null.
///
public OnWebSocketErrorDelegate OnError;
///
/// Called when an error is encountered. The parameter will be the description of the error.
///
public OnWebSocketErrorDescriptionDelegate OnErrorDesc;
#if (!UNITY_WEBGL || UNITY_EDITOR)
///
/// Called when an incomplete frame received. No attempt will be made to reassemble these fragments internally, and no reference are stored after this event to this frame.
///
public OnWebSocketIncompleteFrameDelegate OnIncompleteFrame;
#endif
#endregion
#region Private Fields
#if (!UNITY_WEBGL || UNITY_EDITOR)
///
/// Indicates wheter we sent out the connection request to the server.
///
private bool requestSent;
///
/// The internal WebSocketResponse object
///
private WebSocketResponse webSocket;
#else
internal static Dictionary WebSockets = new Dictionary();
private uint ImplementationId;
private Uri Uri;
private string Protocol;
#endif
#endregion
#region Constructors
///
/// Creates a WebSocket instance from the given uri.
///
/// The uri of the WebSocket server
public WebSocket(Uri uri)
:this(uri, string.Empty, string.Empty)
{
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_GZIP
this.Extensions = new IExtension[] { new PerMessageCompression(/*compression level: */ Decompression.Zlib.CompressionLevel.Default,
/*clientNoContextTakeover: */ false,
/*serverNoContextTakeover: */ false,
/*clientMaxWindowBits: */ Decompression.Zlib.ZlibConstants.WindowBitsMax,
/*desiredServerMaxWindowBits: */ Decompression.Zlib.ZlibConstants.WindowBitsMax,
/*minDatalengthToCompress: */ 5) };
#endif
}
///
/// Creates a WebSocket instance from the given uri, protocol and origin.
///
/// The uri of the WebSocket server
/// Servers that are not intended to process input from any web page but only for certain sites SHOULD verify the |Origin| field is an origin they expect.
/// If the origin indicated is unacceptable to the server, then it SHOULD respond to the WebSocket handshake with a reply containing HTTP 403 Forbidden status code.
/// The application-level protocol that the client want to use(eg. "chat", "leaderboard", etc.). Can be null or empty string if not used.
/// Optional IExtensions implementations
public WebSocket(Uri uri, string origin, string protocol
#if !UNITY_WEBGL || UNITY_EDITOR
, params IExtension[] extensions
#endif
)
{
string scheme = HTTPProtocolFactory.IsSecureProtocol(uri) ? "wss" : "ws";
int port = uri.Port != -1 ? uri.Port : (scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? 443 : 80);
// Somehow if i use the UriBuilder it's not the same as if the uri is constructed from a string...
//uri = new UriBuilder(uri.Scheme, uri.Host, uri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ? 443 : 80, uri.PathAndQuery).Uri;
uri = new Uri(scheme + "://" + uri.Host + ":" + port + uri.GetRequestPathAndQueryURL());
#if !UNITY_WEBGL || UNITY_EDITOR
// Set up some default values.
this.PingFrequency = 1000;
this.CloseAfterNoMesssage = TimeSpan.FromSeconds(10);
InternalRequest = new HTTPRequest(uri, OnInternalRequestCallback);
// Called when the regular GET request is successfully upgraded to WebSocket
InternalRequest.OnUpgraded = OnInternalRequestUpgraded;
//http://tools.ietf.org/html/rfc6455#section-4
//The request MUST contain a |Host| header field whose value contains /host/ plus optionally ":" followed by /port/ (when not using the default port).
if (uri.Port != 80)
InternalRequest.SetHeader("Host", uri.Host + ":" + uri.Port);
else
InternalRequest.SetHeader("Host", uri.Host);
// The request MUST contain an |Upgrade| header field whose value MUST include the "websocket" keyword.
InternalRequest.SetHeader("Upgrade", "websocket");
// The request MUST contain a |Connection| header field whose value MUST include the "Upgrade" token.
InternalRequest.SetHeader("Connection", "keep-alive, Upgrade");
// The request MUST include a header field with the name |Sec-WebSocket-Key|. The value of this header field MUST be a nonce consisting of a
// randomly selected 16-byte value that has been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection.
InternalRequest.SetHeader("Sec-WebSocket-Key", GetSecKey(new object[] { this, InternalRequest, uri, new object() }));
// The request MUST include a header field with the name |Origin| [RFC6454] if the request is coming from a browser client.
// If the connection is from a non-browser client, the request MAY include this header field if the semantics of that client match the use-case described here for browser clients.
// More on Origin Considerations: http://tools.ietf.org/html/rfc6455#section-10.2
if (!string.IsNullOrEmpty(origin))
InternalRequest.SetHeader("Origin", origin);
// The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13.
InternalRequest.SetHeader("Sec-WebSocket-Version", "13");
if (!string.IsNullOrEmpty(protocol))
InternalRequest.SetHeader("Sec-WebSocket-Protocol", protocol);
// Disable caching
InternalRequest.SetHeader("Cache-Control", "no-cache");
InternalRequest.SetHeader("Pragma", "no-cache");
this.Extensions = extensions;
#if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
InternalRequest.DisableCache = true;
InternalRequest.DisableRetry = true;
#endif
InternalRequest.TryToMinimizeTCPLatency = true;
#if !BESTHTTP_DISABLE_PROXY
// WebSocket is not a request-response based protocol, so we need a 'tunnel' through the proxy
if (HTTPManager.Proxy != null)
InternalRequest.Proxy = new HTTPProxy(HTTPManager.Proxy.Address,
HTTPManager.Proxy.Credentials,
false, /*turn on 'tunneling'*/
false, /*sendWholeUri*/
HTTPManager.Proxy.NonTransparentForHTTPS);
#endif
#else
this.Uri = uri;
this.Protocol = protocol;
#endif
HTTPManager.Setup();
}
#endregion
#region Request Callbacks
#if (!UNITY_WEBGL || UNITY_EDITOR)
private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp)
{
string reason = string.Empty;
switch (req.State)
{
case HTTPRequestStates.Finished:
if (resp.IsSuccess || resp.StatusCode == 101)
{
// The request finished without any problem.
HTTPManager.Logger.Information("WebSocket", string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message));
return;
}
else
reason = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
resp.StatusCode,
resp.Message,
resp.DataAsText);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
reason = "Request Finished with Error! " + (req.Exception != null ? ("Exception: " + req.Exception.Message + req.Exception.StackTrace) : string.Empty);
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
reason = "Request Aborted!";
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
reason = "Connection Timed Out!";
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
reason = "Processing the request Timed Out!";
break;
default:
return;
}
if (this.State != WebSocketStates.Connecting || !string.IsNullOrEmpty(reason))
{
if (OnError != null)
OnError(this, req.Exception);
if (OnErrorDesc != null)
OnErrorDesc(this, reason);
if (OnError == null && OnErrorDesc == null)
HTTPManager.Logger.Error("WebSocket", reason);
}
else if (OnClosed != null)
OnClosed(this, (ushort)WebSocketStausCodes.NormalClosure, "Closed while opening");
if (!req.IsKeepAlive && resp != null && resp is WebSocketResponse)
(resp as WebSocketResponse).CloseStream();
}
private void OnInternalRequestUpgraded(HTTPRequest req, HTTPResponse resp)
{
webSocket = resp as WebSocketResponse;
if (webSocket == null)
{
if (OnError != null)
OnError(this, req.Exception);
if (OnErrorDesc != null)
{
string reason = string.Empty;
if (req.Exception != null)
reason = req.Exception.Message + " " + req.Exception.StackTrace;
OnErrorDesc(this, reason);
}
this.State = WebSocketStates.Closed;
return;
}
// If Close called while we connected
if (this.State == WebSocketStates.Closed)
{
webSocket.CloseStream();
return;
}
webSocket.WebSocket = this;
if (this.Extensions != null)
{
for (int i = 0; i < this.Extensions.Length; ++i)
{
var ext = this.Extensions[i];
try
{
if (ext != null && !ext.ParseNegotiation(webSocket))
this.Extensions[i] = null; // Keep extensions only that successfully negotiated
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "ParseNegotiation", ex);
// Do not try to use a defective extension in the future
this.Extensions[i] = null;
}
}
}
this.State = WebSocketStates.Open;
if (OnOpen != null)
{
try
{
OnOpen(this);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnOpen", ex);
}
}
webSocket.OnText = (ws, msg) =>
{
if (OnMessage != null)
OnMessage(this, msg);
};
webSocket.OnBinary = (ws, bin) =>
{
if (OnBinary != null)
OnBinary(this, bin);
};
webSocket.OnClosed = (ws, code, msg) =>
{
this.State = WebSocketStates.Closed;
if (OnClosed != null)
OnClosed(this, code, msg);
};
if (OnIncompleteFrame != null)
webSocket.OnIncompleteFrame = (ws, frame) =>
{
if (OnIncompleteFrame != null)
OnIncompleteFrame(this, frame);
};
if (StartPingThread)
webSocket.StartPinging(Math.Max(PingFrequency, 100));
webSocket.StartReceive();
}
#endif
#endregion
#region Public Interface
///
/// Start the opening process.
///
public void Open()
{
#if (!UNITY_WEBGL || UNITY_EDITOR)
if (requestSent)
throw new InvalidOperationException("Open already called! You can't reuse this WebSocket instance!");
if (this.Extensions != null)
{
try
{
for (int i = 0; i < this.Extensions.Length; ++i)
{
var ext = this.Extensions[i];
if (ext != null)
ext.AddNegotiation(InternalRequest);
}
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "Open", ex);
}
}
InternalRequest.Send();
requestSent = true;
this.State = WebSocketStates.Connecting;
#else
try
{
ImplementationId = WS_Create(this.Uri.OriginalString, this.Protocol, OnOpenCallback, OnTextCallback, OnBinaryCallback, OnErrorCallback, OnCloseCallback);
WebSockets.Add(ImplementationId, this);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "Open", ex);
}
#endif
}
///
/// It will send the given message to the server in one frame.
///
public void Send(string message)
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Send(message);
#else
WS_Send_String(this.ImplementationId, message);
#endif
}
///
/// It will send the given data to the server in one frame.
///
public void Send(byte[] buffer)
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Send(buffer);
#else
WS_Send_Binary(this.ImplementationId, buffer, 0, buffer.Length);
#endif
}
///
/// Will send count bytes from a byte array, starting from offset.
///
public void Send(byte[] buffer, ulong offset, ulong count)
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Send(buffer, offset, count);
#else
WS_Send_Binary(this.ImplementationId, buffer, (int)offset, (int)count);
#endif
}
#if (!UNITY_WEBGL || UNITY_EDITOR)
///
/// It will send the given frame to the server.
///
public void Send(WebSocketFrame frame)
{
if (IsOpen)
webSocket.Send(frame);
}
#endif
///
/// It will initiate the closing of the connection to the server.
///
public void Close()
{
if (State >= WebSocketStates.Closing)
return;
#if !UNITY_WEBGL || UNITY_EDITOR
if (this.State == WebSocketStates.Connecting)
{
this.State = WebSocketStates.Closed;
if (OnClosed != null)
OnClosed(this, (ushort)WebSocketStausCodes.NoStatusCode, string.Empty);
}
else
{
this.State = WebSocketStates.Closing;
webSocket.Close();
}
#else
WS_Close(this.ImplementationId, 1000, "Bye!");
#endif
}
///
/// It will initiate the closing of the connection to the server sending the given code and message.
///
public void Close(UInt16 code, string message)
{
if (!IsOpen)
return;
#if (!UNITY_WEBGL || UNITY_EDITOR)
webSocket.Close(code, message);
#else
WS_Close(this.ImplementationId, code, message);
#endif
}
public static byte[] EncodeCloseData(UInt16 code, string message)
{
//If there is a body, the first two bytes of the body MUST be a 2-byte unsigned integer
// (in network byte order) representing a status code with value /code/ defined in Section 7.4 (http://tools.ietf.org/html/rfc6455#section-7.4). Following the 2-byte integer,
// the body MAY contain UTF-8-encoded data with value /reason/, the interpretation of which is not defined by this specification.
// This data is not necessarily human readable but may be useful for debugging or passing information relevant to the script that opened the connection.
int msgLen = Encoding.UTF8.GetByteCount(message);
using (MemoryStream ms = new MemoryStream(2 + msgLen))
{
byte[] buff = BitConverter.GetBytes(code);
if (BitConverter.IsLittleEndian)
Array.Reverse(buff, 0, buff.Length);
ms.Write(buff, 0, buff.Length);
buff = Encoding.UTF8.GetBytes(message);
ms.Write(buff, 0, buff.Length);
return ms.ToArray();
}
}
#endregion
#region Private Helpers
#if !UNITY_WEBGL || UNITY_EDITOR
private string GetSecKey(object[] from)
{
byte[] keys = new byte[16];
int pos = 0;
for (int i = 0; i < from.Length; ++i)
{
byte[] hash = BitConverter.GetBytes((Int32)from[i].GetHashCode());
for (int cv = 0; cv < hash.Length && pos < keys.Length; ++cv)
keys[pos++] = hash[cv];
}
return Convert.ToBase64String(keys);
}
#endif
#endregion
#region WebGL Static Callbacks
#if UNITY_WEBGL && !UNITY_EDITOR
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketOpenDelegate))]
static void OnOpenCallback(uint id)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnOpen != null)
{
try
{
ws.OnOpen(ws);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnOpen", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnOpenCallback - No WebSocket found for id: " + id.ToString());
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketTextDelegate))]
static void OnTextCallback(uint id, string text)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnMessage != null)
{
try
{
ws.OnMessage(ws, text);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnMessage", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnTextCallback - No WebSocket found for id: " + id.ToString());
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketBinaryDelegate))]
static void OnBinaryCallback(uint id, IntPtr pBuffer, int length)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
if (ws.OnBinary != null)
{
try
{
byte[] buffer = new byte[length];
// Copy data from the 'unmanaged' memory to managed memory. Buffer will be reclaimed by the GC.
Marshal.Copy(pBuffer, buffer, 0, length);
ws.OnBinary(ws, buffer);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnBinary", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnBinaryCallback - No WebSocket found for id: " + id.ToString());
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketErrorDelegate))]
static void OnErrorCallback(uint id, string error)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
WebSockets.Remove(id);
if (ws.OnError != null)
{
try
{
ws.OnError(ws, new Exception(error));
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnError", ex);
}
}
if (ws.OnErrorDesc != null)
{
try
{
ws.OnErrorDesc(ws, error);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnErrorDesc", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnErrorCallback - No WebSocket found for id: " + id.ToString());
try
{
WS_Release(id);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "WS_Release", ex);
}
}
[AOT.MonoPInvokeCallback(typeof(OnWebGLWebSocketCloseDelegate))]
static void OnCloseCallback(uint id, int code, string reason)
{
WebSocket ws;
if (WebSockets.TryGetValue(id, out ws))
{
WebSockets.Remove(id);
if (ws.OnClosed != null)
{
try
{
ws.OnClosed(ws, (ushort)code, reason);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "OnClosed", ex);
}
}
}
else
HTTPManager.Logger.Warning("WebSocket", "OnCloseCallback - No WebSocket found for id: " + id.ToString());
try
{
WS_Release(id);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("WebSocket", "WS_Release", ex);
}
}
#endif
#endregion
#region WebGL Interface
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
static extern uint WS_Create(string url, string protocol, OnWebGLWebSocketOpenDelegate onOpen, OnWebGLWebSocketTextDelegate onText, OnWebGLWebSocketBinaryDelegate onBinary, OnWebGLWebSocketErrorDelegate onError, OnWebGLWebSocketCloseDelegate onClose);
[DllImport("__Internal")]
static extern WebSocketStates WS_GetState(uint id);
[DllImport("__Internal")]
static extern int WS_GetBufferedAmount(uint id);
[DllImport("__Internal")]
static extern int WS_Send_String(uint id, string strData);
[DllImport("__Internal")]
static extern int WS_Send_Binary(uint id, byte[] buffer, int pos, int length);
[DllImport("__Internal")]
static extern void WS_Close(uint id, ushort code, string reason);
[DllImport("__Internal")]
static extern void WS_Release(uint id);
#endif
#endregion
}
}
#endif