#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; /// /// A transport implementation that can communicate with a SocketIO server. /// 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; } /// /// Closes the transport and cleans up resources. /// 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; } /// /// Polling implementation. With WebSocket it's just a skeleton. /// public void Poll() { } #endregion #region WebSocket Events /// /// WebSocket implementation OnOpen event handler. /// 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")); } /// /// WebSocket implementation OnMessage event handler. /// 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); } } /// /// WebSocket implementation OnBinary event handler. /// 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? } } /// /// WebSocket implementation OnError event handler. /// 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; } /// /// WebSocket implementation OnClosed event handler. /// 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 /// /// A WebSocket implementation of the packet sending. /// 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); } } } /// /// A WebSocket implementation of the packet sending. /// public void Send(List packets) { for (int i = 0; i < packets.Count; ++i) Send(packets[i]); packets.Clear(); } #endregion #region Packet Handling /// /// Will only process packets that need to upgrade. All other packets are passed to the Manager. /// 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