123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- #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();
- // SaveLocal 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());
- // SaveLocal 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
|