Packet.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. #if !BESTHTTP_DISABLE_SOCKETIO
  2. using System.Text;
  3. namespace BestHTTP.SocketIO
  4. {
  5. using System;
  6. using System.Collections.Generic;
  7. using BestHTTP.JSON;
  8. public sealed class Packet
  9. {
  10. private enum PayloadTypes : byte
  11. {
  12. Textual = 0,
  13. Binary = 1
  14. }
  15. public const string Placeholder = "_placeholder";
  16. #region Public properties
  17. /// <summary>
  18. /// Event type of this packet on the transport layer.
  19. /// </summary>
  20. public TransportEventTypes TransportEvent { get; private set; }
  21. /// <summary>
  22. /// The packet's type in the Socket.IO protocol.
  23. /// </summary>
  24. public SocketIOEventTypes SocketIOEvent { get; private set; }
  25. /// <summary>
  26. /// How many attachment should have this packet.
  27. /// </summary>
  28. public int AttachmentCount { get; private set; }
  29. /// <summary>
  30. /// The internal ack-id of this packet.
  31. /// </summary>
  32. public int Id { get; private set; }
  33. /// <summary>
  34. /// The sender namespace's name.
  35. /// </summary>
  36. public string Namespace { get; private set; }
  37. /// <summary>
  38. /// The payload as a Json string.
  39. /// </summary>
  40. public string Payload { get; private set; }
  41. /// <summary>
  42. /// The decoded event name from the payload string.
  43. /// </summary>
  44. public string EventName { get; private set; }
  45. /// <summary>
  46. /// All binary data attached to this event.
  47. /// </summary>
  48. public List<byte[]> Attachments { get { return attachments; } set { attachments = value; AttachmentCount = attachments != null ? attachments.Count : 0; } }
  49. private List<byte[]> attachments;
  50. /// <summary>
  51. /// Property to check whether all attachments are received to this packet.
  52. /// </summary>
  53. public bool HasAllAttachment { get { return Attachments != null && Attachments.Count == AttachmentCount; } }
  54. /// <summary>
  55. /// True if it's already decoded. The DecodedArgs still can be null after the Decode call.
  56. /// </summary>
  57. public bool IsDecoded { get; private set; }
  58. /// <summary>
  59. /// The decoded arguments from the result of a Json string -> c# object convert.
  60. /// </summary>
  61. public object[] DecodedArgs { get; private set; }
  62. #endregion
  63. #region Constructors
  64. /// <summary>
  65. /// Internal constructor. Don't use it directly!
  66. /// </summary>
  67. internal Packet()
  68. {
  69. this.TransportEvent = TransportEventTypes.Unknown;
  70. this.SocketIOEvent = SocketIOEventTypes.Unknown;
  71. this.Payload = string.Empty;
  72. }
  73. /// <summary>
  74. /// Internal constructor. Don't use it directly!
  75. /// </summary>
  76. internal Packet(string from)
  77. {
  78. this.Parse(from);
  79. }
  80. /// <summary>
  81. /// Internal constructor. Don't use it directly!
  82. /// </summary>
  83. public Packet(TransportEventTypes transportEvent, SocketIOEventTypes packetType, string nsp, string payload, int attachment = 0, int id = 0)
  84. {
  85. this.TransportEvent = transportEvent;
  86. this.SocketIOEvent = packetType;
  87. this.Namespace = nsp;
  88. this.Payload = payload;
  89. this.AttachmentCount = attachment;
  90. this.Id = id;
  91. }
  92. #endregion
  93. #region Public Functions
  94. public object[] Decode(BestHTTP.SocketIO.JsonEncoders.IJsonEncoder encoder)
  95. {
  96. if (IsDecoded || encoder == null)
  97. return DecodedArgs;
  98. IsDecoded = true;
  99. if (string.IsNullOrEmpty(Payload))
  100. return DecodedArgs;
  101. List<object> decoded = encoder.Decode(Payload);
  102. if (decoded != null && decoded.Count > 0)
  103. {
  104. if (this.SocketIOEvent == SocketIOEventTypes.Ack || this.SocketIOEvent == SocketIOEventTypes.BinaryAck)
  105. DecodedArgs = decoded.ToArray();
  106. else
  107. {
  108. decoded.RemoveAt(0);
  109. DecodedArgs = decoded.ToArray();
  110. }
  111. }
  112. return DecodedArgs;
  113. }
  114. /// <summary>
  115. /// Will set and return with the EventName from the packet's Payload string.
  116. /// </summary>
  117. public string DecodeEventName()
  118. {
  119. // Already decoded
  120. if (!string.IsNullOrEmpty(EventName))
  121. return EventName;
  122. // No Payload to decode
  123. if (string.IsNullOrEmpty(Payload))
  124. return string.Empty;
  125. // Not array encoded, we can't decode
  126. if (Payload[0] != '[')
  127. return string.Empty;
  128. int idx = 1;
  129. // Search for the string-begin mark( ' or " chars)
  130. while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'')
  131. idx++;
  132. // Reached the end of the string
  133. if (Payload.Length <= idx)
  134. return string.Empty;
  135. int startIdx = ++idx;
  136. // Search for the trailing mark of the string
  137. while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'')
  138. idx++;
  139. // Reached the end of the string
  140. if (Payload.Length <= idx)
  141. return string.Empty;
  142. return EventName = Payload.Substring(startIdx, idx - startIdx);
  143. }
  144. public string RemoveEventName(bool removeArrayMarks)
  145. {
  146. // No Payload to decode
  147. if (string.IsNullOrEmpty(Payload))
  148. return string.Empty;
  149. // Not array encoded, we can't decode
  150. if (Payload[0] != '[')
  151. return string.Empty;
  152. int idx = 1;
  153. // Search for the string-begin mark( ' or " chars)
  154. while (Payload.Length > idx && Payload[idx] != '"' && Payload[idx] != '\'')
  155. idx++;
  156. // Reached the end of the string
  157. if (Payload.Length <= idx)
  158. return string.Empty;
  159. int startIdx = idx;
  160. // Search for end of first element, or end of the array marks
  161. while (Payload.Length > idx && Payload[idx] != ',' && Payload[idx] != ']')
  162. idx++;
  163. // Reached the end of the string
  164. if (Payload.Length <= ++idx)
  165. return string.Empty;
  166. string payload = Payload.Remove(startIdx, idx - startIdx);
  167. if (removeArrayMarks)
  168. payload = payload.Substring(1, payload.Length - 2);
  169. return payload;
  170. }
  171. /// <summary>
  172. /// Will switch the "{'_placeholder':true,'num':X}" to a the index num X.
  173. /// </summary>
  174. /// <returns>True if successfully reconstructed, false otherwise.</returns>
  175. public bool ReconstructAttachmentAsIndex()
  176. {
  177. //"452-["multiImage",{"image":true,"buffer1":{"_placeholder":true,"num":0},"buffer2":{"_placeholder":true,"num":1}}]"
  178. return PlaceholderReplacer((json, obj) =>
  179. {
  180. int idx = Convert.ToInt32(obj["num"]);
  181. this.Payload = this.Payload.Replace(json, idx.ToString());
  182. this.IsDecoded = false;
  183. });
  184. }
  185. /// <summary>
  186. /// Will switch the "{'_placeholder':true,'num':X}" to a the data as a base64 encoded string.
  187. /// </summary>
  188. /// <returns>True if successfully reconstructed, false otherwise.</returns>
  189. public bool ReconstructAttachmentAsBase64()
  190. {
  191. //"452-["multiImage",{"image":true,"buffer1":{"_placeholder":true,"num":0},"buffer2":{"_placeholder":true,"num":1}}]"
  192. if (!HasAllAttachment)
  193. return false;
  194. return PlaceholderReplacer((json, obj) =>
  195. {
  196. int idx = Convert.ToInt32(obj["num"]);
  197. this.Payload = this.Payload.Replace(json, string.Format("\"{0}\"", Convert.ToBase64String(this.Attachments[idx])));
  198. this.IsDecoded = false;
  199. });
  200. }
  201. #endregion
  202. #region Internal Functions
  203. /// <summary>
  204. /// Parse the packet from a server sent textual data. The Payload will be the raw json string.
  205. /// </summary>
  206. internal void Parse(string from)
  207. {
  208. int idx = 0;
  209. this.TransportEvent = (TransportEventTypes)ToInt(from[idx++]);
  210. if (from.Length > idx && ToInt(from[idx]) >= 0)
  211. this.SocketIOEvent = (SocketIOEventTypes)ToInt(from[idx++]);
  212. else
  213. this.SocketIOEvent = SocketIOEventTypes.Unknown;
  214. // Parse Attachment
  215. if (this.SocketIOEvent == SocketIOEventTypes.BinaryEvent || this.SocketIOEvent == SocketIOEventTypes.BinaryAck)
  216. {
  217. int endIdx = from.IndexOf('-', idx);
  218. if (endIdx == -1)
  219. endIdx = from.Length;
  220. int attachment = 0;
  221. int.TryParse(from.Substring(idx, endIdx - idx), out attachment);
  222. this.AttachmentCount = attachment;
  223. idx = endIdx + 1;
  224. }
  225. // Parse Namespace
  226. if (from.Length > idx && from[idx] == '/')
  227. {
  228. int endIdx = from.IndexOf(',', idx);
  229. if (endIdx == -1)
  230. endIdx = from.Length;
  231. this.Namespace = from.Substring(idx, endIdx - idx);
  232. idx = endIdx + 1;
  233. }
  234. else
  235. this.Namespace = "/";
  236. // Parse Id
  237. if (from.Length > idx && ToInt(from[idx]) >= 0)
  238. {
  239. int startIdx = idx++;
  240. while (from.Length > idx && ToInt(from[idx]) >= 0)
  241. idx++;
  242. int id = 0;
  243. int.TryParse(from.Substring(startIdx, idx - startIdx), out id);
  244. this.Id = id;
  245. }
  246. // What left is the payload data
  247. if (from.Length > idx)
  248. this.Payload = from.Substring(idx);
  249. else
  250. this.Payload = string.Empty;
  251. }
  252. /// <summary>
  253. /// Custom function instead of char.GetNumericValue, as it throws an error under WebGL using the new 4.x runtime.
  254. /// It will return the value of the char if it's a numeric one, otherwise -1.
  255. /// </summary>
  256. private int ToInt(char ch)
  257. {
  258. int charValue = Convert.ToInt32(ch);
  259. int num = charValue - '0';
  260. if (num < 0 || num > 9)
  261. return -1;
  262. return num;
  263. }
  264. /// <summary>
  265. /// Encodes this packet to a Socket.IO formatted string.
  266. /// </summary>
  267. internal string Encode()
  268. {
  269. StringBuilder builder = new StringBuilder();
  270. // Set to Message if not set, and we are sending attachments
  271. if (this.TransportEvent == TransportEventTypes.Unknown && this.AttachmentCount > 0)
  272. this.TransportEvent = TransportEventTypes.Message;
  273. if (this.TransportEvent != TransportEventTypes.Unknown)
  274. builder.Append(((int)this.TransportEvent).ToString());
  275. // Set to BinaryEvent if not set, and we are sending attachments
  276. if (this.SocketIOEvent == SocketIOEventTypes.Unknown && this.AttachmentCount > 0)
  277. this.SocketIOEvent = SocketIOEventTypes.BinaryEvent;
  278. if (this.SocketIOEvent != SocketIOEventTypes.Unknown)
  279. builder.Append(((int)this.SocketIOEvent).ToString());
  280. if (this.SocketIOEvent == SocketIOEventTypes.BinaryEvent || this.SocketIOEvent == SocketIOEventTypes.BinaryAck)
  281. {
  282. builder.Append(this.AttachmentCount.ToString());
  283. builder.Append("-");
  284. }
  285. // Add the namespace. If there is any other then the root nsp ("/")
  286. // then we have to add a trailing "," if we have more data.
  287. bool nspAdded = false;
  288. if (this.Namespace != "/")
  289. {
  290. builder.Append(this.Namespace);
  291. nspAdded = true;
  292. }
  293. // ack id, if any
  294. if (this.Id != 0)
  295. {
  296. if (nspAdded)
  297. {
  298. builder.Append(",");
  299. nspAdded = false;
  300. }
  301. builder.Append(this.Id.ToString());
  302. }
  303. // payload
  304. if (!string.IsNullOrEmpty(this.Payload))
  305. {
  306. if (nspAdded)
  307. {
  308. builder.Append(",");
  309. nspAdded = false;
  310. }
  311. builder.Append(this.Payload);
  312. }
  313. return builder.ToString();
  314. }
  315. /// <summary>
  316. /// Encodes this packet to a Socket.IO formatted byte array.
  317. /// </summary>
  318. internal byte[] EncodeBinary()
  319. {
  320. if (AttachmentCount != 0 || (Attachments != null && Attachments.Count != 0))
  321. {
  322. if (Attachments == null)
  323. throw new ArgumentException("packet.Attachments are null!");
  324. if (AttachmentCount != Attachments.Count)
  325. throw new ArgumentException("packet.AttachmentCount != packet.Attachments.Count. Use the packet.AddAttachment function to add data to a packet!");
  326. }
  327. // Encode it as usual
  328. string encoded = Encode();
  329. // Convert it to a byte[]
  330. byte[] payload = Encoding.UTF8.GetBytes(encoded);
  331. // Encode it to a message
  332. byte[] buffer = EncodeData(payload, PayloadTypes.Textual, null);
  333. // If there is any attachment, convert them too, and append them after each other
  334. if (AttachmentCount != 0)
  335. {
  336. int idx = buffer.Length;
  337. // List to temporarily hold the converted attachments
  338. List<byte[]> attachmentDatas = new List<byte[]>(AttachmentCount);
  339. // The sum size of the converted attachments to be able to resize our buffer only once. This way we can avoid some GC garbage
  340. int attachmentDataSize = 0;
  341. // Encode our attachments, and store them in our list
  342. for (int i = 0; i < AttachmentCount; i++)
  343. {
  344. byte[] tmpBuff = EncodeData(Attachments[i], PayloadTypes.Binary, new byte[] { 4 });
  345. attachmentDatas.Add(tmpBuff);
  346. attachmentDataSize += tmpBuff.Length;
  347. }
  348. // Resize our buffer once
  349. Array.Resize(ref buffer, buffer.Length + attachmentDataSize);
  350. // And copy all data into it
  351. for (int i = 0; i < AttachmentCount; ++i)
  352. {
  353. byte[] data = attachmentDatas[i];
  354. Array.Copy(data, 0, buffer, idx, data.Length);
  355. idx += data.Length;
  356. }
  357. }
  358. // Return the buffer
  359. return buffer;
  360. }
  361. /// <summary>
  362. /// Will add the byte[] that the server sent to the attachments list.
  363. /// </summary>
  364. internal void AddAttachmentFromServer(byte[] data, bool copyFull)
  365. {
  366. if (data == null || data.Length == 0)
  367. return;
  368. if (this.attachments == null)
  369. this.attachments = new List<byte[]>(this.AttachmentCount);
  370. if (copyFull)
  371. this.Attachments.Add(data);
  372. else
  373. {
  374. byte[] buff = new byte[data.Length - 1];
  375. Array.Copy(data, 1, buff, 0, data.Length - 1);
  376. this.Attachments.Add(buff);
  377. }
  378. }
  379. #endregion
  380. #region Private Helper Functions
  381. /// <summary>
  382. /// Encodes a byte array to a Socket.IO binary encoded message
  383. /// </summary>
  384. private byte[] EncodeData(byte[] data, PayloadTypes type, byte[] afterHeaderData)
  385. {
  386. // Packet binary encoding:
  387. // [ 0|1 ][ length of data ][ FF ][data]
  388. // <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number 255><data>
  389. // Get the length of the payload. Socket.IO uses a wasteful encoding to send the length of the data.
  390. // 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'.
  391. // Instead of just one byte: 0xF. If the payload is 123 bytes, we can't send as 0x7B...
  392. int afterHeaderLength = (afterHeaderData != null ? afterHeaderData.Length : 0);
  393. string lenStr = (data.Length + afterHeaderLength).ToString();
  394. byte[] len = new byte[lenStr.Length];
  395. for (int cv = 0; cv < lenStr.Length; ++cv)
  396. len[cv] = (byte)char.GetNumericValue(lenStr[cv]);
  397. // We need another buffer to store the final data
  398. byte[] buffer = new byte[data.Length + len.Length + 2 + afterHeaderLength];
  399. // The payload is textual -> 0
  400. buffer[0] = (byte)type;
  401. // Copy the length of the data
  402. for (int cv = 0; cv < len.Length; ++cv)
  403. buffer[1 + cv] = len[cv];
  404. int idx = 1 + len.Length;
  405. // End of the header data
  406. buffer[idx++] = 0xFF;
  407. if (afterHeaderData != null && afterHeaderData.Length > 0)
  408. {
  409. Array.Copy(afterHeaderData, 0, buffer, idx, afterHeaderData.Length);
  410. idx += afterHeaderData.Length;
  411. }
  412. // Copy our payload data to the buffer
  413. Array.Copy(data, 0, buffer, idx, data.Length);
  414. return buffer;
  415. }
  416. /// <summary>
  417. /// Searches for the "{'_placeholder':true,'num':X}" string, and will call the given action to modify the PayLoad
  418. /// </summary>
  419. private bool PlaceholderReplacer(Action<string, Dictionary<string, object>> onFound)
  420. {
  421. if (string.IsNullOrEmpty(this.Payload))
  422. return false;
  423. // Find the first index of the "_placeholder" str
  424. int placeholderIdx = this.Payload.IndexOf(Placeholder);
  425. while (placeholderIdx >= 0)
  426. {
  427. // Find the object-start token
  428. int startIdx = placeholderIdx;
  429. while (this.Payload[startIdx] != '{')
  430. startIdx--;
  431. // Find the object-end token
  432. int endIdx = placeholderIdx;
  433. while (this.Payload.Length > endIdx && this.Payload[endIdx] != '}')
  434. endIdx++;
  435. // We reached the end
  436. if (this.Payload.Length <= endIdx)
  437. return false;
  438. // Get the object, and decode it
  439. string placeholderJson = this.Payload.Substring(startIdx, endIdx - startIdx + 1);
  440. bool success = false;
  441. Dictionary<string, object> obj = Json.Decode(placeholderJson, ref success) as Dictionary<string, object>;
  442. if (!success)
  443. return false;
  444. // Check for presence and value of _placeholder
  445. object value;
  446. if (!obj.TryGetValue(Placeholder, out value) ||
  447. !(bool)value)
  448. return false;
  449. // Check for presence of num
  450. if (!obj.TryGetValue("num", out value))
  451. return false;
  452. // Let do, what we have to do
  453. onFound(placeholderJson, obj);
  454. // Find the next attachment if there is any
  455. placeholderIdx = this.Payload.IndexOf(Placeholder);
  456. }
  457. return true;
  458. }
  459. #endregion
  460. #region Overrides and Interface Implementations
  461. /// <summary>
  462. /// Returns with the Payload of this packet.
  463. /// </summary>
  464. public override string ToString()
  465. {
  466. return this.Payload;
  467. }
  468. /// <summary>
  469. /// Will clone this packet to an identical packet instance.
  470. /// </summary>
  471. internal Packet Clone()
  472. {
  473. Packet packet = new Packet(this.TransportEvent, this.SocketIOEvent, this.Namespace, this.Payload, 0, this.Id);
  474. packet.EventName = this.EventName;
  475. packet.AttachmentCount = this.AttachmentCount;
  476. packet.attachments = this.attachments;
  477. return packet;
  478. }
  479. #endregion
  480. }
  481. }
  482. #endif