PollingTransport.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. #if !BESTHTTP_DISABLE_SOCKETIO
  2. using System;
  3. using System.Linq;
  4. using System.Text;
  5. using BestHTTP.Extensions;
  6. namespace BestHTTP.SocketIO.Transports
  7. {
  8. public sealed class PollingTransport : ITransport
  9. {
  10. #region Public (ITransport) Properties
  11. public TransportTypes Type { get { return TransportTypes.Polling; } }
  12. public TransportStates State { get; private set; }
  13. public SocketManager Manager { get; private set; }
  14. public bool IsRequestInProgress { get { return LastRequest != null; } }
  15. public bool IsPollingInProgress { get { return PollRequest != null; } }
  16. #endregion
  17. #region Private Fields
  18. /// <summary>
  19. /// The last POST request we sent to the server.
  20. /// </summary>
  21. private HTTPRequest LastRequest;
  22. /// <summary>
  23. /// Last GET request we sent to the server.
  24. /// </summary>
  25. private HTTPRequest PollRequest;
  26. /// <summary>
  27. /// The last packet with expected binary attachments
  28. /// </summary>
  29. private Packet PacketWithAttachment;
  30. #endregion
  31. public enum PayloadTypes : byte
  32. {
  33. Text,
  34. Binary
  35. }
  36. public PollingTransport(SocketManager manager)
  37. {
  38. Manager = manager;
  39. }
  40. public void Open()
  41. {
  42. string format = "{0}?EIO={1}&transport=polling&t={2}-{3}{5}";
  43. if (Manager.Handshake != null)
  44. format += "&sid={4}";
  45. bool sendAdditionalQueryParams = !Manager.Options.QueryParamsOnlyForHandshake || (Manager.Options.QueryParamsOnlyForHandshake && Manager.Handshake == null);
  46. HTTPRequest request = new HTTPRequest(new Uri(string.Format(format,
  47. Manager.Uri.ToString(),
  48. Manager.ProtocolVersion,
  49. Manager.Timestamp.ToString(),
  50. Manager.RequestCounter++.ToString(),
  51. Manager.Handshake != null ? Manager.Handshake.Sid : string.Empty,
  52. sendAdditionalQueryParams ? Manager.Options.BuildQueryParams() : string.Empty)),
  53. OnRequestFinished);
  54. #if !BESTHTTP_DISABLE_CACHING
  55. // Don't even try to cache it
  56. request.DisableCache = true;
  57. #endif
  58. request.MaxRetries = 0;
  59. if (this.Manager.Options.HTTPRequestCustomizationCallback != null)
  60. this.Manager.Options.HTTPRequestCustomizationCallback(this.Manager, request);
  61. request.Send();
  62. State = TransportStates.Opening;
  63. }
  64. /// <summary>
  65. /// Closes the transport and cleans up resources.
  66. /// </summary>
  67. public void Close()
  68. {
  69. if (State == TransportStates.Closed)
  70. return;
  71. State = TransportStates.Closed;
  72. /*
  73. if (LastRequest != null)
  74. LastRequest.Abort();
  75. if (PollRequest != null)
  76. PollRequest.Abort();*/
  77. }
  78. #region Packet Sending Implementation
  79. private System.Collections.Generic.List<Packet> lonelyPacketList = new System.Collections.Generic.List<Packet>(1);
  80. public void Send(Packet packet)
  81. {
  82. try
  83. {
  84. lonelyPacketList.Add(packet);
  85. Send(lonelyPacketList);
  86. }
  87. finally
  88. {
  89. lonelyPacketList.Clear();
  90. }
  91. }
  92. public void Send(System.Collections.Generic.List<Packet> packets)
  93. {
  94. if (State != TransportStates.Opening && State != TransportStates.Open)
  95. return;
  96. if (IsRequestInProgress)
  97. throw new Exception("Sending packets are still in progress!");
  98. LastRequest = new HTTPRequest(new Uri(string.Format("{0}?EIO={1}&transport=polling&t={2}-{3}&sid={4}{5}",
  99. Manager.Uri.ToString(),
  100. Manager.ProtocolVersion,
  101. Manager.Timestamp.ToString(),
  102. Manager.RequestCounter++.ToString(),
  103. Manager.Handshake.Sid,
  104. !Manager.Options.QueryParamsOnlyForHandshake ? Manager.Options.BuildQueryParams() : string.Empty)),
  105. HTTPMethods.Post,
  106. OnRequestFinished);
  107. #if !BESTHTTP_DISABLE_CACHING
  108. // Don't even try to cache it
  109. LastRequest.DisableCache = true;
  110. #endif
  111. if (this.Manager.Options.ServerVersion == SupportedSocketIOVersions.v2)
  112. SendV2(packets, LastRequest);
  113. else
  114. SendV3(packets, LastRequest);
  115. if (this.Manager.Options.HTTPRequestCustomizationCallback != null)
  116. this.Manager.Options.HTTPRequestCustomizationCallback(this.Manager, LastRequest);
  117. LastRequest.Send();
  118. }
  119. StringBuilder sendBuilder = new StringBuilder();
  120. private void SendV3(System.Collections.Generic.List<Packet> packets, HTTPRequest request)
  121. {
  122. sendBuilder.Length = 0;
  123. try
  124. {
  125. for (int i = 0; i < packets.Count; ++i)
  126. {
  127. var packet = packets[i];
  128. if (i > 0)
  129. sendBuilder.Append((char)0x1E);
  130. sendBuilder.Append(packet.Encode());
  131. if (packet.Attachments != null && packet.Attachments.Count > 0)
  132. for(int cv = 0; cv < packet.Attachments.Count; ++cv)
  133. {
  134. sendBuilder.Append((char)0x1E);
  135. sendBuilder.Append('b');
  136. sendBuilder.Append(Convert.ToBase64String(packet.Attachments[i]));
  137. }
  138. }
  139. packets.Clear();
  140. }
  141. catch (Exception ex)
  142. {
  143. (Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
  144. return;
  145. }
  146. var str = sendBuilder.ToString();
  147. request.RawData = System.Text.Encoding.UTF8.GetBytes(str);
  148. request.SetHeader("Content-Type", "text/plain; charset=UTF-8");
  149. }
  150. private void SendV2(System.Collections.Generic.List<Packet> packets, HTTPRequest request)
  151. {
  152. byte[] buffer = null;
  153. try
  154. {
  155. buffer = packets[0].EncodeBinary();
  156. for (int i = 1; i < packets.Count; ++i)
  157. {
  158. byte[] tmpBuffer = packets[i].EncodeBinary();
  159. Array.Resize(ref buffer, buffer.Length + tmpBuffer.Length);
  160. Array.Copy(tmpBuffer, 0, buffer, buffer.Length - tmpBuffer.Length, tmpBuffer.Length);
  161. }
  162. packets.Clear();
  163. }
  164. catch (Exception ex)
  165. {
  166. (Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
  167. return;
  168. }
  169. request.SetHeader("Content-Type", "application/octet-stream");
  170. request.RawData = buffer;
  171. }
  172. private void OnRequestFinished(HTTPRequest req, HTTPResponse resp)
  173. {
  174. // Clear out the LastRequest variable, so we can start sending out new packets
  175. LastRequest = null;
  176. if (State == TransportStates.Closed)
  177. return;
  178. string errorString = null;
  179. switch (req.State)
  180. {
  181. // The request finished without any problem.
  182. case HTTPRequestStates.Finished:
  183. if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
  184. HTTPManager.Logger.Verbose("PollingTransport", "OnRequestFinished: " + resp.DataAsText);
  185. if (resp.IsSuccess)
  186. {
  187. // When we are sending data, the response is an 'ok' string
  188. if (req.MethodType != HTTPMethods.Post)
  189. ParseResponse(resp);
  190. }
  191. else
  192. errorString = string.Format("Polling - Request finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2} Uri: {3}",
  193. resp.StatusCode,
  194. resp.Message,
  195. resp.DataAsText,
  196. req.CurrentUri);
  197. break;
  198. // The request finished with an unexpected error. The request's Exception property may contain more info about the error.
  199. case HTTPRequestStates.Error:
  200. errorString = (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception");
  201. break;
  202. // The request aborted, initiated by the user.
  203. case HTTPRequestStates.Aborted:
  204. errorString = string.Format("Polling - Request({0}) Aborted!", req.CurrentUri);
  205. break;
  206. // Connecting to the server is timed out.
  207. case HTTPRequestStates.ConnectionTimedOut:
  208. errorString = string.Format("Polling - Connection Timed Out! Uri: {0}", req.CurrentUri);
  209. break;
  210. // The request didn't finished in the given time.
  211. case HTTPRequestStates.TimedOut:
  212. errorString = string.Format("Polling - Processing the request({0}) Timed Out!", req.CurrentUri);
  213. break;
  214. }
  215. if (!string.IsNullOrEmpty(errorString))
  216. (Manager as IManager).OnTransportError(this, errorString);
  217. }
  218. #endregion
  219. #region Polling Implementation
  220. public void Poll()
  221. {
  222. if (PollRequest != null || State == TransportStates.Paused)
  223. return;
  224. PollRequest = new HTTPRequest(new Uri(string.Format("{0}?EIO={1}&transport=polling&t={2}-{3}&sid={4}{5}",
  225. Manager.Uri.ToString(),
  226. Manager.ProtocolVersion,
  227. Manager.Timestamp.ToString(),
  228. Manager.RequestCounter++.ToString(),
  229. Manager.Handshake.Sid,
  230. !Manager.Options.QueryParamsOnlyForHandshake ? Manager.Options.BuildQueryParams() : string.Empty)),
  231. HTTPMethods.Get,
  232. OnPollRequestFinished);
  233. #if !BESTHTTP_DISABLE_CACHING
  234. // Don't even try to cache it
  235. PollRequest.DisableCache = true;
  236. #endif
  237. PollRequest.MaxRetries = 0;
  238. if (this.Manager.Options.HTTPRequestCustomizationCallback != null)
  239. this.Manager.Options.HTTPRequestCustomizationCallback(this.Manager, PollRequest);
  240. PollRequest.Send();
  241. }
  242. private void OnPollRequestFinished(HTTPRequest req, HTTPResponse resp)
  243. {
  244. // Clear the PollRequest variable, so we can start a new poll.
  245. PollRequest = null;
  246. if (State == TransportStates.Closed)
  247. return;
  248. string errorString = null;
  249. switch (req.State)
  250. {
  251. // The request finished without any problem.
  252. case HTTPRequestStates.Finished:
  253. if (HTTPManager.Logger.Level <= BestHTTP.Logger.Loglevels.All)
  254. HTTPManager.Logger.Verbose("PollingTransport", "OnPollRequestFinished: " + resp.DataAsText);
  255. if (resp.IsSuccess)
  256. ParseResponse(resp);
  257. else
  258. errorString = string.Format("Polling - Request finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2} Uri: {3}",
  259. resp.StatusCode,
  260. resp.Message,
  261. resp.DataAsText,
  262. req.CurrentUri);
  263. break;
  264. // The request finished with an unexpected error. The request's Exception property may contain more info about the error.
  265. case HTTPRequestStates.Error:
  266. errorString = req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception";
  267. break;
  268. // The request aborted, initiated by the user.
  269. case HTTPRequestStates.Aborted:
  270. errorString = string.Format("Polling - Request({0}) Aborted!", req.CurrentUri);
  271. break;
  272. // Connecting to the server is timed out.
  273. case HTTPRequestStates.ConnectionTimedOut:
  274. errorString = string.Format("Polling - Connection Timed Out! Uri: {0}", req.CurrentUri);
  275. break;
  276. // The request didn't finished in the given time.
  277. case HTTPRequestStates.TimedOut:
  278. errorString = string.Format("Polling - Processing the request({0}) Timed Out!", req.CurrentUri);
  279. break;
  280. }
  281. if (!string.IsNullOrEmpty(errorString))
  282. (Manager as IManager).OnTransportError(this, errorString);
  283. }
  284. #endregion
  285. #region Packet Parsing and Handling
  286. /// <summary>
  287. /// Preprocessing and sending out packets to the manager.
  288. /// </summary>
  289. private void OnPacket(Packet packet)
  290. {
  291. if (packet.AttachmentCount != 0 && !packet.HasAllAttachment)
  292. {
  293. PacketWithAttachment = packet;
  294. return;
  295. }
  296. switch (packet.TransportEvent)
  297. {
  298. case TransportEventTypes.Open:
  299. if (this.State != TransportStates.Opening)
  300. HTTPManager.Logger.Warning("PollingTransport", "Received 'Open' packet while state is '" + State.ToString() + "'");
  301. else
  302. State = TransportStates.Open;
  303. goto default;
  304. case TransportEventTypes.Message:
  305. if (packet.SocketIOEvent == SocketIOEventTypes.Connect) //2:40
  306. this.State = TransportStates.Open;
  307. goto default;
  308. default:
  309. (Manager as IManager).OnPacket(packet);
  310. break;
  311. }
  312. }
  313. private SupportedSocketIOVersions GetServerVersion(HTTPResponse resp)
  314. {
  315. string contentTypeValue = resp.GetFirstHeaderValue("content-type");
  316. if (string.IsNullOrEmpty(contentTypeValue))
  317. return SupportedSocketIOVersions.v2;
  318. HeaderParser contentType = new HeaderParser(contentTypeValue);
  319. PayloadTypes type = contentType.Values.FirstOrDefault().Key == "text/plain" ? PayloadTypes.Text : PayloadTypes.Binary;
  320. if (type != PayloadTypes.Text)
  321. return SupportedSocketIOVersions.v2;
  322. // https://github.com/socketio/engine.io-protocol/issues/35
  323. // v3: 96:0{ "sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}
  324. // v4: 0{ "sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}
  325. for (int i = 0; i< resp.Data.Length; ++i)
  326. {
  327. if (resp.Data[i] == ':')
  328. return SupportedSocketIOVersions.v2;
  329. if (resp.Data[i] == '{')
  330. return SupportedSocketIOVersions.v3;
  331. }
  332. return SupportedSocketIOVersions.Unknown;
  333. }
  334. private void ParseResponse(HTTPResponse resp)
  335. {
  336. if (this.Manager.Options.ServerVersion == SupportedSocketIOVersions.Unknown)
  337. this.Manager.Options.ServerVersion = GetServerVersion(resp);
  338. if (this.Manager.Options.ServerVersion == SupportedSocketIOVersions.v2)
  339. this.ParseResponseV2(resp);
  340. else
  341. this.ParseResponseV3(resp);
  342. }
  343. private void ParseResponseV3(HTTPResponse resp)
  344. {
  345. try
  346. {
  347. if (resp == null || resp.Data == null || resp.Data.Length < 1)
  348. return;
  349. //HeaderParser contentType = new HeaderParser(resp.GetFirstHeaderValue("content-type"));
  350. //PayloadTypes type = contentType.Values.FirstOrDefault().Key == "text/plain" ? PayloadTypes.Text : PayloadTypes.Binary;
  351. int idx = 0;
  352. while (idx < resp.Data.Length)
  353. {
  354. int endIdx = FindNextRecordSeparator(resp.Data, idx);
  355. int length = endIdx - idx;
  356. if (length <= 0)
  357. break;
  358. Packet packet = null;
  359. if (resp.Data[idx] == 'b')
  360. {
  361. if (PacketWithAttachment != null)
  362. {
  363. // First byte is the binary indicator('b'). We must skip it, so we advance our idx and also have to decrease length
  364. idx++;
  365. length--;
  366. var base64Encoded = System.Text.Encoding.UTF8.GetString(resp.Data, idx, length);
  367. PacketWithAttachment.AddAttachmentFromServer(Convert.FromBase64String(base64Encoded), true);
  368. if (PacketWithAttachment.HasAllAttachment)
  369. {
  370. packet = PacketWithAttachment;
  371. PacketWithAttachment = null;
  372. }
  373. }
  374. else
  375. HTTPManager.Logger.Warning("PollingTransport", "Received binary but no packet to attach to!");
  376. }
  377. else
  378. {
  379. packet = new Packet(Encoding.UTF8.GetString(resp.Data, idx, length));
  380. }
  381. if (packet != null)
  382. {
  383. try
  384. {
  385. OnPacket(packet);
  386. }
  387. catch (Exception ex)
  388. {
  389. HTTPManager.Logger.Exception("PollingTransport", "ParseResponseV3 - OnPacket", ex);
  390. (Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
  391. }
  392. }
  393. idx = endIdx + 1;
  394. }
  395. }
  396. catch (Exception ex)
  397. {
  398. (Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
  399. HTTPManager.Logger.Exception("PollingTransport", "ParseResponseV3", ex);
  400. }
  401. }
  402. private int FindNextRecordSeparator(byte[] data, int startIdx)
  403. {
  404. for (int i = startIdx; i < data.Length; ++i)
  405. {
  406. if (data[i] == 0x1E)
  407. return i;
  408. }
  409. return data.Length;
  410. }
  411. /// <summary>
  412. /// Will parse the response, and send out the parsed packets.
  413. /// </summary>
  414. private void ParseResponseV2(HTTPResponse resp)
  415. {
  416. try
  417. {
  418. if (resp != null && resp.Data != null && resp.Data.Length >= 1)
  419. {
  420. int idx = 0;
  421. while (idx < resp.Data.Length)
  422. {
  423. PayloadTypes type = PayloadTypes.Text;
  424. int length = 0;
  425. if (resp.Data[idx] < '0')
  426. {
  427. type = (PayloadTypes)resp.Data[idx++];
  428. byte num = resp.Data[idx++];
  429. while (num != 0xFF)
  430. {
  431. length = (length * 10) + num;
  432. num = resp.Data[idx++];
  433. }
  434. }
  435. else
  436. {
  437. byte next = resp.Data[idx++];
  438. while (next != ':')
  439. {
  440. length = (length * 10) + (next - '0');
  441. next = resp.Data[idx++];
  442. }
  443. // Because length can be different from the byte length, we have to do a little post-processing to support unicode characters.
  444. int brackets = 0;
  445. int tmpIdx = idx;
  446. while (tmpIdx < idx + length)
  447. {
  448. if (resp.Data[tmpIdx] == '[')
  449. brackets++;
  450. else if (resp.Data[tmpIdx] == ']')
  451. brackets--;
  452. tmpIdx++;
  453. }
  454. if (brackets > 0)
  455. {
  456. while (brackets > 0)
  457. {
  458. if (resp.Data[tmpIdx] == '[')
  459. brackets++;
  460. else if (resp.Data[tmpIdx] == ']')
  461. brackets--;
  462. tmpIdx++;
  463. }
  464. length = tmpIdx - idx;
  465. }
  466. }
  467. Packet packet = null;
  468. switch (type)
  469. {
  470. case PayloadTypes.Text:
  471. packet = new Packet(Encoding.UTF8.GetString(resp.Data, idx, length));
  472. break;
  473. case PayloadTypes.Binary:
  474. if (PacketWithAttachment != null)
  475. {
  476. // First byte is the packet type. We can skip it, so we advance our idx and we also have
  477. // to decrease length
  478. idx++;
  479. length--;
  480. byte[] buffer = new byte[length];
  481. Array.Copy(resp.Data, idx, buffer, 0, length);
  482. PacketWithAttachment.AddAttachmentFromServer(buffer, true);
  483. if (PacketWithAttachment.HasAllAttachment)
  484. {
  485. packet = PacketWithAttachment;
  486. PacketWithAttachment = null;
  487. }
  488. }
  489. break;
  490. } // switch
  491. if (packet != null)
  492. {
  493. try
  494. {
  495. OnPacket(packet);
  496. }
  497. catch (Exception ex)
  498. {
  499. HTTPManager.Logger.Exception("PollingTransport", "ParseResponseV2 - OnPacket", ex);
  500. (Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
  501. }
  502. }
  503. idx += length;
  504. }// while
  505. }
  506. }
  507. catch (Exception ex)
  508. {
  509. (Manager as IManager).EmitError(SocketIOErrors.Internal, ex.Message + " " + ex.StackTrace);
  510. HTTPManager.Logger.Exception("PollingTransport", "ParseResponseV2", ex);
  511. }
  512. }
  513. #endregion
  514. }
  515. }
  516. #endif