PageRenderTime 43ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/SignalR.Client/Connection.cs

https://github.com/kpmrafeeq/SignalR
C# | 449 lines | 294 code | 60 blank | 95 comment | 37 complexity | 6eb341114c4eff0edfe72c5b05b5224a MD5 | raw file
Possible License(s): MIT
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Reflection;
  8. using System.Threading.Tasks;
  9. using Newtonsoft.Json;
  10. using Newtonsoft.Json.Linq;
  11. using SignalR.Client.Http;
  12. using SignalR.Client.Transports;
  13. namespace SignalR.Client
  14. {
  15. /// <summary>
  16. /// Provides client connections for SignalR services.
  17. /// </summary>
  18. public class Connection : IConnection
  19. {
  20. private static Version _assemblyVersion;
  21. private IClientTransport _transport;
  22. // The default connection state is disconnected
  23. private ConnectionState _state = ConnectionState.Disconnected;
  24. // Used to synchornize state changes
  25. private readonly object _stateLock = new object();
  26. /// <summary>
  27. /// Occurs when the <see cref="Connection"/> has received data from the server.
  28. /// </summary>
  29. public event Action<string> Received;
  30. /// <summary>
  31. /// Occurs when the <see cref="Connection"/> has encountered an error.
  32. /// </summary>
  33. public event Action<Exception> Error;
  34. /// <summary>
  35. /// Occurs when the <see cref="Connection"/> is stopped.
  36. /// </summary>
  37. public event Action Closed;
  38. /// <summary>
  39. /// Occurs when the <see cref="Connection"/> successfully reconnects after a timeout.
  40. /// </summary>
  41. public event Action Reconnected;
  42. /// <summary>
  43. /// Occurs when the <see cref="Connection"/> state changes.
  44. /// </summary>
  45. public event Action<StateChange> StateChanged;
  46. /// <summary>
  47. /// Initializes a new instance of the <see cref="Connection"/> class.
  48. /// </summary>
  49. /// <param name="url">The url to connect to.</param>
  50. public Connection(string url)
  51. : this(url, (string)null)
  52. {
  53. }
  54. /// <summary>
  55. /// Initializes a new instance of the <see cref="Connection"/> class.
  56. /// </summary>
  57. /// <param name="url">The url to connect to.</param>
  58. /// <param name="queryString">The query string data to pass to the server.</param>
  59. public Connection(string url, IDictionary<string, string> queryString)
  60. : this(url, CreateQueryString(queryString))
  61. {
  62. }
  63. /// <summary>
  64. /// Initializes a new instance of the <see cref="Connection"/> class.
  65. /// </summary>
  66. /// <param name="url">The url to connect to.</param>
  67. /// <param name="queryString">The query string data to pass to the server.</param>
  68. public Connection(string url, string queryString)
  69. {
  70. if (url.Contains("?"))
  71. {
  72. throw new ArgumentException("Url cannot contain QueryString directly. Pass QueryString values in using available overload.", "url");
  73. }
  74. if (!url.EndsWith("/"))
  75. {
  76. url += "/";
  77. }
  78. Url = url;
  79. QueryString = queryString;
  80. Groups = Enumerable.Empty<string>();
  81. Items = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
  82. State = ConnectionState.Disconnected;
  83. }
  84. /// <summary>
  85. /// Gets or sets the cookies associated with the connection.
  86. /// </summary>
  87. public CookieContainer CookieContainer { get; set; }
  88. /// <summary>
  89. /// Gets or sets authentication information for the connection.
  90. /// </summary>
  91. public ICredentials Credentials { get; set; }
  92. /// <summary>
  93. /// Gets or sets the groups for the connection.
  94. /// </summary>
  95. public IEnumerable<string> Groups { get; set; }
  96. /// <summary>
  97. /// Gets the url for the connection.
  98. /// </summary>
  99. public string Url { get; private set; }
  100. /// <summary>
  101. /// Gets or sets the last message id for the connection.
  102. /// </summary>
  103. public string MessageId { get; set; }
  104. /// <summary>
  105. /// Gets or sets the connection id for the connection.
  106. /// </summary>
  107. public string ConnectionId { get; set; }
  108. /// <summary>
  109. /// Gets a dictionary for storing state for a the connection.
  110. /// </summary>
  111. public IDictionary<string, object> Items { get; private set; }
  112. /// <summary>
  113. /// Gets the querystring specified in the ctor.
  114. /// </summary>
  115. public string QueryString { get; private set; }
  116. /// <summary>
  117. /// Gets the current <see cref="ConnectionState"/> of the connection.
  118. /// </summary>
  119. public ConnectionState State
  120. {
  121. get
  122. {
  123. lock (_stateLock)
  124. {
  125. return _state;
  126. }
  127. }
  128. private set
  129. {
  130. if (_state != value)
  131. {
  132. if (StateChanged != null)
  133. {
  134. StateChanged(new StateChange(_state, value));
  135. }
  136. _state = value;
  137. }
  138. }
  139. }
  140. /// <summary>
  141. /// Starts the <see cref="Connection"/>.
  142. /// </summary>
  143. /// <returns>A task that represents when the connection has started.</returns>
  144. public Task Start()
  145. {
  146. return Start(new DefaultHttpClient());
  147. }
  148. /// <summary>
  149. /// Starts the <see cref="Connection"/>.
  150. /// </summary>
  151. /// <param name="httpClient">The http client</param>
  152. /// <returns>A task that represents when the connection has started.</returns>
  153. public Task Start(IHttpClient httpClient)
  154. {
  155. // Pick the best transport supported by the client
  156. return Start(new AutoTransport(httpClient));
  157. }
  158. /// <summary>
  159. /// Starts the <see cref="Connection"/>.
  160. /// </summary>
  161. /// <param name="transport">The transport to use.</param>
  162. /// <returns>A task that represents when the connection has started.</returns>
  163. public virtual Task Start(IClientTransport transport)
  164. {
  165. if (!ChangeState(ConnectionState.Disconnected, ConnectionState.Connecting))
  166. {
  167. return TaskAsyncHelper.Empty;
  168. }
  169. _transport = transport;
  170. return Negotiate(transport);
  171. }
  172. protected virtual string OnSending()
  173. {
  174. return null;
  175. }
  176. private Task Negotiate(IClientTransport transport)
  177. {
  178. var negotiateTcs = new TaskCompletionSource<object>();
  179. transport.Negotiate(this).Then(negotiationResponse =>
  180. {
  181. VerifyProtocolVersion(negotiationResponse.ProtocolVersion);
  182. ConnectionId = negotiationResponse.ConnectionId;
  183. var data = OnSending();
  184. StartTransport(data).ContinueWith(negotiateTcs);
  185. })
  186. .ContinueWithNotComplete(negotiateTcs);
  187. var tcs = new TaskCompletionSource<object>();
  188. negotiateTcs.Task.ContinueWith(task =>
  189. {
  190. try
  191. {
  192. // If there's any errors starting then Stop the connection
  193. if (task.IsFaulted)
  194. {
  195. Stop();
  196. tcs.SetException(task.Exception.Unwrap());
  197. }
  198. else if (task.IsCanceled)
  199. {
  200. Stop();
  201. tcs.SetCanceled();
  202. }
  203. else
  204. {
  205. tcs.SetResult(null);
  206. }
  207. }
  208. catch (Exception ex)
  209. {
  210. tcs.SetException(ex);
  211. }
  212. },
  213. TaskContinuationOptions.ExecuteSynchronously);
  214. return tcs.Task;
  215. }
  216. private Task StartTransport(string data)
  217. {
  218. return _transport.Start(this, data)
  219. .Then(() =>
  220. {
  221. ChangeState(ConnectionState.Connecting, ConnectionState.Connected);
  222. });
  223. }
  224. private bool ChangeState(ConnectionState oldState, ConnectionState newState)
  225. {
  226. return ((IConnection)this).ChangeState(oldState, newState);
  227. }
  228. bool IConnection.ChangeState(ConnectionState oldState, ConnectionState newState)
  229. {
  230. lock (_stateLock)
  231. {
  232. // If we're in the expected old state then change state and return true
  233. if (_state == oldState)
  234. {
  235. State = newState;
  236. return true;
  237. }
  238. // Invalid transition
  239. return false;
  240. }
  241. }
  242. private static void VerifyProtocolVersion(string versionString)
  243. {
  244. Version version;
  245. if (String.IsNullOrEmpty(versionString) ||
  246. !TryParseVersion(versionString, out version) ||
  247. !(version.Major == 1 && version.Minor == 0))
  248. {
  249. throw new InvalidOperationException("Incompatible protocol version.");
  250. }
  251. }
  252. /// <summary>
  253. /// Stops the <see cref="Connection"/>.
  254. /// </summary>
  255. public virtual void Stop()
  256. {
  257. try
  258. {
  259. // Do nothing if the connection is offline
  260. if (State == ConnectionState.Disconnected)
  261. {
  262. return;
  263. }
  264. _transport.Stop(this);
  265. if (Closed != null)
  266. {
  267. Closed();
  268. }
  269. }
  270. finally
  271. {
  272. State = ConnectionState.Disconnected;
  273. }
  274. }
  275. /// <summary>
  276. /// Sends data asynchronously over the connection.
  277. /// </summary>
  278. /// <param name="data">The data to send.</param>
  279. /// <returns>A task that represents when the data has been sent.</returns>
  280. public Task Send(string data)
  281. {
  282. return ((IConnection)this).Send<object>(data);
  283. }
  284. /// <summary>
  285. /// Sends an object that will be JSON serialized asynchronously over the connection.
  286. /// </summary>
  287. /// <param name="value">The value to serialize.</param>
  288. /// <returns>A task that represents when the data has been sent.</returns>
  289. public Task Send(object value)
  290. {
  291. return Send(JsonConvert.SerializeObject(value));
  292. }
  293. Task<T> IConnection.Send<T>(string data)
  294. {
  295. if (State == ConnectionState.Disconnected)
  296. {
  297. throw new InvalidOperationException("Start must be called before data can be sent.");
  298. }
  299. if (State == ConnectionState.Connecting)
  300. {
  301. throw new InvalidOperationException("The connection has not been established.");
  302. }
  303. return _transport.Send<T>(this, data);
  304. }
  305. void IConnection.OnReceived(JToken message)
  306. {
  307. OnReceived(message);
  308. }
  309. protected virtual void OnReceived(JToken message)
  310. {
  311. if (Received != null)
  312. {
  313. Received(message.ToString());
  314. }
  315. }
  316. void IConnection.OnError(Exception error)
  317. {
  318. if (Error != null)
  319. {
  320. Error(error);
  321. }
  322. }
  323. void IConnection.OnReconnected()
  324. {
  325. if (Reconnected != null)
  326. {
  327. Reconnected();
  328. }
  329. }
  330. void IConnection.PrepareRequest(IRequest request)
  331. {
  332. #if WINDOWS_PHONE
  333. // http://msdn.microsoft.com/en-us/library/ff637320(VS.95).aspx
  334. request.UserAgent = CreateUserAgentString("SignalR.Client.WP7");
  335. #else
  336. #if SILVERLIGHT
  337. // Useragent is not possible to set with Silverlight, not on the UserAgent property of the request nor in the Headers key/value in the request
  338. #else
  339. request.UserAgent = CreateUserAgentString("SignalR.Client");
  340. #endif
  341. #endif
  342. if (Credentials != null)
  343. {
  344. request.Credentials = Credentials;
  345. }
  346. if (CookieContainer != null)
  347. {
  348. request.CookieContainer = CookieContainer;
  349. }
  350. }
  351. private static string CreateUserAgentString(string client)
  352. {
  353. if (_assemblyVersion == null)
  354. {
  355. #if NETFX_CORE
  356. _assemblyVersion = new Version("0.5.0.0");
  357. #else
  358. _assemblyVersion = new AssemblyName(typeof(Connection).Assembly.FullName).Version;
  359. #endif
  360. }
  361. #if NETFX_CORE
  362. return String.Format(CultureInfo.InvariantCulture, "{0}/{1} ({2})", client, _assemblyVersion, "Unknown OS");
  363. #else
  364. return String.Format(CultureInfo.InvariantCulture, "{0}/{1} ({2})", client, _assemblyVersion, Environment.OSVersion);
  365. #endif
  366. }
  367. private static bool TryParseVersion(string versionString, out Version version)
  368. {
  369. #if WINDOWS_PHONE || NET35
  370. try
  371. {
  372. version = new Version(versionString);
  373. return true;
  374. }
  375. catch
  376. {
  377. version = null;
  378. return false;
  379. }
  380. #else
  381. return Version.TryParse(versionString, out version);
  382. #endif
  383. }
  384. private static string CreateQueryString(IDictionary<string, string> queryString)
  385. {
  386. return String.Join("&", queryString.Select(kvp => kvp.Key + "=" + kvp.Value).ToArray());
  387. }
  388. }
  389. }