PageRenderTime 52ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Microsoft.AspNet.SignalR.Client/Connection.cs

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