Added WebSocketServer

This commit is contained in:
sta
2012-08-04 15:51:31 +09:00
parent b1463379af
commit ec79f59229
64 changed files with 936 additions and 68 deletions

View File

@@ -0,0 +1,42 @@
#region MIT License
/**
* ConnectionEventArgs.cs
*
* The MIT License
*
* Copyright (c) 2012 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;
namespace WebSocketSharp
{
public class ConnectionEventArgs : EventArgs
{
public WebSocket Socket { get; private set; }
public ConnectionEventArgs(WebSocket webSocket)
{
Socket = webSocket;
}
}
}

View File

@@ -0,0 +1,42 @@
#region MIT License
/**
* ErrorEventArgs.cs
*
* The MIT License
*
* Copyright (c) 2012 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;
namespace WebSocketSharp
{
public class ErrorEventArgs : EventArgs
{
public string Message { get; private set; }
public ErrorEventArgs(string message)
{
Message = message;
}
}
}

View File

@@ -32,6 +32,7 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -60,12 +61,14 @@ namespace WebSocketSharp
#region Private Fields
private AutoResetEvent _autoEvent;
private string _base64key;
private string _binaryType;
private string _extensions;
private Object _forClose;
private Object _forSend;
private int _fragmentLen;
private bool _isClient;
private Thread _msgThread;
private NetworkStream _netStream;
private string _protocol;
@@ -125,7 +128,7 @@ namespace WebSocketSharp
switch (value)
{
case WsState.OPEN:
messageThreadStart();
startMessageThread();
OnOpen.Emit(this, EventArgs.Empty);
break;
case WsState.CLOSING:
@@ -153,7 +156,7 @@ namespace WebSocketSharp
public event EventHandler OnOpen;
public event EventHandler<MessageEventArgs> OnMessage;
public event EventHandler<MessageEventArgs> OnError;
public event EventHandler<ErrorEventArgs> OnError;
public event EventHandler<CloseEventArgs> OnClose;
#endregion
@@ -174,27 +177,43 @@ namespace WebSocketSharp
#endregion
#region Internal Constructors
internal WebSocket(string url, TcpClient tcpClient)
: this()
{
_uri = new Uri(url);
if (!isValidScheme(_uri))
{
throw new ArgumentException("Unsupported WebSocket URI scheme: " + _uri.Scheme);
}
_tcpClient = tcpClient;
_isClient = false;
}
#endregion
#region Public Constructors
public WebSocket(string url, params string[] protocols)
: this()
: this()
{
_uri = new Uri(url);
string scheme = _uri.Scheme;
if (scheme != "ws" && scheme != "wss")
_uri = new Uri(url);
if (!isValidScheme(_uri))
{
throw new ArgumentException("Unsupported WebSocket URI scheme: " + scheme);
throw new ArgumentException("Unsupported WebSocket URI scheme: " + _uri.Scheme);
}
_protocols = protocols.ToString(", ");
_protocols = protocols.ToString(", ");
_isClient = true;
}
public WebSocket(
string url,
EventHandler onOpen,
EventHandler<MessageEventArgs> onMessage,
EventHandler<MessageEventArgs> onError,
EventHandler<ErrorEventArgs> onError,
EventHandler<CloseEventArgs> onClose,
params string[] protocols)
: this(url, protocols)
@@ -211,10 +230,37 @@ namespace WebSocketSharp
#region Private Methods
private void acceptHandshake()
{
string msg, response;
string[] request;
request = receiveOpeningHandshake();
#if DEBUG
Console.WriteLine("\nWS: Info@acceptHandshake: Opening handshake from client:\n");
foreach (string s in request)
{
Console.WriteLine("{0}", s);
}
#endif
if (!isValidRequest(request, out msg))
{
throw new InvalidOperationException(msg);
}
response = createResponseHandshake();
#if DEBUG
Console.WriteLine("\nWS: Info@acceptHandshake: Opening handshake from server:\n{0}", response);
#endif
sendResponseHandshake(response);
ReadyState = WsState.OPEN;
}
private void close(PayloadData data)
{
#if DEBUG
Console.WriteLine("WS: Info@close: Current thread IsBackground?: {0}", Thread.CurrentThread.IsBackground);
Console.WriteLine("\nWS: Info@close: Current thread IsBackground?: {0}", Thread.CurrentThread.IsBackground);
#endif
lock(_forClose)
{
@@ -291,13 +337,20 @@ namespace WebSocketSharp
if (!Thread.CurrentThread.IsBackground)
{
_msgThread.Join(5000);
if (_isClient)
{
_msgThread.Join(5000);
}
else
{
_autoEvent.WaitOne();
}
}
ReadyState = WsState.CLOSED;
}
private void createConnection()
private void createClientStream()
{
string scheme = _uri.Scheme;
string host = _uri.DnsSafeHost;
@@ -400,6 +453,45 @@ namespace WebSocketSharp
crlf;
}
private string createResponseHandshake()
{
string crlf = "\r\n";
string resStatus = "HTTP/1.1 101 Switching Protocols" + crlf;
string resUpgrade = "Upgrade: websocket" + crlf;
string resConnection = "Connection: Upgrade" + crlf;
string secWsAccept = String.Format("Sec-WebSocket-Accept: {0}{1}", createExpectedKey(), crlf);
//string secWsProtocol = "Sec-WebSocket-Protocol: chat" + crlf;
string secWsVersion = String.Format("Sec-WebSocket-Version: {0}{1}", _version, crlf);
return resStatus +
resUpgrade +
resConnection +
secWsAccept +
//secWsProtocol +
secWsVersion +
crlf;
}
private void createServerStream()
{
_netStream = _tcpClient.GetStream();
if (_uri.Scheme == "wss")
{
_sslStream = new SslStream(_netStream);
string certPath = ConfigurationManager.AppSettings["ServerCertPath"];
_sslStream.AuthenticateAsServer(new X509Certificate(certPath));
_wsStream = new WsStream<SslStream>(_sslStream);
}
else
{
_wsStream = new WsStream<NetworkStream>(_netStream);
}
}
private void doHandshake()
{
string msg, request;
@@ -407,11 +499,11 @@ namespace WebSocketSharp
request = createOpeningHandshake();
#if DEBUG
Console.WriteLine("WS: Info@doHandshake: Opening handshake from client:\n{0}", request);
Console.WriteLine("\nWS: Info@doHandshake: Opening handshake from client:\n{0}", request);
#endif
response = sendOpeningHandshake(request);
#if DEBUG
Console.WriteLine("WS: Info@doHandshake: Opening handshake from server:");
Console.WriteLine("\nWS: Info@doHandshake: Opening handshake from server:\n");
foreach (string s in response)
{
Console.WriteLine("{0}", s);
@@ -432,18 +524,123 @@ namespace WebSocketSharp
var caller = callerFrame.GetMethod();
Console.WriteLine("WS: Error@{0}: {1}", caller.Name, message);
#endif
OnError.Emit(this, new MessageEventArgs(message));
OnError.Emit(this, new ErrorEventArgs(message));
}
private bool isValidRequest(string[] request, out string message)
{
string reqConnection, reqHost, reqUpgrade, secWsVersion;
string[] reqRequest;
List<string> extensionList = new List<string>();
Func<string, Func<string, string, string>> func = s =>
{
return (e, a) =>
{
return String.Format("Invalid request {0} value: {1}(expected: {2})", s, a, e);
};
};
string expectedHost = _uri.DnsSafeHost;
int port = ((IPEndPoint)_tcpClient.Client.LocalEndPoint).Port;
if (port != 80)
{
expectedHost += ":" + port;
}
reqRequest = request[0].Split(' ');
if ("GET".NotEqualsDo(reqRequest[0], func("HTTP Method"), out message, false))
{
return false;
}
if ("HTTP/1.1".NotEqualsDo(reqRequest[2], func("HTTP Version"), out message, false))
{
return false;
}
for (int i = 1; i < request.Length; i++)
{
if (request[i].Contains("Connection:"))
{
reqConnection = request[i].GetHeaderValue(":");
if ("Upgrade".NotEqualsDo(reqConnection, func("Connection"), out message, true))
{
return false;
}
}
else if (request[i].Contains("Host:"))
{
reqHost = request[i].GetHeaderValue(":");
if (expectedHost.NotEqualsDo(reqHost, func("Host"), out message, true))
{
return false;
}
}
else if (request[i].Contains("Origin:"))
{
continue;
}
else if (request[i].Contains("Upgrade:"))
{
reqUpgrade = request[i].GetHeaderValue(":");
if ("websocket".NotEqualsDo(reqUpgrade, func("Upgrade"), out message, true))
{
return false;
}
}
else if (request[i].Contains("Sec-WebSocket-Extensions:"))
{
extensionList.Add(request[i].GetHeaderValue(":"));
}
else if (request[i].Contains("Sec-WebSocket-Key:"))
{
_base64key = request[i].GetHeaderValue(":");
}
else if (request[i].Contains("Sec-WebSocket-Protocol:"))
{
_protocols = request[i].GetHeaderValue(":");
#if DEBUG
Console.WriteLine("WS: Info@isValidRequest: Sub protocol: {0}", _protocols);
#endif
}
else if (request[i].Contains("Sec-WebSocket-Version:"))
{
secWsVersion = request[i].GetHeaderValue(":");
if (_version.NotEqualsDo(secWsVersion, func("Sec-WebSocket-Version"), out message, true))
{
return false;
}
}
else
{
Console.WriteLine("WS: Info@isValidRequest: Unsupported request header line: {0}", request[i]);
}
}
if (String.IsNullOrEmpty(_base64key))
{
message = "Sec-WebSocket-Key header field does not exist or the value isn't set.";
return false;
}
#if DEBUG
foreach (string s in extensionList)
{
Console.WriteLine("WS: Info@isValidRequest: Extensions: {0}", s);
}
#endif
message = String.Empty;
return true;
}
private bool isValidResponse(string[] response, out string message)
{
Func<string, Func<string, string, string>> func;
string resUpgrade, resConnection;
string secWsAccept, secWsVersion;
string resUpgrade, resConnection, secWsAccept, secWsVersion;
string[] resStatus;
List<string> extensionList = new List<string>();
func = s =>
Func<string, Func<string, string, string>> func = s =>
{
return (e, a) =>
{
@@ -501,9 +698,10 @@ namespace WebSocketSharp
else if (response[i].Contains("Sec-WebSocket-Version:"))
{
secWsVersion = response[i].GetHeaderValue(":");
#if DEBUG
Console.WriteLine("WS: Info@isValidResponse: Version: {0}", secWsVersion);
#endif
if (_version.NotEqualsDo(secWsVersion, func("Sec-WebSocket-Version"), out message, true))
{
return false;
}
}
else
{
@@ -520,38 +718,81 @@ namespace WebSocketSharp
return true;
}
private bool isValidScheme(Uri uri)
{
string scheme = uri.Scheme;
if (scheme == "ws" || scheme == "wss")
{
return true;
}
return false;
}
private void message()
{
#if DEBUG
Console.WriteLine("WS: Info@message: Current thread IsBackground?: {0}", Thread.CurrentThread.IsBackground);
#endif
MessageEventArgs eventArgs;
while (_readyState == WsState.OPEN)
try
{
try
{
eventArgs = receive();
MessageEventArgs eventArgs = receive();
if (eventArgs != null)
{
OnMessage.Emit(this, eventArgs);
}
}
catch (WsReceivedTooBigMessageException ex)
if (eventArgs != null)
{
close(CloseStatusCode.TOO_BIG, ex.Message);
OnMessage.Emit(this, eventArgs);
}
}
catch (WsReceivedTooBigMessageException ex)
{
close(CloseStatusCode.TOO_BIG, ex.Message);
}
}
private void messageLoop()
{
#if DEBUG
Console.WriteLine("WS: Info@message: Exit message method.");
Console.WriteLine("\nWS: Info@messageLoop: Current thread IsBackground?: {0}", Thread.CurrentThread.IsBackground);
#endif
while (_readyState == WsState.OPEN)
{
message();
}
#if DEBUG
Console.WriteLine("WS: Info@messageLoop: Exit messageLoop method.");
#endif
}
private void messageThreadStart()
private void startMessageThread()
{
_msgThread = new Thread(new ThreadStart(message));
_msgThread.IsBackground = true;
_msgThread.Start();
if (_isClient)
{
_msgThread = new Thread(new ThreadStart(messageLoop));
_msgThread.IsBackground = true;
_msgThread.Start();
}
else
{
_autoEvent = new AutoResetEvent(false);
Action act = () =>
{
if (_readyState == WsState.OPEN)
{
message();
}
};
AsyncCallback callback = (ar) =>
{
act.EndInvoke(ar);
if (_readyState == WsState.OPEN)
{
act.BeginInvoke(callback, null);
}
else
{
_autoEvent.Set();
}
};
act.BeginInvoke(callback, null);
}
}
private MessageEventArgs receive()
@@ -671,6 +912,26 @@ namespace WebSocketSharp
pong(payloadData);
}
private string[] receiveOpeningHandshake()
{
var readData = new List<byte>();
while (true)
{
if (_wsStream.ReadByte().EqualsAndSaveTo('\r', readData) &&
_wsStream.ReadByte().EqualsAndSaveTo('\n', readData) &&
_wsStream.ReadByte().EqualsAndSaveTo('\r', readData) &&
_wsStream.ReadByte().EqualsAndSaveTo('\n', readData))
{
break;
}
}
return Encoding.UTF8.GetString(readData.ToArray())
.Replace("\r\n", "\n").Replace("\n\n", "\n").TrimEnd('\n')
.Split('\n');
}
private bool send(WsFrame frame)
{
if (_readyState != WsState.OPEN)
@@ -832,6 +1093,12 @@ namespace WebSocketSharp
.Split('\n');
}
private void sendResponseHandshake(string value)
{
var buffer = Encoding.UTF8.GetBytes(value);
_wsStream.Write(buffer, 0, buffer.Length);
}
#endregion
#region Public Methods
@@ -840,14 +1107,22 @@ namespace WebSocketSharp
{
if (_readyState == WsState.OPEN)
{
Console.WriteLine("WS: Info@Connect: Connection is already established.");
Console.WriteLine("\nWS: Info@Connect: Connection is already established.");
return;
}
try
{
createConnection();
doHandshake();
{
if (_isClient)
{
createClientStream();
doHandshake();
}
else
{
createServerStream();
acceptHandshake();
}
}
catch (Exception ex)
{

View File

@@ -0,0 +1,237 @@
#region MIT License
/**
* WebSocketServer.cs
*
* A C# implementation of a WebSocket protocol server.
*
* The MIT License
*
* Copyright (c) 2012 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.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using WebSocketSharp.Frame;
namespace WebSocketSharp
{
public class WebSocketServer
{
#region Private Fields
private TcpListener _tcpListener;
private Uri _uri;
private SynchronizedCollection<WebSocket> _webSockets;
#endregion
#region Properties
public IPAddress Address
{
get { return Endpoint.Address; }
}
public IPEndPoint Endpoint
{
get { return (IPEndPoint)_tcpListener.LocalEndpoint; }
}
public int Port
{
get { return Endpoint.Port; }
}
public string Url
{
get { return _uri.ToString(); }
}
#endregion
#region Events
public event EventHandler<ConnectionEventArgs> OnConnection;
public event EventHandler<ErrorEventArgs> OnError;
#endregion
#region Public Constructor
public WebSocketServer(string url)
{
_uri = new Uri(url);
if (!isValidScheme(_uri))
{
throw new ArgumentException("Unsupported WebSocket URI scheme: " + _uri.Scheme);
}
string scheme = _uri.Scheme;
int port = _uri.Port;
if (port <= 0)
{
if (scheme == "wss")
{
port = 443;
}
else
{
port = 80;
}
}
_tcpListener = new TcpListener(IPAddress.Any, port);
_webSockets = new SynchronizedCollection<WebSocket>();
}
#endregion
#region Private Methods
private void acceptClient(IAsyncResult ar)
{
TcpListener listener = (TcpListener)ar.AsyncState;
if (listener.Server == null || !listener.Server.IsBound)
{
return;
}
try
{
TcpClient client = listener.EndAcceptTcpClient(ar);
WebSocket ws = new WebSocket(_uri.ToString(), client);
OnConnection.Emit(this, new ConnectionEventArgs(ws));
_webSockets.Add(ws);
ws.Connect();
}
catch (ObjectDisposedException)
{
// TcpListener has been stopped.
return;
}
catch (Exception ex)
{
error(ex.Message);
}
listener.BeginAcceptTcpClient(acceptClient, listener);
}
private void error(string message)
{
#if DEBUG
var callerFrame = new StackFrame(1);
var caller = callerFrame.GetMethod();
Console.WriteLine("WSSV: Error@{0}: {1}", caller.Name, message);
#endif
OnError.Emit(this, new ErrorEventArgs(message));
}
private bool isValidScheme(Uri uri)
{
string scheme = uri.Scheme;
if (scheme == "ws" || scheme == "wss")
{
return true;
}
return false;
}
#endregion
#region Public Methods
public void Close(CloseStatusCode code, string reason)
{
lock (_webSockets.SyncRoot)
{
foreach (WebSocket ws in _webSockets)
{
if (ws.ReadyState == WsState.OPEN)
{
ws.Close(code, reason);
}
}
}
}
public void Send(byte[] data)
{
WaitCallback broadcast = (state) =>
{
lock (_webSockets.SyncRoot)
{
foreach (WebSocket ws in _webSockets)
{
if (ws.ReadyState == WsState.OPEN)
{
ws.Send(data);
}
}
}
};
ThreadPool.QueueUserWorkItem(broadcast);
}
public void Send(string data)
{
WaitCallback broadcast = (state) =>
{
lock (_webSockets.SyncRoot)
{
foreach (WebSocket ws in _webSockets)
{
if (ws.ReadyState == WsState.OPEN)
{
ws.Send(data);
}
}
}
};
ThreadPool.QueueUserWorkItem(broadcast);
}
public void Start()
{
_tcpListener.Start();
_tcpListener.BeginAcceptTcpClient(acceptClient, _tcpListener);
}
public void Stop()
{
_tcpListener.Stop();
Close(CloseStatusCode.NORMAL, String.Empty);
}
#endregion
}
}

View File

@@ -53,11 +53,11 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Configuration" />
</ItemGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.cs" />
<Compile Include="Ext.cs" />
<Compile Include="WebSocket.cs" />
<Compile Include="WsState.cs" />
<Compile Include="MessageEventArgs.cs" />
<Compile Include="CloseEventArgs.cs" />
@@ -72,6 +72,10 @@
<Compile Include="Frame\Opcode.cs" />
<Compile Include="Frame\PayloadData.cs" />
<Compile Include="Frame\Rsv.cs" />
<Compile Include="ConnectionEventArgs.cs" />
<Compile Include="ErrorEventArgs.cs" />
<Compile Include="WebSocketServer.cs" />
<Compile Include="WebSocket.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ItemGroup>

Binary file not shown.