EventSource.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. #if !BESTHTTP_DISABLE_SERVERSENT_EVENTS
  2. using System;
  3. using System.Collections.Generic;
  4. using BestHTTP.Extensions;
  5. #if UNITY_WEBGL && !UNITY_EDITOR
  6. using System.Runtime.InteropServices;
  7. #endif
  8. namespace BestHTTP.ServerSentEvents
  9. {
  10. /// <summary>
  11. /// Possible states of an EventSource object.
  12. /// </summary>
  13. public enum States
  14. {
  15. Initial,
  16. Connecting,
  17. Open,
  18. Retrying,
  19. Closing,
  20. Closed
  21. }
  22. public delegate void OnGeneralEventDelegate(EventSource eventSource);
  23. public delegate void OnMessageDelegate(EventSource eventSource, BestHTTP.ServerSentEvents.Message message);
  24. public delegate void OnErrorDelegate(EventSource eventSource, string error);
  25. public delegate bool OnRetryDelegate(EventSource eventSource);
  26. public delegate void OnEventDelegate(EventSource eventSource, BestHTTP.ServerSentEvents.Message message);
  27. public delegate void OnStateChangedDelegate(EventSource eventSource, States oldState, States newState);
  28. #if UNITY_WEBGL && !UNITY_EDITOR
  29. delegate void OnWebGLEventSourceOpenDelegate(uint id);
  30. delegate void OnWebGLEventSourceMessageDelegate(uint id, string eventStr, string data, string eventId, int retry);
  31. delegate void OnWebGLEventSourceErrorDelegate(uint id, string reason);
  32. #endif
  33. /// <summary>
  34. /// http://www.w3.org/TR/eventsource/
  35. /// </summary>
  36. public class EventSource
  37. #if !UNITY_WEBGL || UNITY_EDITOR
  38. : IHeartbeat
  39. #endif
  40. {
  41. #region Public Properties
  42. /// <summary>
  43. /// Uri of the remote endpoint.
  44. /// </summary>
  45. public Uri Uri { get; private set; }
  46. /// <summary>
  47. /// Current state of the EventSource object.
  48. /// </summary>
  49. public States State
  50. {
  51. get
  52. {
  53. return _state;
  54. }
  55. private set
  56. {
  57. States oldState = _state;
  58. _state = value;
  59. if (OnStateChanged != null)
  60. {
  61. try
  62. {
  63. OnStateChanged(this, oldState, _state);
  64. }
  65. catch(Exception ex)
  66. {
  67. HTTPManager.Logger.Exception("EventSource", "OnStateChanged", ex);
  68. }
  69. }
  70. }
  71. }
  72. private States _state;
  73. /// <summary>
  74. /// Time to wait to do a reconnect attempt. Default to 2 sec. The server can overwrite this setting.
  75. /// </summary>
  76. public TimeSpan ReconnectionTime { get; set; }
  77. /// <summary>
  78. /// The last successfully received event's id.
  79. /// </summary>
  80. public string LastEventId { get; private set; }
  81. #if !UNITY_WEBGL || UNITY_EDITOR
  82. /// <summary>
  83. /// The internal request object of the EventSource.
  84. /// </summary>
  85. public HTTPRequest InternalRequest { get; private set; }
  86. #else
  87. public bool WithCredentials { get; set; }
  88. #endif
  89. #endregion
  90. #region Public Events
  91. /// <summary>
  92. /// Called when successfully connected to the server.
  93. /// </summary>
  94. public event OnGeneralEventDelegate OnOpen;
  95. /// <summary>
  96. /// Called on every message received from the server.
  97. /// </summary>
  98. public event OnMessageDelegate OnMessage;
  99. /// <summary>
  100. /// Called when an error occurs.
  101. /// </summary>
  102. public event OnErrorDelegate OnError;
  103. #if !UNITY_WEBGL || UNITY_EDITOR
  104. /// <summary>
  105. /// Called when the EventSource will try to do a retry attempt. If this function returns with false, it will cancel the attempt.
  106. /// </summary>
  107. public event OnRetryDelegate OnRetry;
  108. #endif
  109. /// <summary>
  110. /// Called when the EventSource object closed.
  111. /// </summary>
  112. public event OnGeneralEventDelegate OnClosed;
  113. /// <summary>
  114. /// Called every time when the State property changed.
  115. /// </summary>
  116. public event OnStateChangedDelegate OnStateChanged;
  117. #endregion
  118. #region Privates
  119. /// <summary>
  120. /// A dictionary to store eventName => delegate mapping.
  121. /// </summary>
  122. private Dictionary<string, OnEventDelegate> EventTable;
  123. #if !UNITY_WEBGL || UNITY_EDITOR
  124. /// <summary>
  125. /// Number of retry attempts made.
  126. /// </summary>
  127. private byte RetryCount;
  128. /// <summary>
  129. /// When we called the Retry function. We will delay the Open call from here.
  130. /// </summary>
  131. private DateTime RetryCalled;
  132. #else
  133. private static Dictionary<uint, EventSource> EventSources = new Dictionary<uint, EventSource>();
  134. private uint Id;
  135. #endif
  136. #endregion
  137. public EventSource(Uri uri)
  138. {
  139. this.Uri = uri;
  140. this.ReconnectionTime = TimeSpan.FromMilliseconds(2000);
  141. #if !UNITY_WEBGL || UNITY_EDITOR
  142. this.InternalRequest = new HTTPRequest(Uri, HTTPMethods.Get, true, true, OnRequestFinished);
  143. // SaveLocal headers
  144. this.InternalRequest.SetHeader("Accept", "text/event-stream");
  145. this.InternalRequest.SetHeader("Cache-Control", "no-cache");
  146. this.InternalRequest.SetHeader("Accept-Encoding", "identity");
  147. // SaveLocal protocol stuff
  148. this.InternalRequest.ProtocolHandler = SupportedProtocols.ServerSentEvents;
  149. this.InternalRequest.OnUpgraded = OnUpgraded;
  150. // Disable internal retry
  151. this.InternalRequest.DisableRetry = true;
  152. #else
  153. if (!ES_IsSupported())
  154. throw new NotSupportedException("This browser isn't support the EventSource protocol!");
  155. this.Id = ES_Create(this.Uri.ToString(), WithCredentials, OnOpenCallback, OnMessageCallback, OnErrorCallback);
  156. EventSources.Add(this.Id, this);
  157. #endif
  158. }
  159. #region Public Functions
  160. /// <summary>
  161. /// Start to connect to the remote server.
  162. /// </summary>
  163. public void Open()
  164. {
  165. if (this.State != States.Initial &&
  166. this.State != States.Retrying &&
  167. this.State != States.Closed)
  168. return;
  169. this.State = States.Connecting;
  170. #if !UNITY_WEBGL || UNITY_EDITOR
  171. if (!string.IsNullOrEmpty(this.LastEventId))
  172. this.InternalRequest.SetHeader("Last-Event-ID", this.LastEventId);
  173. this.InternalRequest.Send();
  174. #endif
  175. }
  176. /// <summary>
  177. /// Start to close the connection.
  178. /// </summary>
  179. public void Close()
  180. {
  181. if (this.State == States.Closing ||
  182. this.State == States.Closed)
  183. return;
  184. this.State = States.Closing;
  185. #if !UNITY_WEBGL || UNITY_EDITOR
  186. if (this.InternalRequest != null)
  187. this.InternalRequest.Abort();
  188. else
  189. this.State = States.Closed;
  190. #else
  191. ES_Close(this.Id);
  192. SetClosed("Close");
  193. EventSources.Remove(this.Id);
  194. ES_Release(this.Id);
  195. #endif
  196. }
  197. /// <summary>
  198. /// With this function an event handler can be subscribed for an event name.
  199. /// </summary>
  200. public void On(string eventName, OnEventDelegate action)
  201. {
  202. if (EventTable == null)
  203. EventTable = new Dictionary<string, OnEventDelegate>();
  204. EventTable[eventName] = action;
  205. #if UNITY_WEBGL && !UNITY_EDITOR
  206. ES_AddEventHandler(this.Id, eventName);
  207. #endif
  208. }
  209. /// <summary>
  210. /// With this function the event handler can be removed for the given event name.
  211. /// </summary>
  212. /// <param name="eventName"></param>
  213. public void Off(string eventName)
  214. {
  215. if (eventName == null || EventTable == null)
  216. return;
  217. EventTable.Remove(eventName);
  218. }
  219. #endregion
  220. #region Private Helper Functions
  221. private void CallOnError(string error, string msg)
  222. {
  223. if (OnError != null)
  224. {
  225. try
  226. {
  227. OnError(this, error);
  228. }
  229. catch (Exception ex)
  230. {
  231. HTTPManager.Logger.Exception("EventSource", msg + " - OnError", ex);
  232. }
  233. }
  234. }
  235. #if !UNITY_WEBGL || UNITY_EDITOR
  236. private bool CallOnRetry()
  237. {
  238. if (OnRetry != null)
  239. {
  240. try
  241. {
  242. return OnRetry(this);
  243. }
  244. catch(Exception ex)
  245. {
  246. HTTPManager.Logger.Exception("EventSource", "CallOnRetry", ex);
  247. }
  248. }
  249. return true;
  250. }
  251. #endif
  252. private void SetClosed(string msg)
  253. {
  254. this.State = States.Closed;
  255. if (OnClosed != null)
  256. {
  257. try
  258. {
  259. OnClosed(this);
  260. }
  261. catch (Exception ex)
  262. {
  263. HTTPManager.Logger.Exception("EventSource", msg + " - OnClosed", ex);
  264. }
  265. }
  266. }
  267. #if !UNITY_WEBGL || UNITY_EDITOR
  268. private void Retry()
  269. {
  270. if (RetryCount > 0 ||
  271. !CallOnRetry())
  272. {
  273. SetClosed("Retry");
  274. return;
  275. }
  276. RetryCount++;
  277. RetryCalled = DateTime.UtcNow;
  278. HTTPManager.Heartbeats.Subscribe(this);
  279. this.State = States.Retrying;
  280. }
  281. #endif
  282. #endregion
  283. #region HTTP Request Implementation
  284. #if !UNITY_WEBGL || UNITY_EDITOR
  285. /// <summary>
  286. /// We are successfully upgraded to the EventSource protocol, we can start to receive and parse the incoming data.
  287. /// </summary>
  288. private void OnUpgraded(HTTPRequest originalRequest, HTTPResponse response)
  289. {
  290. EventSourceResponse esResponse = response as EventSourceResponse;
  291. if (esResponse == null)
  292. {
  293. CallOnError("Not an EventSourceResponse!", "OnUpgraded");
  294. return;
  295. }
  296. if (OnOpen != null)
  297. {
  298. try
  299. {
  300. OnOpen(this);
  301. }
  302. catch (Exception ex)
  303. {
  304. HTTPManager.Logger.Exception("EventSource", "OnOpen", ex);
  305. }
  306. }
  307. esResponse.OnMessage += OnMessageReceived;
  308. esResponse.StartReceive();
  309. this.RetryCount = 0;
  310. this.State = States.Open;
  311. }
  312. private void OnRequestFinished(HTTPRequest req, HTTPResponse resp)
  313. {
  314. if (this.State == States.Closed)
  315. return;
  316. if (this.State == States.Closing ||
  317. req.State == HTTPRequestStates.Aborted)
  318. {
  319. SetClosed("OnRequestFinished");
  320. return;
  321. }
  322. string reason = string.Empty;
  323. // In some cases retry is prohibited
  324. bool canRetry = true;
  325. switch (req.State)
  326. {
  327. // The server sent all the data it's wanted.
  328. case HTTPRequestStates.Processing:
  329. canRetry = !resp.HasHeader("content-length");
  330. break;
  331. // The request finished without any problem.
  332. case HTTPRequestStates.Finished:
  333. // HTTP 200 OK responses that have a Content-Type specifying an unsupported type, or that have no Content-Type at all, must cause the user agent to fail the connection.
  334. if (resp.StatusCode == 200 && !resp.HasHeaderWithValue("content-type", "text/event-stream"))
  335. {
  336. reason = "No Content-Type header with value 'text/event-stream' present.";
  337. canRetry = false;
  338. }
  339. // HTTP 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, and 504 Gateway Timeout responses, and any network error that prevents the connection
  340. // from being established in the first place (e.g. DNS errors), must cause the user agent to asynchronously reestablish the connection.
  341. // Any other HTTP response code not listed here must cause the user agent to fail the connection.
  342. if (canRetry &&
  343. resp.StatusCode != 500 &&
  344. resp.StatusCode != 502 &&
  345. resp.StatusCode != 503 &&
  346. resp.StatusCode != 504)
  347. {
  348. canRetry = false;
  349. reason = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
  350. resp.StatusCode,
  351. resp.Message,
  352. resp.DataAsText);
  353. }
  354. break;
  355. // The request finished with an unexpected error. The request's Exception property may contain more info about the error.
  356. case HTTPRequestStates.Error:
  357. reason = "Request Finished with Error! " + (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception");
  358. break;
  359. // The request aborted, initiated by the user.
  360. case HTTPRequestStates.Aborted:
  361. // If the state is Closing, then it's a normal behaviour, and we close the EventSource
  362. reason = "OnRequestFinished - Aborted without request. EventSource's State: " + this.State;
  363. break;
  364. // Connecting to the server is timed out.
  365. case HTTPRequestStates.ConnectionTimedOut:
  366. reason = "Connection Timed Out!";
  367. break;
  368. // The request didn't finished in the given time.
  369. case HTTPRequestStates.TimedOut:
  370. reason = "Processing the request Timed Out!";
  371. break;
  372. }
  373. // If we are not closing the EventSource, then we will try to reconnect.
  374. if (this.State < States.Closing)
  375. {
  376. if (!string.IsNullOrEmpty(reason))
  377. CallOnError(reason, "OnRequestFinished");
  378. if (canRetry)
  379. Retry();
  380. else
  381. SetClosed("OnRequestFinished");
  382. }
  383. else
  384. SetClosed("OnRequestFinished");
  385. }
  386. #endif
  387. #endregion
  388. #region EventStreamResponse Event Handlers
  389. private void OnMessageReceived(
  390. #if !UNITY_WEBGL || UNITY_EDITOR
  391. EventSourceResponse resp,
  392. #endif
  393. BestHTTP.ServerSentEvents.Message message)
  394. {
  395. if (this.State >= States.Closing)
  396. return;
  397. // 1.) SaveLocal the last event ID string of the event source to value of the last event ID buffer.
  398. // The buffer does not get reset, so the last event ID string of the event source remains set to this value until the next time it is set by the server.
  399. // We check here only for null, because it can be a non-null but empty string.
  400. if (message.Id != null)
  401. this.LastEventId = message.Id;
  402. if (message.Retry.TotalMilliseconds > 0)
  403. this.ReconnectionTime = message.Retry;
  404. // 2.) If the data buffer is an empty string, set the data buffer and the event type buffer to the empty string and abort these steps.
  405. if (string.IsNullOrEmpty(message.Data))
  406. return;
  407. // 3.) If the data buffer's last character is a U+000A LINE FEED (LF) character, then remove the last character from the data buffer.
  408. // This step can be ignored. We constructed the string to be able to skip this step.
  409. if (OnMessage != null)
  410. {
  411. try
  412. {
  413. OnMessage(this, message);
  414. }
  415. catch (Exception ex)
  416. {
  417. HTTPManager.Logger.Exception("EventSource", "OnMessageReceived - OnMessage", ex);
  418. }
  419. }
  420. if (EventTable != null && !string.IsNullOrEmpty(message.Event))
  421. {
  422. OnEventDelegate action;
  423. if (EventTable.TryGetValue(message.Event, out action))
  424. {
  425. if (action != null)
  426. {
  427. try
  428. {
  429. action(this, message);
  430. }
  431. catch(Exception ex)
  432. {
  433. HTTPManager.Logger.Exception("EventSource", "OnMessageReceived - action", ex);
  434. }
  435. }
  436. }
  437. }
  438. }
  439. #endregion
  440. #region IHeartbeat Implementation
  441. #if !UNITY_WEBGL || UNITY_EDITOR
  442. void IHeartbeat.OnHeartbeatUpdate(TimeSpan dif)
  443. {
  444. if (this.State != States.Retrying)
  445. {
  446. HTTPManager.Heartbeats.Unsubscribe(this);
  447. return;
  448. }
  449. if (DateTime.UtcNow - RetryCalled >= ReconnectionTime)
  450. {
  451. Open();
  452. if (this.State != States.Connecting)
  453. SetClosed("OnHeartbeatUpdate");
  454. HTTPManager.Heartbeats.Unsubscribe(this);
  455. }
  456. }
  457. #endif
  458. #endregion
  459. #region WebGL Static Callbacks
  460. #if UNITY_WEBGL && !UNITY_EDITOR
  461. [AOT.MonoPInvokeCallback(typeof(OnWebGLEventSourceOpenDelegate))]
  462. static void OnOpenCallback(uint id)
  463. {
  464. EventSource es;
  465. if (EventSources.TryGetValue(id, out es))
  466. {
  467. if (es.OnOpen != null)
  468. {
  469. try
  470. {
  471. es.OnOpen(es);
  472. }
  473. catch(Exception ex)
  474. {
  475. HTTPManager.Logger.Exception("EventSource", "OnOpen", ex);
  476. }
  477. }
  478. es.State = States.Open;
  479. }
  480. else
  481. HTTPManager.Logger.Warning("EventSource", "OnOpenCallback - No EventSource found for id: " + id.ToString());
  482. }
  483. [AOT.MonoPInvokeCallback(typeof(OnWebGLEventSourceMessageDelegate))]
  484. static void OnMessageCallback(uint id, string eventStr, string data, string eventId, int retry)
  485. {
  486. EventSource es;
  487. if (EventSources.TryGetValue(id, out es))
  488. {
  489. var msg = new BestHTTP.ServerSentEvents.Message();
  490. msg.Id = eventId;
  491. msg.Data = data;
  492. msg.Event = eventStr;
  493. msg.Retry = TimeSpan.FromSeconds(retry);
  494. es.OnMessageReceived(msg);
  495. }
  496. }
  497. [AOT.MonoPInvokeCallback(typeof(OnWebGLEventSourceErrorDelegate))]
  498. static void OnErrorCallback(uint id, string reason)
  499. {
  500. EventSource es;
  501. if (EventSources.TryGetValue(id, out es))
  502. {
  503. es.CallOnError(reason, "OnErrorCallback");
  504. es.SetClosed("OnError");
  505. EventSources.Remove(id);
  506. }
  507. try
  508. {
  509. ES_Release(id);
  510. }
  511. catch (Exception ex)
  512. {
  513. HTTPManager.Logger.Exception("EventSource", "ES_Release", ex);
  514. }
  515. }
  516. #endif
  517. #endregion
  518. #region WebGL Interface
  519. #if UNITY_WEBGL && !UNITY_EDITOR
  520. [DllImport("__Internal")]
  521. static extern bool ES_IsSupported();
  522. [DllImport("__Internal")]
  523. static extern uint ES_Create(string url, bool withCred, OnWebGLEventSourceOpenDelegate onOpen, OnWebGLEventSourceMessageDelegate onMessage, OnWebGLEventSourceErrorDelegate onError);
  524. [DllImport("__Internal")]
  525. static extern void ES_AddEventHandler(uint id, string eventName);
  526. [DllImport("__Internal")]
  527. static extern void ES_Close(uint id);
  528. [DllImport("__Internal")]
  529. static extern void ES_Release(uint id);
  530. #endif
  531. #endregion
  532. }
  533. }
  534. #endif