#region License
/*
* WebSocketServiceManager.cs
*
* The MIT License
*
* Copyright (c) 2012-2015 sta.blockhead
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using WebSocketSharp.Net;
namespace WebSocketSharp.Server
{
///
/// Manages the WebSocket services provided by the or
/// .
///
public class WebSocketServiceManager
{
#region Private Fields
private volatile bool _clean;
private Dictionary _hosts;
private Logger _logger;
private volatile ServerState _state;
private object _sync;
private TimeSpan _waitTime;
#endregion
#region Internal Constructors
internal WebSocketServiceManager ()
: this (new Logger ())
{
}
internal WebSocketServiceManager (Logger logger)
{
_logger = logger;
_clean = true;
_hosts = new Dictionary ();
_state = ServerState.Ready;
_sync = ((ICollection) _hosts).SyncRoot;
_waitTime = TimeSpan.FromSeconds (1);
}
#endregion
#region Public Properties
///
/// Gets the number of the WebSocket services.
///
///
/// An that represents the number of the services.
///
public int Count {
get {
lock (_sync)
return _hosts.Count;
}
}
///
/// Gets the host instances for the Websocket services.
///
///
/// An IEnumerable<WebSocketServiceHost> instance that provides an enumerator
/// which supports the iteration over the collection of the host instances for the services.
///
public IEnumerable Hosts {
get {
lock (_sync)
return _hosts.Values.ToList ();
}
}
///
/// Gets the WebSocket service host with the specified .
///
///
/// A instance that provides the access to
/// the information in the service, or if it's not found.
///
///
/// A that represents the absolute path to the service to find.
///
public WebSocketServiceHost this[string path] {
get {
WebSocketServiceHost host;
TryGetServiceHost (path, out host);
return host;
}
}
///
/// Gets a value indicating whether the manager cleans up the inactive sessions
/// in the WebSocket services periodically.
///
///
/// true if the manager cleans up the inactive sessions every 60 seconds;
/// otherwise, false.
///
public bool KeepClean {
get {
return _clean;
}
internal set {
lock (_sync) {
if (!(value ^ _clean))
return;
_clean = value;
foreach (var host in _hosts.Values)
host.KeepClean = value;
}
}
}
///
/// Gets the paths for the WebSocket services.
///
///
/// An IEnumerable<string> instance that provides an enumerator which supports
/// the iteration over the collection of the paths for the services.
///
public IEnumerable Paths {
get {
lock (_sync)
return _hosts.Keys.ToList ();
}
}
///
/// Gets the total number of the sessions in the WebSocket services.
///
///
/// An that represents the total number of the sessions in the services.
///
public int SessionCount {
get {
var cnt = 0;
foreach (var host in Hosts) {
if (_state != ServerState.Start)
break;
cnt += host.Sessions.Count;
}
return cnt;
}
}
///
/// Gets the wait time for the response to the WebSocket Ping or Close.
///
///
/// A that represents the wait time.
///
public TimeSpan WaitTime {
get {
return _waitTime;
}
internal set {
lock (_sync) {
if (value == _waitTime)
return;
_waitTime = value;
foreach (var host in _hosts.Values)
host.WaitTime = value;
}
}
}
#endregion
#region Private Methods
private void broadcast (Opcode opcode, byte[] data, Action completed)
{
var cache = new Dictionary ();
try {
foreach (var host in Hosts) {
if (_state != ServerState.Start)
break;
host.Sessions.Broadcast (opcode, data, cache);
}
if (completed != null)
completed ();
}
catch (Exception ex) {
_logger.Fatal (ex.ToString ());
}
finally {
cache.Clear ();
}
}
private void broadcast (Opcode opcode, Stream stream, Action completed)
{
var cache = new Dictionary ();
try {
foreach (var host in Hosts) {
if (_state != ServerState.Start)
break;
host.Sessions.Broadcast (opcode, stream, cache);
}
if (completed != null)
completed ();
}
catch (Exception ex) {
_logger.Fatal (ex.ToString ());
}
finally {
foreach (var cached in cache.Values)
cached.Dispose ();
cache.Clear ();
}
}
private void broadcastAsync (Opcode opcode, byte[] data, Action completed)
{
ThreadPool.QueueUserWorkItem (state => broadcast (opcode, data, completed));
}
private void broadcastAsync (Opcode opcode, Stream stream, Action completed)
{
ThreadPool.QueueUserWorkItem (state => broadcast (opcode, stream, completed));
}
private Dictionary> broadping (
byte[] frameAsBytes, TimeSpan timeout)
{
var ret = new Dictionary> ();
foreach (var host in Hosts) {
if (_state != ServerState.Start)
break;
ret.Add (host.Path, host.Sessions.Broadping (frameAsBytes, timeout));
}
return ret;
}
#endregion
#region Internal Methods
internal void Add (string path, Func initializer)
where TBehavior : WebSocketBehavior
{
path = HttpUtility.UrlDecode (path).TrimEndSlash ();
lock (_sync) {
WebSocketServiceHost host;
if (_hosts.TryGetValue (path, out host)) {
_logger.Error (
"A WebSocket service with the specified path has already existed."
);
return;
}
host = new WebSocketServiceHost (path, initializer, _logger);
if (!_clean)
host.KeepClean = false;
if (_waitTime != host.WaitTime)
host.WaitTime = _waitTime;
if (_state == ServerState.Start)
host.Start ();
_hosts.Add (path, host);
}
}
internal bool InternalTryGetServiceHost (
string path, out WebSocketServiceHost host
)
{
path = HttpUtility.UrlDecode (path).TrimEndSlash ();
lock (_sync)
return _hosts.TryGetValue (path, out host);
}
internal bool Remove (string path)
{
path = HttpUtility.UrlDecode (path).TrimEndSlash ();
WebSocketServiceHost host;
lock (_sync) {
if (!_hosts.TryGetValue (path, out host)) {
_logger.Error (
"A WebSocket service with the specified path could not be found."
);
return false;
}
_hosts.Remove (path);
}
if (host.State == ServerState.Start)
host.Stop (1001, String.Empty);
return true;
}
internal void Start ()
{
lock (_sync) {
foreach (var host in _hosts.Values)
host.Start ();
_state = ServerState.Start;
}
}
internal void Stop (ushort code, string reason)
{
lock (_sync) {
_state = ServerState.ShuttingDown;
foreach (var host in _hosts.Values)
host.Stop (code, reason);
_state = ServerState.Stop;
}
}
internal void Stop (CloseEventArgs e, bool send, bool receive)
{
lock (_sync) {
_state = ServerState.ShuttingDown;
var bytes = send ? WebSocketFrame.CreateCloseFrame (e.PayloadData, false).ToArray () : null;
foreach (var host in _hosts.Values)
host.Sessions.Stop (e, bytes, receive);
_hosts.Clear ();
_state = ServerState.Stop;
}
}
#endregion
#region Public Methods
///
/// Sends binary to every client in the WebSocket services.
///
///
/// An array of that represents the binary data to send.
///
public void Broadcast (byte[] data)
{
var msg = _state.CheckIfAvailable (false, true, false) ??
WebSocket.CheckSendParameter (data);
if (msg != null) {
_logger.Error (msg);
return;
}
if (data.LongLength <= WebSocket.FragmentLength)
broadcast (Opcode.Binary, data, null);
else
broadcast (Opcode.Binary, new MemoryStream (data), null);
}
///
/// Sends text to every client in the WebSocket services.
///
///
/// A that represents the text data to send.
///
public void Broadcast (string data)
{
var msg = _state.CheckIfAvailable (false, true, false) ??
WebSocket.CheckSendParameter (data);
if (msg != null) {
_logger.Error (msg);
return;
}
var bytes = data.UTF8Encode ();
if (bytes.LongLength <= WebSocket.FragmentLength)
broadcast (Opcode.Text, bytes, null);
else
broadcast (Opcode.Text, new MemoryStream (bytes), null);
}
///
/// Sends binary asynchronously to every client in
/// the WebSocket services.
///
///
/// This method doesn't wait for the send to be complete.
///
///
/// An array of that represents the binary data to send.
///
///
/// An delegate that references the method(s) called when
/// the send is complete.
///
public void BroadcastAsync (byte[] data, Action completed)
{
var msg = _state.CheckIfAvailable (false, true, false) ??
WebSocket.CheckSendParameter (data);
if (msg != null) {
_logger.Error (msg);
return;
}
if (data.LongLength <= WebSocket.FragmentLength)
broadcastAsync (Opcode.Binary, data, completed);
else
broadcastAsync (Opcode.Binary, new MemoryStream (data), completed);
}
///
/// Sends text asynchronously to every client in
/// the WebSocket services.
///
///
/// This method doesn't wait for the send to be complete.
///
///
/// A that represents the text data to send.
///
///
/// An delegate that references the method(s) called when
/// the send is complete.
///
public void BroadcastAsync (string data, Action completed)
{
var msg = _state.CheckIfAvailable (false, true, false) ??
WebSocket.CheckSendParameter (data);
if (msg != null) {
_logger.Error (msg);
return;
}
var bytes = data.UTF8Encode ();
if (bytes.LongLength <= WebSocket.FragmentLength)
broadcastAsync (Opcode.Text, bytes, completed);
else
broadcastAsync (Opcode.Text, new MemoryStream (bytes), completed);
}
///
/// Sends binary data from the specified asynchronously to
/// every client in the WebSocket services.
///
///
/// This method doesn't wait for the send to be complete.
///
///
/// A from which contains the binary data to send.
///
///
/// An that represents the number of bytes to send.
///
///
/// An delegate that references the method(s) called when
/// the send is complete.
///
public void BroadcastAsync (Stream stream, int length, Action completed)
{
var msg = _state.CheckIfAvailable (false, true, false) ??
WebSocket.CheckSendParameters (stream, length);
if (msg != null) {
_logger.Error (msg);
return;
}
stream.ReadBytesAsync (
length,
data => {
var len = data.Length;
if (len == 0) {
_logger.Error ("The data cannot be read from 'stream'.");
return;
}
if (len < length)
_logger.Warn (
String.Format (
"The data with 'length' cannot be read from 'stream':\n expected: {0}\n actual: {1}",
length,
len));
if (len <= WebSocket.FragmentLength)
broadcast (Opcode.Binary, data, completed);
else
broadcast (Opcode.Binary, new MemoryStream (data), completed);
},
ex => _logger.Fatal (ex.ToString ()));
}
///
/// Sends a Ping to every client in the WebSocket services.
///
///
/// A Dictionary<string, Dictionary<string, bool>> that contains
/// a collection of pairs of a service path and a collection of pairs of a session ID
/// and a value indicating whether the manager received a Pong from each client in a time,
/// or if this method isn't available.
///
public Dictionary> Broadping ()
{
var msg = _state.CheckIfAvailable (false, true, false);
if (msg != null) {
_logger.Error (msg);
return null;
}
return broadping (WebSocketFrame.EmptyPingBytes, _waitTime);
}
///
/// Sends a Ping with the specified to every client in
/// the WebSocket services.
///
///
/// A Dictionary<string, Dictionary<string, bool>> that contains
/// a collection of pairs of a service path and a collection of pairs of a session ID
/// and a value indicating whether the manager received a Pong from each client in a time,
/// or if this method isn't available or
/// is invalid.
///
///
/// A that represents the message to send.
///
public Dictionary> Broadping (string message)
{
if (message == null || message.Length == 0)
return Broadping ();
byte[] data = null;
var msg = _state.CheckIfAvailable (false, true, false) ??
WebSocket.CheckPingParameter (message, out data);
if (msg != null) {
_logger.Error (msg);
return null;
}
return broadping (WebSocketFrame.CreatePingFrame (data, false).ToArray (), _waitTime);
}
///
/// Tries to get the WebSocket service host with
/// the specified .
///
///
/// true if the service is successfully found;
/// otherwise, false.
///
///
/// A that represents the absolute path to
/// the service to find.
///
///
/// When this method returns, a
/// instance that provides the access to the information in
/// the service or if it is not found.
///
public bool TryGetServiceHost (string path, out WebSocketServiceHost host)
{
host = null;
if (path == null || path.Length == 0)
return false;
if (path[0] != '/')
return false;
if (path.IndexOfAny (new[] { '?', '#' }) > -1)
return false;
return InternalTryGetServiceHost (path, out host);
}
#endregion
}
}