source: other-projects/playing-in-the-street/summer-2013/trunk/Playing-in-the-Street-WPF/Microsoft.Samples.Kinect.Webserver/WebSocketChannelBase.cs@ 28897

Last change on this file since 28897 was 28897, checked in by davidb, 10 years ago

GUI front-end to server base plus web page content

File size: 22.8 KB
Line 
1//------------------------------------------------------------------------------
2// <copyright file="WebSocketChannelBase.cs" company="Microsoft">
3// Copyright (c) Microsoft Corporation. All rights reserved.
4// </copyright>
5//------------------------------------------------------------------------------
6
7namespace Microsoft.Samples.Kinect.Webserver
8{
9 using System;
10 using System.Diagnostics;
11 using System.Net;
12 using System.Net.WebSockets;
13 using System.Threading;
14 using System.Threading.Tasks;
15 using System.Windows.Threading;
16
17 /// <summary>
18 /// Base class representing a web socket communication channel.
19 /// </summary>
20 /// <remarks>
21 /// This class has asynchronous functionality but it is NOT thread-safe, so it is expected
22 /// to be called by a single-threaded scheduler, e.g.: one running over a Dispatcher or
23 /// other SynchronizationContext implementation.
24 /// </remarks>
25 public class WebSocketChannelBase : IDisposable
26 {
27 /// <summary>
28 /// Maximum time allowed to pass between successive checks for web socket disconnection.
29 /// </summary>
30 private static readonly TimeSpan DisconnectionCheckTimeout = TimeSpan.FromSeconds(2.0);
31
32 /// <summary>
33 /// Timer used to ensure we periodically check for disconnection even if no messages are
34 /// being sent between server and client.
35 /// </summary>
36 /// <remarks>
37 /// The disconnection timer helps us notice that there is a web socket resource ready to
38 /// be disposed.
39 /// </remarks>
40 private readonly DispatcherTimer disconnectionCheckTimer = new DispatcherTimer();
41
42 /// <summary>
43 /// Action to perform when web socket becomes closed.
44 /// </summary>
45 private readonly Action<WebSocketChannelBase> closedAction;
46
47 /// <summary>
48 /// Non-null if someone has requested that this object be closed and disposed. Null otherwise.
49 /// </summary>
50 private Task disposingTask;
51
52 /// <summary>
53 /// Non-null if channel is currently sending a message. Null otherwise.
54 /// </summary>
55 private Task sendingTask;
56
57 /// <summary>
58 /// True if we're performing the protocol close handshake and notifying clients
59 /// that socket has been closed.
60 /// </summary>
61 private bool isClosing;
62
63 /// <summary>
64 /// True if this object has already been disposed. False otherwise.
65 /// </summary>
66 private bool isDisposed;
67
68 /// <summary>
69 /// True if Closed event has been sent already. False otherwise.
70 /// </summary>
71 private bool closedSent;
72
73 /// <summary>
74 /// True if disconnection monitoring task has been started, false otherwise.
75 /// </summary>
76 private bool isDisconnectionMonitorStarted;
77
78 /// <summary>
79 /// Initializes a new instance of the <see cref="WebSocketChannelBase"/> class.
80 /// </summary>
81 /// <param name="context">
82 /// Web socket context.
83 /// </param>
84 /// <param name="closedAction">
85 /// Action to perform when web socket becomes closed.
86 /// </param>
87 protected WebSocketChannelBase(WebSocketContext context, Action<WebSocketChannelBase> closedAction)
88 {
89 if ((context == null) || (context.WebSocket == null))
90 {
91 throw new ArgumentNullException("context", @"Context and associated web socket must not be null");
92 }
93
94 this.Socket = context.WebSocket;
95 this.CancellationTokenSource = new CancellationTokenSource();
96
97 this.disconnectionCheckTimer.Interval = DisconnectionCheckTimeout;
98 this.disconnectionCheckTimer.Tick += this.OnDisconnectionCheckTimerTick;
99 this.disconnectionCheckTimer.Start();
100
101 this.closedAction = closedAction;
102 }
103
104 /// <summary>
105 /// True if this web socket channel is open for sending/receiving messages.
106 /// </summary>
107 public bool IsOpen
108 {
109 get
110 {
111 return !this.isDisposed && ((this.Socket.State == WebSocketState.Open) || (this.Socket.State == WebSocketState.Connecting));
112 }
113 }
114
115 /// <summary>
116 /// Web socket used for communications.
117 /// </summary>
118 protected WebSocket Socket { get; private set; }
119
120 /// <summary>
121 /// Token source used to cancel pending socket operations.
122 /// </summary>
123 protected CancellationTokenSource CancellationTokenSource { get; private set; }
124
125 /// <summary>
126 /// Cancel all pending socket operations.
127 /// </summary>
128 public void Cancel()
129 {
130 this.CancellationTokenSource.Cancel();
131 }
132
133 /// <summary>
134 /// Implement IDisposable.
135 /// </summary>
136 /// <remarks>
137 /// Releases resources right away if underlying socket is already closed.
138 /// Otherwise asynchronously awaits to complete web socket close handshake
139 /// and then disposes resources.
140 /// Call <see cref="CloseAsync"/> instead to be able to await for socket to
141 /// finish disposing resources.
142 /// </remarks>
143 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Asynchronous disposal. Does call Dispose(true) and GC.SuppressFinalize.")]
144 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly", Justification = "Asynchronous disposal. Does call Dispose(true) and GC.SuppressFinalize.")]
145 public async void Dispose()
146 {
147 await this.CloseAsync();
148 }
149
150 /// <summary>
151 /// Cancel all pending operations, initiate web socket close handshake and send Closed
152 /// event to clients if necessary.
153 /// </summary>
154 /// <returns>
155 /// Await-able task.
156 /// </returns>
157 /// <remarks>
158 /// Disposes of socket resources. There is no need to call <see cref="Dispose"/> after
159 /// calling this method.
160 /// </remarks>
161 public async Task CloseAsync()
162 {
163 if (this.disposingTask != null)
164 {
165 await this.disposingTask;
166 return;
167 }
168
169 this.disposingTask = this.CloseAndDisposeAsync();
170 await this.disposingTask;
171 }
172
173 /// <summary>
174 /// Determine if this web socket channel is open for sending/receiving messages
175 /// or if it has been closed
176 /// </summary>
177 /// <returns>
178 /// True if this web socket channel is still open, false otherwise.
179 /// </returns>
180 /// <remarks>
181 /// This call is expected to perform more comprehensive connection state checks
182 /// than IsOpen property, which might include sending remote messages, if the
183 /// specific <see cref="WebSocketChannelBase"/> subclass warrants it, so callers
184 /// should be careful not to call this method too often.
185 /// </remarks>
186 public virtual bool CheckConnectionStatus()
187 {
188 return this.IsOpen;
189 }
190
191 /// <summary>
192 /// Try to establish a web socket context from the specified HTTP request context.
193 /// </summary>
194 /// <param name="listenerContext">
195 /// HTTP listener context.
196 /// </param>
197 /// <returns>
198 /// A web socket context if communications channel was successfully established.
199 /// Null if web socket channel could not be established.
200 /// </returns>
201 /// <remarks>
202 /// If <paramref name="listenerContext"/> does not represent a web socket request, or if
203 /// web socket channel could not be established, an appropriate status code will be
204 /// returned via <paramref name="listenerContext"/>'s Response property, and the return
205 /// value will be null.
206 /// </remarks>
207 protected static async Task<WebSocketContext> HandleWebSocketRequestAsync(HttpListenerContext listenerContext)
208 {
209 if (!listenerContext.Request.IsWebSocketRequest)
210 {
211 listenerContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
212 listenerContext.Response.Close();
213 return null;
214 }
215
216 try
217 {
218 return await listenerContext.AcceptWebSocketAsync(null);
219 }
220 catch (WebSocketException)
221 {
222 listenerContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
223 listenerContext.Response.Close();
224 return null;
225 }
226 }
227
228 /// <summary>
229 /// Sends data over the WebSocket connection asynchronously.
230 /// </summary>
231 /// <param name="buffer">
232 /// The buffer to be sent over the connection.
233 /// </param>
234 /// <param name="messageType">
235 /// Indicates whether the application is sending a binary or text message.
236 /// </param>
237 /// <returns>
238 /// true if the message was sent successfully. false otherwise.
239 /// </returns>
240 protected async Task<bool> SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType)
241 {
242 if (await this.CheckForDisconnectionAsync())
243 {
244 return false;
245 }
246
247 if (this.sendingTask != null)
248 {
249 Trace.TraceError("Channel is unable to start sending a new websocket message while it is already sending a previous message.");
250 return false;
251 }
252
253 bool result = true;
254
255 // We create a separate task from the one corresponding to the WebSocket.SendAsync
256 // method call, because we need to provide a guarantee to other parts of the code
257 // waiting on this task that SendAsync method call has returned by the time the
258 // sendingTask has completed.
259 // If we don't do this, it would be possible for other code awaiting on task to
260 // start executing before SendAsync stack frame gets a chance to clear the "sending"
261 // state to get ready to receive other calls that initiate data sending.
262 this.sendingTask = SharedConstants.CreateNonstartedTask();
263
264 try
265 {
266 await this.Socket.SendAsync(buffer, messageType, true, this.CancellationTokenSource.Token);
267 }
268 catch (Exception e)
269 {
270 if (!IsSendReceiveException(e))
271 {
272 throw;
273 }
274
275 result = false;
276 }
277 finally
278 {
279 this.sendingTask.Start();
280 this.sendingTask = null;
281 }
282
283 if (!result)
284 {
285 // Client might have disconnected
286 await this.CheckForDisconnectionAsync();
287 }
288
289 return result;
290 }
291
292 /// <summary>
293 /// Receives data from the WebSocket connection asynchronously.
294 /// </summary>
295 /// <param name="buffer">
296 /// References the application buffer that is the storage location for the received data.
297 /// </param>
298 /// <returns>
299 /// A receive result representing a full message if we could receive one successfully
300 /// within the specified buffer. null otherwise.
301 /// </returns>
302 /// <remarks>
303 /// This method receives data from socket until either we get to the end of message sent
304 /// by socket client or we fill the specified buffer. If we don't get to end of message
305 /// within the allocated buffer space, we return a result value indicating that message
306 /// is still incomplete.
307 /// </remarks>
308 protected async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer)
309 {
310 if (await this.CheckForDisconnectionAsync())
311 {
312 return null;
313 }
314
315 int receiveOffset = 0;
316 int receiveCount = 0;
317 bool isResponseComplete = false;
318 WebSocketReceiveResult receiveResult = null;
319
320 try
321 {
322 while (!isResponseComplete)
323 {
324 if (receiveCount >= buffer.Count)
325 {
326 // If we've filled up the buffer and response message is still not
327 // complete, we won't have space for response at all, so just return
328 // incomplete message.
329 return new WebSocketReceiveResult(
330 receiveCount, receiveResult != null ? receiveResult.MessageType : WebSocketMessageType.Text, false);
331 }
332
333 receiveResult = await this.Socket.ReceiveAsync(new ArraySegment<byte>(buffer.Array, receiveOffset + buffer.Offset, buffer.Count - receiveOffset), this.CancellationTokenSource.Token);
334
335 if (receiveResult.MessageType == WebSocketMessageType.Close)
336 {
337 await this.SendCloseMessagesAsync(Properties.Resources.SocketClosedByClient, true);
338 return null;
339 }
340
341 receiveCount += receiveResult.Count;
342 receiveOffset += receiveResult.Count;
343 isResponseComplete = receiveResult.EndOfMessage;
344 }
345
346 receiveResult = new WebSocketReceiveResult(receiveCount, receiveResult.MessageType, true);
347 }
348 catch (Exception e)
349 {
350 if (!IsSendReceiveException(e))
351 {
352 throw;
353 }
354
355 return null;
356 }
357
358 return receiveResult;
359 }
360
361 /// <summary>
362 /// Dispose resources owned by this object.
363 /// </summary>
364 /// <param name="disposing">
365 /// True if called from IDisposable.Dispose. False if called by runtime during
366 /// finalization.
367 /// </param>
368 protected virtual void Dispose(bool disposing)
369 {
370 if (!this.isDisposed)
371 {
372 if (disposing)
373 {
374 this.disconnectionCheckTimer.Stop();
375
376 this.Socket.Dispose();
377 this.Socket = null;
378
379 this.CancellationTokenSource.Dispose();
380 this.CancellationTokenSource = null;
381 }
382
383 this.isDisposed = true;
384 }
385 }
386
387 /// <summary>
388 /// Start monitoring for client disconnection requests.
389 /// </summary>
390 /// <remarks>
391 /// Should not be called if legitimate (non-disconnection request) messages are
392 /// expected from client.
393 /// </remarks>
394 protected async void StartDisconnectionMonitor()
395 {
396 if (this.isDisposed)
397 {
398 return;
399 }
400
401 if (this.isDisconnectionMonitorStarted)
402 {
403 // Monitor is already started
404 return;
405 }
406
407 this.isDisconnectionMonitorStarted = true;
408 var dummyBuffer = new byte[1];
409
410 try
411 {
412 WebSocketReceiveResult result;
413
414 do
415 {
416 // We don't use a real cancellation token because explicitly cancelling a
417 // send or receive operation will put the socket in "Aborted" state, and
418 // it will appear to client as if we have forcefully closed the connection,
419 // when in reality our goal is just to passively monitor for client closing
420 // requests until socket connection is closed by either party.
421 var receiveTask = this.Socket.ReceiveAsync(new ArraySegment<byte>(dummyBuffer), CancellationToken.None);
422 await receiveTask;
423
424 if (!receiveTask.IsCompleted)
425 {
426 return;
427 }
428
429 result = receiveTask.Result;
430 }
431 while (result.MessageType != WebSocketMessageType.Close); // If message received was not a close message, keep looping.
432
433 // We have received a close message, so initiate closing actions.
434 await this.SendCloseMessagesAsync(Properties.Resources.SocketClosedByClient, true);
435 }
436 catch (WebSocketException)
437 {
438 // If connection closing is server-driven, our call to receive data will throw
439 // a WebSocketException and we won't receive any closing message from client.
440 }
441 }
442
443 /// <summary>
444 /// Determine if the specified exception is one of the standard web socket send/receive
445 /// exceptions.
446 /// </summary>
447 /// <param name="ex">
448 /// Caught exception.
449 /// </param>
450 /// <returns>
451 /// True if exception is of a type recognized as a standard web socket send/receive
452 /// exception.
453 /// </returns>
454 private static bool IsSendReceiveException(Exception ex)
455 {
456 return (ex is WebSocketException) || (ex is HttpListenerException) || (ex is OperationCanceledException);
457 }
458
459 /// <summary>
460 /// Checks if socket has been disconnected
461 /// </summary>
462 /// <returns>
463 /// true if web socket has been disconnected already. false otherwise.
464 /// </returns>
465 private async Task<bool> CheckForDisconnectionAsync()
466 {
467 if (this.isDisposed || this.isClosing)
468 {
469 return true;
470 }
471
472 // Every time we check for disconnection, stop the disconnection check timer
473 this.disconnectionCheckTimer.Stop();
474
475 bool isDisconnected = !this.IsOpen;
476
477 if (!isDisconnected)
478 {
479 // re-start timer if we're still connected
480 this.disconnectionCheckTimer.Start();
481 }
482 else
483 {
484 this.CancellationTokenSource.Cancel();
485 await this.SendCloseMessagesAsync(Properties.Resources.SocketClientDisconnectionDetected, false);
486 }
487
488 return isDisconnected;
489 }
490
491 /// <summary>
492 /// Send Closed event if it hasn't been sent already.
493 /// </summary>
494 private void SendClosed()
495 {
496 if (!this.closedSent)
497 {
498 if (this.closedAction != null)
499 {
500 this.closedAction(this);
501 }
502
503 this.closedSent = true;
504 }
505 }
506
507 /// <summary>
508 /// Cancel all pending operations, initiate web socket close handshake, send Closed
509 /// event to clients if necessary, and dispose of socket resources.
510 /// </summary>
511 /// <returns>
512 /// Await-able task.
513 /// </returns>
514 private async Task CloseAndDisposeAsync()
515 {
516 if (!await this.CheckForDisconnectionAsync())
517 {
518 this.CancellationTokenSource.Cancel();
519
520 await this.SendCloseMessagesAsync(Properties.Resources.SocketClosedByServer, false);
521 }
522
523 this.Dispose(true);
524
525 GC.SuppressFinalize(this);
526 }
527
528 /// <summary>
529 /// Gracefully close socket by performing the protocol close handshake and notify clients
530 /// that socket has been closed.
531 /// </summary>
532 /// <param name="closeMessage">
533 /// Human readable explanation as to why the connection is being closed.
534 /// </param>
535 /// <param name="awaitAcknowledgement">
536 /// True if we should wait for client acknowledgement of socket close request.
537 /// False if we shouldn't wait.
538 /// </param>
539 /// <returns>
540 /// Await-able task.
541 /// </returns>
542 private async Task SendCloseMessagesAsync(string closeMessage, bool awaitAcknowledgement)
543 {
544 // If we're already closing, there's no need to start closing again
545 if (this.isClosing)
546 {
547 return;
548 }
549
550 this.isClosing = true;
551
552 try
553 {
554 // Store task reference because instance variable could be set to null while
555 // we await, and await operator will reference task again once we are done
556 // awaiting.
557 var task = this.sendingTask;
558 if (task != null)
559 {
560 // Wait for the sending task to complete, if it is pending.
561 await task;
562 }
563 }
564 catch (Exception e)
565 {
566 if (!IsSendReceiveException(e))
567 {
568 throw;
569 }
570 }
571
572 try
573 {
574 if (awaitAcknowledgement)
575 {
576 await this.Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, closeMessage, CancellationToken.None);
577 }
578 else
579 {
580 await this.Socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeMessage, CancellationToken.None);
581 }
582 }
583 catch (Exception e)
584 {
585 if (!IsSendReceiveException(e))
586 {
587 throw;
588 }
589
590 Trace.TraceWarning(
591 "Problem encountered while closing socket. Client might have gone away abruptly without initiating socket close handshake.\n{0}",
592 e);
593 }
594
595 this.SendClosed();
596 }
597
598 /// <summary>
599 /// Handler for Tick event of disconnection check timer.
600 /// </summary>
601 /// <param name="sender">
602 /// Object that sent this event.
603 /// </param>
604 /// <param name="args">
605 /// Event arguments.
606 /// </param>
607 private async void OnDisconnectionCheckTimerTick(object sender, EventArgs args)
608 {
609 await this.CheckForDisconnectionAsync();
610 }
611 }
612}
Note: See TracBrowser for help on using the repository browser.