├── .gitignore ├── LICENSE.txt ├── README.md ├── WebSockets.sln ├── WebSockets.zip ├── WebSockets ├── Client │ └── WebSocketClient.cs ├── Common │ ├── BinaryReaderWriter.cs │ ├── IWebSocketLogger.cs │ ├── WebSocketBase.cs │ ├── WebSocketCloseCode.cs │ ├── WebSocketFrame.cs │ ├── WebSocketFrameReader.cs │ ├── WebSocketFrameWriter.cs │ └── WebSocketOpCode.cs ├── Events │ ├── BinaryFrameEventArgs.cs │ ├── BinaryMultiFrameEventArgs.cs │ ├── ConnectionCloseEventArgs.cs │ ├── PingEventArgs.cs │ ├── PongEventArgs.cs │ ├── TextFrameEventArgs.cs │ └── TextMultiFrameEventArgs.cs ├── Exceptions │ ├── EntityTooLargeException.cs │ ├── ServerListenerSocketException.cs │ ├── TODO.txt │ ├── WebSocketHandshakeFailedException.cs │ └── WebSocketVersionNotSupportedException.cs ├── MimeTypes.config ├── Properties │ └── AssemblyInfo.cs ├── README.txt ├── Server │ ├── ConnectionDetails.cs │ ├── ConnectionType.cs │ ├── Http │ │ ├── BadRequestService.cs │ │ ├── HttpHelper.cs │ │ ├── HttpService.cs │ │ ├── MimeTypes.cs │ │ └── MimeTypesFactory.cs │ ├── IService.cs │ ├── IServiceFactory.cs │ ├── WebServer.cs │ └── WebSocket │ │ └── WebSocketService.cs └── WebSockets.csproj ├── WebSocketsCmd ├── App.config ├── Client │ └── ChatWebSocketClient.cs ├── CustomConsoleTraceListener.cs ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Settings.Designer.cs │ └── Settings.settings ├── README.md ├── Server │ ├── ChatWebSocketService.cs │ └── ServiceFactory.cs ├── WebSocketLogger.cs ├── WebSocketsCmd.csproj ├── client.html └── favicon.ico └── gitpublish.bat /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | _ReSharper.* 4 | bin 5 | obj 6 | packages -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Haig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

WebSocket Server in c#

2 | 3 |

NOTE: This is no longer maintained. See https://github.com/ninjasource/Ninja.WebSockets

4 |

Set WebSockets.Cmd as the startup project

5 | 6 |

License

7 | 8 | The MIT License (MIT) 9 |
See LICENCE.txt 10 | 11 |

Introduction

12 | 13 |

A lot of the Web Socket examples out there are for old Web Socket versions and included complicated code (and external libraries) for fall back communication. All modern browsers that anyone cares about (including safari on an iphone) support at least version 13 of the Web Socket protocol so I'd rather not complicate things. This is a bare bones implementation of the web socket protocol in C# with no external libraries involved. You can connect using standard HTML5 JavaScript.

14 | 15 |

This application serves up basic html pages as well as handling WebSocket connections. This may seem confusing but it allows you to send the client the html they need to make a web socket connection and also allows you to share the same port. However, the HttpConnection is very rudimentary. I'm sure it has some glaring security problems. It was just made to make this demo easier to run. Replace it with your own or don't use it.

16 | 17 |

Background

18 | 19 |

There is nothing magical about Web Sockets. The spec is easy to follow and there is no need to use special libraries. At one point, I was even considering somehow communicating with Node.js but that is not necessary. The spec can be a bit fiddly with bits and bytes but this was probably done to keep the overheads low. This is my first CodeProject article and I hope you will find it easy to follow. The following links offer some great advice:

20 | 21 |

Step by step guide

22 | 23 | 26 | 27 |

The official Web Socket spec

28 | 29 | 32 | 33 |

Some useful stuff in C#

34 | 35 | 38 | 39 |

Using the Code

40 | 41 |

NOTE You will get a firewall warning because you are listening on a port. This is normal for any socket based server.

42 |

A good place to put a breakpoint is in the WebServer class in the HandleAsyncConnection function. Note that this is a multithreaded server so you may want to freeze threads if this gets confusing. The console output prints the thread id to make things easier. If you want to skip past all the plumbing, then another good place to start is the Respond function in the WebSocketConnection class. If you are not interested in the inner workings of Web Sockets and just want to use them, then take a look at the OnTextFrame in the ChatWebSocketConnection class. See below.

43 | 44 |

Implementation of a chat web socket connection is as follows:

45 | 46 |
 47 | internal class ChatWebSocketService : WebSocketService
 48 | {
 49 |     private readonly IWebSocketLogger _logger;
 50 | 
 51 |     public ChatWebSocketService(NetworkStream networkStream, TcpClient tcpClient, string header, IWebSocketLogger logger)
 52 |         : base(networkStream, tcpClient, header, true, logger)
 53 |     {
 54 |         _logger = logger;
 55 |     }
 56 | 
 57 |     protected override void OnTextFrame(string text)
 58 |     {
 59 |         string response = "ServerABC: " + text;
 60 |         base.Send(response);
 61 |         _logger.Information(this.GetType(), response);
 62 |     }
 63 | }
64 | 65 |

The factory used to create the connection is as follows:

66 | 67 |
 68 | internal class ServiceFactory : IServiceFactory
 69 | {
 70 |     public ServiceFactory(string webRoot, IWebSocketLogger logger)
 71 |     {
 72 |         _logger = logger;
 73 |         _webRoot = webRoot;
 74 |     }
 75 | 
 76 |     public IService CreateInstance(ConnectionDetails connectionDetails)
 77 |     {
 78 |         switch (connectionDetails.ConnectionType)
 79 |         {
 80 |             case ConnectionType.WebSocket:
 81 |                 // you can support different kinds of web socket connections using a different path
 82 |                 if (connectionDetails.Path == "/chat")
 83 |                 {
 84 |                     return new ChatWebSocketService(connectionDetails.NetworkStream, connectionDetails.TcpClient, connectionDetails.Header, _logger);
 85 |                 }
 86 |                 break;
 87 |             case ConnectionType.Http:
 88 |                 // this path actually refers to the reletive location of some html file or image
 89 |                 return new HttpService(connectionDetails.NetworkStream, connectionDetails.Path, _webRoot, _logger);
 90 |         }
 91 | 
 92 |         return new BadRequestService(connectionDetails.NetworkStream, connectionDetails.Header, _logger);
 93 |     }
 94 | }
95 | 96 |

HTML5 JavaScript used to connect:

97 | 98 |
 99 | // open the connection to the Web Socket server
100 | var CONNECTION = new WebSocket('ws://localhost/chat');
101 | 
102 | // Log messages from the server
103 | CONNECTION.onmessage = function (e) {
104 |     console.log(e.data);
105 | };
106 |         
107 | CONNECTION.send('Hellow World');
108 | 109 |

Starting the server and the test client:

110 | 111 |
112 | private static void Main(string[] args)
113 | {
114 |     IWebSocketLogger logger = new WebSocketLogger();
115 |                 
116 |     try
117 |     {
118 |         string webRoot = Settings.Default.WebRoot;
119 |         int port = Settings.Default.Port;
120 | 
121 |         // used to decide what to do with incoming connections
122 |         ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger);
123 | 
124 |         using (WebServer server = new WebServer(serviceFactory, logger))
125 |         {
126 |             server.Listen(port);
127 |             Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient));
128 |             clientThread.IsBackground = false;
129 |             clientThread.Start(logger);
130 |             Console.ReadKey();
131 |         }
132 |     }
133 |     catch (Exception ex)
134 |     {
135 |         logger.Error(null, ex);
136 |         Console.ReadKey();
137 |     }
138 | }
139 | 140 |

The test client runs a short self test to make sure that everything is fine. Opening and closing handshakes are tested here.

141 | 142 |

Web Socket Protocol

143 | 144 |

The first thing to realize about the protocol is that it is, in essence, a basic duplex TCP/IP socket connection. The connection starts off with the client connecting to a remote server and sending http header text to that server. The header text asks the web server to upgrade the connection to a web socket connection. This is done as a handshake where the web server responds with an appropriate http text header and from then onwards, the client and server will talk the Web Socket language.

145 | 146 |

Server Handshake

147 | 148 |
149 | Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)");
150 | Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)");
151 | 
152 | // check the version. Support version 13 and above
153 | const int WebSocketVersion = 13;
154 | int secWebSocketVersion = Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim());
155 | if (secWebSocketVersion < WebSocketVersion)
156 | {
157 |     throw new WebSocketVersionNotSupportedException(string.Format("WebSocket Version {0} not suported. Must be {1} or above", secWebSocketVersion, WebSocketVersion));
158 | }
159 | 
160 | string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim();
161 | string setWebSocketAccept = base.ComputeSocketAcceptString(secWebSocketKey);
162 | string response = ("HTTP/1.1 101 Switching Protocols" + Environment.NewLine
163 |                             + "Connection: Upgrade" + Environment.NewLine
164 |                             + "Upgrade: websocket" + Environment.NewLine
165 |                             + "Sec-WebSocket-Accept: " + setWebSocketAccept);
166 | 
167 | HttpHelper.WriteHttpHeader(response, networkStream);
168 | 169 |

This computes the accept string:

170 | 171 |
172 | /// <summary>
173 | /// Combines the key supplied by the client with a guid and returns the sha1 hash of the combination
174 | /// </summary>
175 | public static string ComputeSocketAcceptString(string secWebSocketKey)
176 | {
177 |     // this is a guid as per the web socket spec
178 |     const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
179 | 
180 |     string concatenated = secWebSocketKey + webSocketGuid;
181 |     byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
182 |     byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
183 |     string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
184 |     return secWebSocketAccept;
185 | }
186 | 187 |

Client Handshake

188 |
189 | Uri uri = _uri;
190 | WebSocketFrameReader reader = new WebSocketFrameReader();
191 | Random rand = new Random();
192 | byte[] keyAsBytes = new byte[16];
193 | rand.NextBytes(keyAsBytes);
194 | string secWebSocketKey = Convert.ToBase64String(keyAsBytes);
195 | 
196 | string handshakeHttpRequestTemplate = @"GET {0} HTTP/1.1{4}" +
197 |                                         "Host: {1}:{2}{4}" +
198 |                                         "Upgrade: websocket{4}" +
199 |                                         "Connection: Upgrade{4}" +
200 |                                         "Sec-WebSocket-Key: {3}{4}" +
201 |                                         "Sec-WebSocket-Version: 13{4}{4}";
202 | 
203 | string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate, uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey, Environment.NewLine);
204 | byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
205 | networkStream.Write(httpRequest, 0, httpRequest.Length);
206 | 207 |

Reading and Writing

208 | 209 |

After the handshake as been performed, the server goes into a read loop. The following two class convert a stream of bytes to a web socket frame and visa versa: WebSocketFrameReader and WebSocketFrameWriter.

210 | 211 |
212 | // from WebSocketFrameReader class
213 | public WebSocketFrame Read(NetworkStream stream, Socket socket)
214 | {
215 |     byte byte1;
216 | 
217 |     try
218 |     {
219 |         byte1 = (byte) stream.ReadByte();
220 |     }
221 |     catch (IOException)
222 |     {
223 |         if (socket.Connected)
224 |         {
225 |             throw;
226 |         }
227 |         else
228 |         {
229 |             return null;
230 |         }
231 |     }
232 | 
233 |     // process first byte
234 |     byte finBitFlag = 0x80;
235 |     byte opCodeFlag = 0x0F;
236 |     bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag;
237 |     WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag);
238 | 
239 |     // read and process second byte
240 |     byte byte2 = (byte) stream.ReadByte();
241 |     byte maskFlag = 0x80;
242 |     bool isMaskBitSet = (byte2 & maskFlag) == maskFlag;
243 |     uint len = ReadLength(byte2, stream);
244 |     byte[] decodedPayload;
245 | 
246 |     // use the masking key to decode the data if needed
247 |     if (isMaskBitSet)
248 |     {
249 |         const int maskKeyLen = 4;
250 |         byte[] maskKey = BinaryReaderWriter.ReadExactly(maskKeyLen, stream);
251 |         byte[] encodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
252 |         decodedPayload = new byte[len];
253 | 
254 |         // apply the mask key
255 |         for (int i = 0; i < encodedPayload.Length; i++)
256 |         {
257 |             decodedPayload[i] = (Byte) (encodedPayload[i] ^ maskKey[i%maskKeyLen]);
258 |         }
259 |     }
260 |     else
261 |     {
262 |         decodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
263 |     }
264 | 
265 |     WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, decodedPayload, true);
266 |     return frame;
267 | }
268 | 269 |
270 | // from WebSocketFrameWriter class
271 | public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame)
272 | {
273 |     // best to write everything to a memory stream before we push it onto the wire
274 |     // not really necessary but I like it this way
275 |     using (MemoryStream memoryStream = new MemoryStream())
276 |     {
277 |         byte finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00;
278 |         byte byte1 = (byte)(finBitSetAsByte | (byte)opCode);
279 |         memoryStream.WriteByte(byte1);
280 | 
281 |         // NB, dont set the mask flag. No need to mask data from server to client
282 |         // depending on the size of the length we want to write it as a byte, ushort or ulong
283 |         if (payload.Length < 126)
284 |         {
285 |             byte byte2 = (byte)payload.Length;
286 |             memoryStream.WriteByte(byte2);
287 |         }
288 |         else if (payload.Length <= ushort.MaxValue)
289 |         {
290 |             byte byte2 = 126;
291 |             memoryStream.WriteByte(byte2);
292 |             BinaryReaderWriter.WriteUShort((ushort)payload.Length, memoryStream, false);
293 |         }
294 |         else
295 |         {
296 |             byte byte2 = 127;
297 |             memoryStream.WriteByte(byte2);
298 |             BinaryReaderWriter.WriteULong((ulong)payload.Length, memoryStream, false);
299 |         }
300 | 
301 |         memoryStream.Write(payload, 0, payload.Length);
302 |         byte[] buffer = memoryStream.ToArray();
303 |         _stream.Write(buffer, 0, buffer.Length);
304 |     }            
305 | }
306 | 
307 | 308 | 309 |

Points of Interest

310 | 311 |

Problems with Proxy Servers:
312 | Proxy servers which have not been configured to support Web sockets will not work well with them.
313 | I suggest that you use transport layer security if you want this to work across the wider internet especially from within a corporation.

314 | 315 |

History

316 | 317 | 322 | -------------------------------------------------------------------------------- /WebSockets.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSockets", "WebSockets\WebSockets.csproj", "{D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketsCmd", "WebSocketsCmd\WebSocketsCmd.csproj", "{8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|Mixed Platforms = Debug|Mixed Platforms 12 | Debug|x86 = Debug|x86 13 | Release|Any CPU = Release|Any CPU 14 | Release|Mixed Platforms = Release|Mixed Platforms 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 21 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 22 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|x86.ActiveCfg = Debug|Any CPU 23 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 26 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|Mixed Platforms.Build.0 = Release|Any CPU 27 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|x86.ActiveCfg = Release|Any CPU 28 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|Any CPU.ActiveCfg = Debug|x86 29 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 30 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|Mixed Platforms.Build.0 = Debug|x86 31 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|x86.ActiveCfg = Debug|x86 32 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|x86.Build.0 = Debug|x86 33 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|Any CPU.ActiveCfg = Release|x86 34 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|Mixed Platforms.ActiveCfg = Release|x86 35 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|Mixed Platforms.Build.0 = Release|x86 36 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|x86.ActiveCfg = Release|x86 37 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|x86.Build.0 = Release|x86 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /WebSockets.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninjasource/websocket-server/3fc27cf05077235c9f764728d3ec9e7139845275/WebSockets.zip -------------------------------------------------------------------------------- /WebSockets/Client/WebSocketClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Security; 7 | using System.Net.Sockets; 8 | using System.Text; 9 | using System.Text.RegularExpressions; 10 | using WebSockets.Common; 11 | using System.Diagnostics; 12 | using System.Security.Policy; 13 | using WebSockets.Exceptions; 14 | using WebSockets.Server.WebSocket; 15 | using WebSockets.Server.Http; 16 | using System.Threading; 17 | using WebSockets.Events; 18 | using System.Threading.Tasks; 19 | using System.Security.Cryptography.X509Certificates; 20 | 21 | namespace WebSockets.Client 22 | { 23 | public class WebSocketClient : WebSocketBase, IDisposable 24 | { 25 | private readonly bool _noDelay; 26 | private readonly IWebSocketLogger _logger; 27 | private TcpClient _tcpClient; 28 | private Stream _stream; 29 | private Uri _uri; 30 | private ManualResetEvent _conectionCloseWait; 31 | 32 | public WebSocketClient(bool noDelay, IWebSocketLogger logger) 33 | : base(logger) 34 | { 35 | _noDelay = noDelay; 36 | _logger = logger; 37 | _conectionCloseWait = new ManualResetEvent(false); 38 | } 39 | 40 | // The following method is invoked by the RemoteCertificateValidationDelegate. 41 | public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) 42 | { 43 | if (sslPolicyErrors == SslPolicyErrors.None) 44 | { 45 | return true; 46 | } 47 | 48 | Console.WriteLine("Certificate error: {0}", sslPolicyErrors); 49 | 50 | // Do not allow this client to communicate with unauthenticated servers. 51 | return false; 52 | } 53 | 54 | private Stream GetStream(TcpClient tcpClient, bool isSecure) 55 | { 56 | if (isSecure) 57 | { 58 | SslStream sslStream = new SslStream(tcpClient.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null); 59 | _logger.Information(this.GetType(), "Attempting to secure connection..."); 60 | sslStream.AuthenticateAsClient("clusteredanalytics.com"); 61 | _logger.Information(this.GetType(), "Connection successfully secured."); 62 | return sslStream; 63 | } 64 | else 65 | { 66 | _logger.Information(this.GetType(), "Connection not secure"); 67 | return tcpClient.GetStream(); 68 | } 69 | } 70 | 71 | public virtual void OpenBlocking(Uri uri) 72 | { 73 | if (!_isOpen) 74 | { 75 | string host = uri.Host; 76 | int port = uri.Port; 77 | _tcpClient = new TcpClient(); 78 | _tcpClient.NoDelay = _noDelay; 79 | 80 | IPAddress ipAddress; 81 | if (IPAddress.TryParse(host, out ipAddress)) 82 | { 83 | _tcpClient.Connect(ipAddress, port); 84 | } 85 | else 86 | { 87 | _tcpClient.Connect(host, port); 88 | } 89 | 90 | bool isSecure = port == 443; 91 | _stream = GetStream(_tcpClient, isSecure); 92 | 93 | _uri = uri; 94 | _isOpen = true; 95 | base.OpenBlocking(_stream, _tcpClient.Client); 96 | _isOpen = false; 97 | } 98 | } 99 | 100 | protected override void PerformHandshake(Stream stream) 101 | { 102 | Uri uri = _uri; 103 | WebSocketFrameReader reader = new WebSocketFrameReader(); 104 | Random rand = new Random(); 105 | byte[] keyAsBytes = new byte[16]; 106 | rand.NextBytes(keyAsBytes); 107 | string secWebSocketKey = Convert.ToBase64String(keyAsBytes); 108 | 109 | string handshakeHttpRequestTemplate = @"GET {0} HTTP/1.1{4}" + 110 | "Host: {1}:{2}{4}" + 111 | "Upgrade: websocket{4}" + 112 | "Connection: Upgrade{4}" + 113 | "Sec-WebSocket-Key: {3}{4}" + 114 | "Sec-WebSocket-Version: 13{4}{4}"; 115 | 116 | string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate, uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey, Environment.NewLine); 117 | byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest); 118 | stream.Write(httpRequest, 0, httpRequest.Length); 119 | _logger.Information(this.GetType(), "Handshake sent. Waiting for response."); 120 | 121 | // make sure we escape the accept string which could contain special regex characters 122 | string regexPattern = "Sec-WebSocket-Accept: (.*)"; 123 | Regex regex = new Regex(regexPattern); 124 | 125 | string response = string.Empty; 126 | 127 | try 128 | { 129 | response = HttpHelper.ReadHttpHeader(stream); 130 | } 131 | catch (Exception ex) 132 | { 133 | throw new WebSocketHandshakeFailedException("Handshake unexpected failure", ex); 134 | } 135 | 136 | // check the accept string 137 | string expectedAcceptString = base.ComputeSocketAcceptString(secWebSocketKey); 138 | string actualAcceptString = regex.Match(response).Groups[1].Value.Trim(); 139 | if (expectedAcceptString != actualAcceptString) 140 | { 141 | throw new WebSocketHandshakeFailedException(string.Format("Handshake failed because the accept string from the server '{0}' was not the expected string '{1}'", expectedAcceptString, actualAcceptString)); 142 | } 143 | else 144 | { 145 | _logger.Information(this.GetType(), "Handshake response received. Connection upgraded to WebSocket protocol."); 146 | } 147 | } 148 | 149 | public virtual void Dispose() 150 | { 151 | if (_isOpen) 152 | { 153 | using (MemoryStream stream = new MemoryStream()) 154 | { 155 | // set the close reason to GoingAway 156 | BinaryReaderWriter.WriteUShort((ushort) WebSocketCloseCode.GoingAway, stream, false); 157 | 158 | // send close message to server to begin the close handshake 159 | Send(WebSocketOpCode.ConnectionClose, stream.ToArray()); 160 | _logger.Information(this.GetType(), "Sent websocket close message to server. Reason: GoingAway"); 161 | } 162 | 163 | // this needs to run on a worker thread so that the read loop (in the base class) is not blocked 164 | Task.Factory.StartNew(WaitForServerCloseMessage); 165 | } 166 | } 167 | 168 | private void WaitForServerCloseMessage() 169 | { 170 | // as per the websocket spec, the server must close the connection, not the client. 171 | // The client is free to close the connection after a timeout period if the server fails to do so 172 | _conectionCloseWait.WaitOne(TimeSpan.FromSeconds(10)); 173 | 174 | // this will only happen if the server has failed to reply with a close response 175 | if (_isOpen) 176 | { 177 | _logger.Warning(this.GetType(), "Server failed to respond with a close response. Closing the connection from the client side."); 178 | 179 | // wait for data to be sent before we close the stream and client 180 | _tcpClient.Client.Shutdown(SocketShutdown.Both); 181 | _stream.Close(); 182 | _tcpClient.Close(); 183 | } 184 | 185 | _logger.Information(this.GetType(), "Client: Connection closed"); 186 | } 187 | 188 | protected override void OnConnectionClose(byte[] payload) 189 | { 190 | // server has either responded to a client close request or closed the connection for its own reasons 191 | // the server will close the tcp connection so the client will not have to do it 192 | _isOpen = false; 193 | _conectionCloseWait.Set(); 194 | base.OnConnectionClose(payload); 195 | } 196 | 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /WebSockets/Common/BinaryReaderWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace WebSockets.Common 8 | { 9 | internal class BinaryReaderWriter 10 | { 11 | public static byte[] ReadExactly(int length, Stream stream) 12 | { 13 | byte[] buffer = new byte[length]; 14 | if (length == 0) 15 | { 16 | return buffer; 17 | } 18 | 19 | int offset = 0; 20 | do 21 | { 22 | int bytesRead = stream.Read(buffer, offset, length - offset); 23 | if (bytesRead == 0) 24 | { 25 | throw new EndOfStreamException(string.Format("Unexpected end of stream encountered whilst attempting to read {0:#,##0} bytes", length)); 26 | } 27 | 28 | offset += bytesRead; 29 | } while (offset < length); 30 | 31 | return buffer; 32 | } 33 | 34 | public static ushort ReadUShortExactly(Stream stream, bool isLittleEndian) 35 | { 36 | byte[] lenBuffer = BinaryReaderWriter.ReadExactly(2, stream); 37 | 38 | if (!isLittleEndian) 39 | { 40 | Array.Reverse(lenBuffer); // big endian 41 | } 42 | 43 | return BitConverter.ToUInt16(lenBuffer, 0); 44 | } 45 | 46 | public static ulong ReadULongExactly(Stream stream, bool isLittleEndian) 47 | { 48 | byte[] lenBuffer = BinaryReaderWriter.ReadExactly(8, stream); 49 | 50 | if (!isLittleEndian) 51 | { 52 | Array.Reverse(lenBuffer); // big endian 53 | } 54 | 55 | return BitConverter.ToUInt64(lenBuffer, 0); 56 | } 57 | 58 | public static long ReadLongExactly(Stream stream, bool isLittleEndian) 59 | { 60 | byte[] lenBuffer = BinaryReaderWriter.ReadExactly(8, stream); 61 | 62 | if (!isLittleEndian) 63 | { 64 | Array.Reverse(lenBuffer); // big endian 65 | } 66 | 67 | return BitConverter.ToInt64(lenBuffer, 0); 68 | } 69 | 70 | public static void WriteULong(ulong value, Stream stream, bool isLittleEndian) 71 | { 72 | byte[] buffer = BitConverter.GetBytes(value); 73 | if (BitConverter.IsLittleEndian && ! isLittleEndian) 74 | { 75 | Array.Reverse(buffer); 76 | } 77 | 78 | stream.Write(buffer, 0, buffer.Length); 79 | } 80 | 81 | public static void WriteLong(long value, Stream stream, bool isLittleEndian) 82 | { 83 | byte[] buffer = BitConverter.GetBytes(value); 84 | if (BitConverter.IsLittleEndian && !isLittleEndian) 85 | { 86 | Array.Reverse(buffer); 87 | } 88 | 89 | stream.Write(buffer, 0, buffer.Length); 90 | } 91 | 92 | public static void WriteUShort(ushort value, Stream stream, bool isLittleEndian) 93 | { 94 | byte[] buffer = BitConverter.GetBytes(value); 95 | if (BitConverter.IsLittleEndian && !isLittleEndian) 96 | { 97 | Array.Reverse(buffer); 98 | } 99 | 100 | stream.Write(buffer, 0, buffer.Length); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /WebSockets/Common/IWebSocketLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Common 7 | { 8 | public interface IWebSocketLogger 9 | { 10 | void Information(Type type, string format, params object[] args); 11 | void Warning(Type type, string format, params object[] args); 12 | void Error(Type type, string format, params object[] args); 13 | void Error(Type type, Exception exception); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WebSockets/Common/WebSocketBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Net.Sockets; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.IO; 9 | using WebSockets.Events; 10 | 11 | namespace WebSockets.Common 12 | { 13 | public abstract class WebSocketBase 14 | { 15 | private readonly IWebSocketLogger _logger; 16 | private readonly object _sendLocker; 17 | private Stream _stream; 18 | private WebSocketFrameWriter _writer; 19 | private WebSocketOpCode _multiFrameOpcode; 20 | private Socket _socket; 21 | protected bool _isOpen; 22 | 23 | public event EventHandler ConnectionOpened; 24 | public event EventHandler ConnectionClose; 25 | public event EventHandler Ping; 26 | public event EventHandler Pong; 27 | public event EventHandler TextFrame; 28 | public event EventHandler TextMultiFrame; 29 | public event EventHandler BinaryFrame; 30 | public event EventHandler BinaryMultiFrame; 31 | 32 | public WebSocketBase(IWebSocketLogger logger) 33 | { 34 | _logger = logger; 35 | _sendLocker = new object(); 36 | _isOpen = false; 37 | } 38 | 39 | protected void OpenBlocking(Stream stream, Socket socket) 40 | { 41 | _socket = socket; 42 | _stream = stream; 43 | _writer = new WebSocketFrameWriter(stream); 44 | PerformHandshake(stream); 45 | _isOpen = true; 46 | MainReadLoop(); 47 | } 48 | 49 | protected virtual void Send(WebSocketOpCode opCode, byte[] toSend, bool isLastFrame) 50 | { 51 | if (_isOpen) 52 | { 53 | lock (_sendLocker) 54 | { 55 | if (_isOpen) 56 | { 57 | _writer.Write(opCode, toSend, isLastFrame); 58 | } 59 | } 60 | } 61 | } 62 | 63 | protected virtual void Send(WebSocketOpCode opCode, byte[] toSend) 64 | { 65 | Send(opCode, toSend, true); 66 | } 67 | 68 | protected virtual void Send(byte[] toSend) 69 | { 70 | Send(WebSocketOpCode.BinaryFrame, toSend, true); 71 | } 72 | 73 | protected virtual void Send(string text) 74 | { 75 | byte[] bytes = Encoding.UTF8.GetBytes(text); 76 | Send(WebSocketOpCode.TextFrame, bytes, true); 77 | } 78 | 79 | protected virtual void OnConnectionOpened() 80 | { 81 | if (ConnectionOpened != null) 82 | { 83 | ConnectionOpened(this, new EventArgs()); 84 | } 85 | } 86 | 87 | protected virtual void OnPing(byte[] payload) 88 | { 89 | Send(WebSocketOpCode.Pong, payload); 90 | 91 | if (Ping != null) 92 | { 93 | Ping(this, new PingEventArgs(payload)); 94 | } 95 | } 96 | 97 | protected virtual void OnPong(byte[] payload) 98 | { 99 | if (Pong != null) 100 | { 101 | Pong(this, new PingEventArgs(payload)); 102 | } 103 | } 104 | 105 | protected virtual void OnTextFrame(string text) 106 | { 107 | if (TextFrame != null) 108 | { 109 | TextFrame(this, new TextFrameEventArgs(text)); 110 | } 111 | } 112 | 113 | protected virtual void OnTextMultiFrame(string text, bool isLastFrame) 114 | { 115 | if (TextMultiFrame != null) 116 | { 117 | TextMultiFrame(this, new TextMultiFrameEventArgs(text, isLastFrame)); 118 | } 119 | } 120 | 121 | protected virtual void OnBinaryFrame(byte[] payload) 122 | { 123 | if (BinaryFrame != null) 124 | { 125 | BinaryFrame(this, new BinaryFrameEventArgs(payload)); 126 | } 127 | } 128 | 129 | protected virtual void OnBinaryMultiFrame(byte[] payload, bool isLastFrame) 130 | { 131 | if (BinaryMultiFrame != null) 132 | { 133 | BinaryMultiFrame(this, new BinaryMultiFrameEventArgs(payload, isLastFrame)); 134 | } 135 | } 136 | 137 | protected virtual void OnConnectionClose(byte[] payload) 138 | { 139 | ConnectionCloseEventArgs args = GetConnectionCloseEventArgsFromPayload(payload); 140 | 141 | if (args.Reason == null) 142 | { 143 | _logger.Information(this.GetType(), "Received web socket close message: {0}", args.Code); 144 | } 145 | else 146 | { 147 | _logger.Information(this.GetType(), "Received web socket close message: Code '{0}' Reason '{1}'", args.Code, args.Reason); 148 | } 149 | 150 | if (ConnectionClose != null) 151 | { 152 | ConnectionClose(this, args); 153 | } 154 | } 155 | 156 | protected abstract void PerformHandshake(Stream stream); 157 | 158 | /// 159 | /// Combines the key supplied by the client with a guid and returns the sha1 hash of the combination 160 | /// 161 | protected string ComputeSocketAcceptString(string secWebSocketKey) 162 | { 163 | // this is a guid as per the web socket spec 164 | const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 165 | 166 | string concatenated = secWebSocketKey + webSocketGuid; 167 | byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated); 168 | byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes); 169 | string secWebSocketAccept = Convert.ToBase64String(sha1Hash); 170 | return secWebSocketAccept; 171 | } 172 | 173 | protected ConnectionCloseEventArgs GetConnectionCloseEventArgsFromPayload(byte[] payload) 174 | { 175 | if (payload.Length >= 2) 176 | { 177 | using (MemoryStream stream = new MemoryStream(payload)) 178 | { 179 | ushort code = BinaryReaderWriter.ReadUShortExactly(stream, false); 180 | 181 | try 182 | { 183 | WebSocketCloseCode closeCode = (WebSocketCloseCode)code; 184 | 185 | if (payload.Length > 2) 186 | { 187 | string reason = Encoding.UTF8.GetString(payload, 2, payload.Length - 2); 188 | return new ConnectionCloseEventArgs(closeCode, reason); 189 | } 190 | else 191 | { 192 | return new ConnectionCloseEventArgs(closeCode, null); 193 | } 194 | } 195 | catch (InvalidCastException) 196 | { 197 | _logger.Warning(this.GetType(), "Close code {0} not recognised", code); 198 | return new ConnectionCloseEventArgs(WebSocketCloseCode.Normal, null); 199 | } 200 | } 201 | } 202 | 203 | return new ConnectionCloseEventArgs(WebSocketCloseCode.Normal, null); 204 | } 205 | 206 | private void MainReadLoop() 207 | { 208 | Stream stream = _stream; 209 | OnConnectionOpened(); 210 | WebSocketFrameReader reader = new WebSocketFrameReader(); 211 | List fragmentedFrames = new List(); 212 | 213 | while (true) 214 | { 215 | WebSocketFrame frame; 216 | 217 | try 218 | { 219 | frame = reader.Read(stream, _socket); 220 | if (frame == null) 221 | { 222 | return; 223 | } 224 | } 225 | catch (ObjectDisposedException) 226 | { 227 | return; 228 | } 229 | 230 | // if we have received unexpected data 231 | if (!frame.IsValid) 232 | { 233 | return; 234 | } 235 | 236 | if (frame.OpCode == WebSocketOpCode.ContinuationFrame) 237 | { 238 | switch (_multiFrameOpcode) 239 | { 240 | case WebSocketOpCode.TextFrame: 241 | String data = Encoding.UTF8.GetString(frame.DecodedPayload, 0, frame.DecodedPayload.Length); 242 | OnTextMultiFrame(data, frame.IsFinBitSet); 243 | break; 244 | case WebSocketOpCode.BinaryFrame: 245 | OnBinaryMultiFrame(frame.DecodedPayload, frame.IsFinBitSet); 246 | break; 247 | } 248 | } 249 | else 250 | { 251 | switch (frame.OpCode) 252 | { 253 | case WebSocketOpCode.ConnectionClose: 254 | OnConnectionClose(frame.DecodedPayload); 255 | return; 256 | case WebSocketOpCode.Ping: 257 | OnPing(frame.DecodedPayload); 258 | break; 259 | case WebSocketOpCode.Pong: 260 | OnPong(frame.DecodedPayload); 261 | break; 262 | case WebSocketOpCode.TextFrame: 263 | String data = Encoding.UTF8.GetString(frame.DecodedPayload, 0, frame.DecodedPayload.Length); 264 | if (frame.IsFinBitSet) 265 | { 266 | OnTextFrame(data); 267 | } 268 | else 269 | { 270 | _multiFrameOpcode = frame.OpCode; 271 | OnTextMultiFrame(data, frame.IsFinBitSet); 272 | } 273 | break; 274 | case WebSocketOpCode.BinaryFrame: 275 | if (frame.IsFinBitSet) 276 | { 277 | OnBinaryFrame(frame.DecodedPayload); 278 | } 279 | else 280 | { 281 | _multiFrameOpcode = frame.OpCode; 282 | OnBinaryMultiFrame(frame.DecodedPayload, frame.IsFinBitSet); 283 | } 284 | break; 285 | } 286 | } 287 | } 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /WebSockets/Common/WebSocketCloseCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Common 7 | { 8 | public enum WebSocketCloseCode 9 | { 10 | Normal = 1000, 11 | GoingAway = 1001, 12 | ProtocolError = 1002, 13 | DataTypeNotSupported = 1003, 14 | Reserverd = 1004, 15 | ReserverdNoStatusCode = 1005, 16 | ReserverdAbnormalClosure = 1006, 17 | MismatchDataNonUTF8 = 1007, 18 | ViolationOfPolicy = 1008, 19 | MessageTooLarge = 1009, 20 | EnpointExpectsExtension = 1010, 21 | ServerUnexpectedCondition = 1011, 22 | ServerRegectTlsHandshake = 1015, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WebSockets/Common/WebSocketFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Common 7 | { 8 | public class WebSocketFrame 9 | { 10 | public bool IsFinBitSet { get; private set; } 11 | public WebSocketOpCode OpCode { get; private set; } 12 | public byte[] DecodedPayload { get; private set; } 13 | public bool IsValid { get; private set; } 14 | 15 | public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, byte[] decodedPayload, bool isValid) 16 | { 17 | IsFinBitSet = isFinBitSet; 18 | OpCode = webSocketOpCode; 19 | DecodedPayload = decodedPayload; 20 | IsValid = isValid; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WebSockets/Common/WebSocketFrameReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.IO; 6 | using System.Net.Sockets; 7 | using System.Diagnostics; 8 | 9 | namespace WebSockets.Common 10 | { 11 | // see http://tools.ietf.org/html/rfc6455 for specification 12 | 13 | public class WebSocketFrameReader 14 | { 15 | private byte[] _buffer; 16 | 17 | public WebSocketFrameReader() 18 | { 19 | _buffer = new byte[1024*64]; 20 | } 21 | 22 | public WebSocketFrame Read(Stream stream, Socket socket) 23 | { 24 | byte byte1; 25 | 26 | try 27 | { 28 | byte1 = (byte) stream.ReadByte(); 29 | } 30 | catch (IOException) 31 | { 32 | if (socket.Connected) 33 | { 34 | throw; 35 | } 36 | else 37 | { 38 | return null; 39 | } 40 | } 41 | 42 | // process first byte 43 | byte finBitFlag = 0x80; 44 | byte opCodeFlag = 0x0F; 45 | bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag; 46 | WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag); 47 | 48 | // read and process second byte 49 | byte byte2 = (byte) stream.ReadByte(); 50 | byte maskFlag = 0x80; 51 | bool isMaskBitSet = (byte2 & maskFlag) == maskFlag; 52 | uint len = ReadLength(byte2, stream); 53 | byte[] decodedPayload; 54 | 55 | // use the masking key to decode the data if needed 56 | if (isMaskBitSet) 57 | { 58 | const int maskKeyLen = 4; 59 | byte[] maskKey = BinaryReaderWriter.ReadExactly(maskKeyLen, stream); 60 | byte[] encodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream); 61 | decodedPayload = new byte[len]; 62 | 63 | // apply the mask key 64 | for (int i = 0; i < encodedPayload.Length; i++) 65 | { 66 | decodedPayload[i] = (Byte) (encodedPayload[i] ^ maskKey[i%maskKeyLen]); 67 | } 68 | } 69 | else 70 | { 71 | decodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream); 72 | } 73 | 74 | WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, decodedPayload, true); 75 | return frame; 76 | } 77 | 78 | private static uint ReadLength(byte byte2, Stream stream) 79 | { 80 | byte payloadLenFlag = 0x7F; 81 | uint len = (uint) (byte2 & payloadLenFlag); 82 | 83 | // read a short length or a long length depending on the value of len 84 | if (len == 126) 85 | { 86 | len = BinaryReaderWriter.ReadUShortExactly(stream, false); 87 | } 88 | else if (len == 127) 89 | { 90 | len = (uint) BinaryReaderWriter.ReadULongExactly(stream, false); 91 | const uint maxLen = 2147483648; // 2GB 92 | 93 | // protect ourselves against bad data 94 | if (len > maxLen || len < 0) 95 | { 96 | throw new ArgumentOutOfRangeException(string.Format("Payload length out of range. Min 0 max 2GB. Actual {0:#,##0} bytes.", len)); 97 | } 98 | } 99 | 100 | return len; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /WebSockets/Common/WebSocketFrameWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.IO; 6 | using System.Net.Sockets; 7 | 8 | namespace WebSockets.Common 9 | { 10 | // see http://tools.ietf.org/html/rfc6455 for specification 11 | // see fragmentation section for sending multi part messages 12 | // EXAMPLE: For a text message sent as three fragments, 13 | // the first fragment would have an opcode of TextFrame and isLastFrame false, 14 | // the second fragment would have an opcode of ContinuationFrame and isLastFrame false, 15 | // the third fragment would have an opcode of ContinuationFrame and isLastFrame true. 16 | 17 | public class WebSocketFrameWriter 18 | { 19 | private readonly Stream _stream; 20 | 21 | public WebSocketFrameWriter(Stream stream) 22 | { 23 | _stream = stream; 24 | } 25 | 26 | public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame) 27 | { 28 | // best to write everything to a memory stream before we push it onto the wire 29 | // not really necessary but I like it this way 30 | using (MemoryStream memoryStream = new MemoryStream()) 31 | { 32 | byte finBitSetAsByte = isLastFrame ? (byte) 0x80 : (byte) 0x00; 33 | byte byte1 = (byte) (finBitSetAsByte | (byte) opCode); 34 | memoryStream.WriteByte(byte1); 35 | 36 | // NB, dont set the mask flag. No need to mask data from server to client 37 | // depending on the size of the length we want to write it as a byte, ushort or ulong 38 | if (payload.Length < 126) 39 | { 40 | byte byte2 = (byte) payload.Length; 41 | memoryStream.WriteByte(byte2); 42 | } 43 | else if (payload.Length <= ushort.MaxValue) 44 | { 45 | byte byte2 = 126; 46 | memoryStream.WriteByte(byte2); 47 | BinaryReaderWriter.WriteUShort((ushort) payload.Length, memoryStream, false); 48 | } 49 | else 50 | { 51 | byte byte2 = 127; 52 | memoryStream.WriteByte(byte2); 53 | BinaryReaderWriter.WriteULong((ulong) payload.Length, memoryStream, false); 54 | } 55 | 56 | memoryStream.Write(payload, 0, payload.Length); 57 | byte[] buffer = memoryStream.ToArray(); 58 | _stream.Write(buffer, 0, buffer.Length); 59 | } 60 | } 61 | 62 | public void Write(WebSocketOpCode opCode, byte[] payload) 63 | { 64 | Write(opCode, payload, true); 65 | } 66 | 67 | public void WriteText(string text) 68 | { 69 | byte[] responseBytes = Encoding.UTF8.GetBytes(text); 70 | Write(WebSocketOpCode.TextFrame, responseBytes); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /WebSockets/Common/WebSocketOpCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Common 7 | { 8 | public enum WebSocketOpCode 9 | { 10 | ContinuationFrame = 0, 11 | TextFrame = 1, 12 | BinaryFrame = 2, 13 | ConnectionClose = 8, 14 | Ping = 9, 15 | Pong = 10 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebSockets/Events/BinaryFrameEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Events 7 | { 8 | public class BinaryFrameEventArgs : EventArgs 9 | { 10 | public byte[] Payload { get; private set; } 11 | 12 | public BinaryFrameEventArgs(byte[] payload) 13 | { 14 | Payload = payload; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebSockets/Events/BinaryMultiFrameEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Events 7 | { 8 | public class BinaryMultiFrameEventArgs : BinaryFrameEventArgs 9 | { 10 | public bool IsLastFrame { get; private set; } 11 | 12 | public BinaryMultiFrameEventArgs(byte[] payload, bool isLastFrame) : base(payload) 13 | { 14 | IsLastFrame = isLastFrame; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebSockets/Events/ConnectionCloseEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using WebSockets.Common; 6 | 7 | namespace WebSockets.Events 8 | { 9 | public class ConnectionCloseEventArgs : EventArgs 10 | { 11 | public WebSocketCloseCode Code { get; private set; } 12 | public string Reason { get; private set; } 13 | 14 | public ConnectionCloseEventArgs(WebSocketCloseCode code, string reason) 15 | { 16 | Code = code; 17 | Reason = reason; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebSockets/Events/PingEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Events 7 | { 8 | public class PingEventArgs : EventArgs 9 | { 10 | public byte[] Payload { get; private set; } 11 | 12 | public PingEventArgs(byte[] payload) 13 | { 14 | Payload = payload; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebSockets/Events/PongEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Events 7 | { 8 | public class PongEventArgs : EventArgs 9 | { 10 | public byte[] Payload { get; private set; } 11 | 12 | public PongEventArgs(byte[] payload) 13 | { 14 | Payload = payload; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebSockets/Events/TextFrameEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Events 7 | { 8 | public class TextFrameEventArgs : EventArgs 9 | { 10 | public string Text { get; private set; } 11 | 12 | public TextFrameEventArgs(string text) 13 | { 14 | Text = text; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebSockets/Events/TextMultiFrameEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Events 7 | { 8 | public class TextMultiFrameEventArgs : TextFrameEventArgs 9 | { 10 | public bool IsLastFrame { get; private set; } 11 | 12 | public TextMultiFrameEventArgs(string text, bool isLastFrame) : base(text) 13 | { 14 | IsLastFrame = isLastFrame; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebSockets/Exceptions/EntityTooLargeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Text; 6 | 7 | namespace WebSockets.Exceptions 8 | { 9 | [Serializable] 10 | public class EntityTooLargeException : Exception 11 | { 12 | public EntityTooLargeException() : base() 13 | { 14 | 15 | } 16 | 17 | /// 18 | /// Http header too large to fit in buffer 19 | /// 20 | public EntityTooLargeException(string message) : base(message) 21 | { 22 | 23 | } 24 | 25 | public EntityTooLargeException(string message, Exception inner) : base(message, inner) 26 | { 27 | 28 | } 29 | 30 | public EntityTooLargeException(SerializationInfo info, StreamingContext context) : base(info, context) 31 | { 32 | 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /WebSockets/Exceptions/ServerListenerSocketException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Text; 6 | using System.Net.Sockets; 7 | 8 | namespace WebSockets.Exceptions 9 | { 10 | [Serializable] 11 | public class ServerListenerSocketException : Exception 12 | { 13 | public ServerListenerSocketException() : base() 14 | { 15 | 16 | } 17 | 18 | public ServerListenerSocketException(string message) : base(message) 19 | { 20 | 21 | } 22 | 23 | public ServerListenerSocketException(string message, Exception inner) : base(message, inner) 24 | { 25 | 26 | } 27 | 28 | public ServerListenerSocketException(SerializationInfo info, StreamingContext context) : base(info, context) 29 | { 30 | 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /WebSockets/Exceptions/TODO.txt: -------------------------------------------------------------------------------- 1 | Make sure that exceptions follow the microsoft standards -------------------------------------------------------------------------------- /WebSockets/Exceptions/WebSocketHandshakeFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Text; 6 | 7 | namespace WebSockets.Exceptions 8 | { 9 | [Serializable] 10 | public class WebSocketHandshakeFailedException : Exception 11 | { 12 | public WebSocketHandshakeFailedException() : base() 13 | { 14 | 15 | } 16 | 17 | public WebSocketHandshakeFailedException(string message) : base(message) 18 | { 19 | 20 | } 21 | 22 | public WebSocketHandshakeFailedException(string message, Exception inner) : base(message, inner) 23 | { 24 | 25 | } 26 | 27 | public WebSocketHandshakeFailedException(SerializationInfo info, StreamingContext context) : base(info, context) 28 | { 29 | 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Runtime.Serialization; 6 | 7 | namespace WebSockets.Exceptions 8 | { 9 | [Serializable] 10 | public class WebSocketVersionNotSupportedException : Exception 11 | { 12 | public WebSocketVersionNotSupportedException() : base() 13 | { 14 | 15 | } 16 | 17 | public WebSocketVersionNotSupportedException(string message) : base(message) 18 | { 19 | 20 | } 21 | 22 | public WebSocketVersionNotSupportedException(string message, Exception inner) : base(message, inner) 23 | { 24 | 25 | } 26 | 27 | public WebSocketVersionNotSupportedException(SerializationInfo info, StreamingContext context) : base(info, context) 28 | { 29 | 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /WebSockets/MimeTypes.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /WebSockets/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WebSockets")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebSockets")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("f1b075ca-df2d-46ea-84ba-8592ae2e570c")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /WebSockets/README.txt: -------------------------------------------------------------------------------- 1 | Thanks to help from the following websites: 2 | 3 | Step by step guide 4 | https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers 5 | 6 | The official Web Socket spec 7 | http://tools.ietf.org/html/rfc6455 8 | 9 | Some useful stuff in c# 10 | https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server 11 | 12 | Web Socket Protocol 13 (and above) supported 13 | 14 | To run: 15 | Run the console application 16 | NOTE You will get a firewall warning because you are listening on a port. This is normal for any socket based server. 17 | The console application will run a self test so you can see if anything is wrong. The self test will open a connection, perform the open handshake, send a message, receive a response and start the close handshake and disconnect. 18 | 19 | If the self test is a success then the following should work too: 20 | Open a browser and enter: http://localhost/client.html 21 | The web server will then serve up the web page requested (client.html), 22 | The javascript in that webpage will execute and attempt to make a WebSocket connection to that same server and port (80) 23 | At this point the webserver will upgrade the connection to a web socket connection and you can chat with the server 24 | If you want to access this from another machine then make sure your firewall is not blocking the port 25 | 26 | Note: 27 | A lot of the Web Socket examples out there are for old Web Socket versions and included complicated code for fall back communication. 28 | All modern browsers (including safari on an iphone) support at least version 13 of the Web Socket protocol so I'd rather not complicate things. 29 | This application serves up basic html pages as well as handling WebSocket connections. 30 | This may seem confusing but it allows you to send the client the html they need to make a web socket connection and also allows you to share the same port 31 | However, the HttpConnection is very rudimentary. I'm sure it has some glaring security problems. It was just made to make this demo easier to run. Replace it with your own or dont use it. 32 | 33 | Debugging: 34 | A good place to put a breakpoint is in the WebServer class in the HandleAsyncConnection function. 35 | Note that this is a multithreaded server to you may want to freeze threads if this gets confusing. The console output prints the thread id to make things easier 36 | If you want to skip past all the plumbing then another good place to start is the Respond function in the WebSocketService class 37 | If you are not interested in the inner workings of Web Sockets and just want to use them then take a look at the OnTextFrame in the ChatWebSocketService class 38 | 39 | Problems with Proxy Servers: 40 | Proxy servers which have not been configured to support Web sockets will not work well with them. 41 | I suggest that you use transport layer security (SSL) if you want this to work across the wider internet especially from within a corporation 42 | 43 | Sub Folders: 44 | You can assign different web socket handlers depending on the folder attribute of the first line of the http request 45 | eg: 46 | GET /chat HTTP/1.1 47 | This folder has been setup to use the ChatWebSocketService class 48 | 49 | Change Log: 50 | 51 | 10 Apr 2016: SSL support 52 | 02 Apr 2016: c# Client 53 | -------------------------------------------------------------------------------- /WebSockets/Server/ConnectionDetails.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Net.Sockets; 6 | using System.IO; 7 | 8 | namespace WebSockets.Server 9 | { 10 | public class ConnectionDetails 11 | { 12 | public Stream Stream { get; private set; } 13 | public TcpClient TcpClient { get; private set; } 14 | public ConnectionType ConnectionType { get; private set; } 15 | public string Header { get; private set; } 16 | 17 | // this is the path attribute in the first line of the http header 18 | public string Path { get; private set; } 19 | 20 | public ConnectionDetails (Stream stream, TcpClient tcpClient, string path, ConnectionType connectionType, string header) 21 | { 22 | Stream = stream; 23 | TcpClient = tcpClient; 24 | Path = path; 25 | ConnectionType = connectionType; 26 | Header = header; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /WebSockets/Server/ConnectionType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Server 7 | { 8 | public enum ConnectionType 9 | { 10 | Http, 11 | WebSocket, 12 | Unknown 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WebSockets/Server/Http/BadRequestService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Net.Sockets; 7 | using WebSockets.Common; 8 | using System.IO; 9 | 10 | namespace WebSockets.Server.Http 11 | { 12 | public class BadRequestService : IService 13 | { 14 | private readonly Stream _stream; 15 | private readonly string _header; 16 | private readonly IWebSocketLogger _logger; 17 | 18 | public BadRequestService(Stream stream, string header, IWebSocketLogger logger) 19 | { 20 | _stream = stream; 21 | _header = header; 22 | _logger = logger; 23 | } 24 | 25 | public void Respond() 26 | { 27 | HttpHelper.WriteHttpHeader("HTTP/1.1 400 Bad Request", _stream); 28 | 29 | // limit what we log. Headers can be up to 16K in size 30 | string header = _header.Length > 255 ? _header.Substring(0,255) + "..." : _header; 31 | _logger.Warning(this.GetType(), "Bad request: '{0}'", header); 32 | } 33 | 34 | public void Dispose() 35 | { 36 | // do nothing 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WebSockets/Server/Http/HttpHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.IO; 6 | using System.Net.Sockets; 7 | using System.Text.RegularExpressions; 8 | using System.Diagnostics; 9 | using System.Threading; 10 | using WebSockets.Exceptions; 11 | 12 | namespace WebSockets.Server.Http 13 | { 14 | public class HttpHelper 15 | { 16 | public static string ReadHttpHeader(Stream stream) 17 | { 18 | int length = 1024*16; // 16KB buffer more than enough for http header 19 | byte[] buffer = new byte[length]; 20 | int offset = 0; 21 | int bytesRead = 0; 22 | do 23 | { 24 | if (offset >= length) 25 | { 26 | throw new EntityTooLargeException("Http header message too large to fit in buffer (16KB)"); 27 | } 28 | 29 | bytesRead = stream.Read(buffer, offset, length - offset); 30 | offset += bytesRead; 31 | string header = Encoding.UTF8.GetString(buffer, 0, offset); 32 | 33 | // as per http specification, all headers should end this this 34 | if (header.Contains("\r\n\r\n")) 35 | { 36 | return header; 37 | } 38 | 39 | } while (bytesRead > 0); 40 | 41 | return string.Empty; 42 | } 43 | 44 | public static void WriteHttpHeader(string response, Stream stream) 45 | { 46 | response = response.Trim() + Environment.NewLine + Environment.NewLine; 47 | Byte[] bytes = Encoding.UTF8.GetBytes(response); 48 | stream.Write(bytes, 0, bytes.Length); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /WebSockets/Server/Http/HttpService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Net.Sockets; 6 | using System.Text; 7 | using System.IO; 8 | using System.Reflection; 9 | using System.Configuration; 10 | using System.Xml; 11 | using WebSockets.Common; 12 | 13 | namespace WebSockets.Server.Http 14 | { 15 | public class HttpService : IService 16 | { 17 | private readonly Stream _stream; 18 | private readonly string _path; 19 | private readonly string _webRoot; 20 | private readonly IWebSocketLogger _logger; 21 | private readonly MimeTypes _mimeTypes; 22 | 23 | public HttpService(Stream stream, string path, string webRoot, IWebSocketLogger logger) 24 | { 25 | _stream = stream; 26 | _path = path; 27 | _webRoot = webRoot; 28 | _logger = logger; 29 | _mimeTypes = MimeTypesFactory.GetMimeTypes(webRoot); 30 | } 31 | 32 | private static bool IsDirectory(string file) 33 | { 34 | if (Directory.Exists(file)) 35 | { 36 | //detect whether its a directory or file 37 | FileAttributes attr = File.GetAttributes(file); 38 | return ((attr & FileAttributes.Directory) == FileAttributes.Directory); 39 | } 40 | 41 | return false; 42 | } 43 | 44 | public void Respond() 45 | { 46 | _logger.Information(this.GetType(), "Request: {0}", _path); 47 | string file = GetSafePath(_path); 48 | 49 | // default to index.html is path is supplied 50 | if (IsDirectory(file)) 51 | { 52 | file += "index.html"; 53 | } 54 | 55 | FileInfo fi = new FileInfo(file); 56 | 57 | if (fi.Exists) 58 | { 59 | string ext = fi.Extension.ToLower(); 60 | 61 | string contentType; 62 | if (_mimeTypes.TryGetValue(ext, out contentType)) 63 | { 64 | Byte[] bytes = File.ReadAllBytes(fi.FullName); 65 | RespondSuccess(contentType, bytes.Length); 66 | _stream.Write(bytes, 0, bytes.Length); 67 | _logger.Information(this.GetType(), "Served file: {0}", file); 68 | } 69 | else 70 | { 71 | RespondMimeTypeFailure(file); 72 | } 73 | } 74 | else 75 | { 76 | RespondNotFoundFailure(file); 77 | } 78 | } 79 | 80 | /// 81 | /// I am not convinced that this function is indeed safe from hacking file path tricks 82 | /// 83 | /// The relative path 84 | /// The file system path 85 | private string GetSafePath(string path) 86 | { 87 | path = path.Trim().Replace("/", "\\"); 88 | if (path.Contains("..") || !path.StartsWith("\\") || path.Contains(":")) 89 | { 90 | return string.Empty; 91 | } 92 | 93 | string file = _webRoot + path; 94 | return file; 95 | } 96 | 97 | public void RespondMimeTypeFailure(string file) 98 | { 99 | HttpHelper.WriteHttpHeader("415 Unsupported Media Type", _stream); 100 | _logger.Warning(this.GetType(), "File extension not found MimeTypes.config: {0}", file); 101 | } 102 | 103 | public void RespondNotFoundFailure(string file) 104 | { 105 | HttpHelper.WriteHttpHeader("HTTP/1.1 404 Not Found", _stream); 106 | _logger.Information(this.GetType(), "File not found: {0}", file); 107 | } 108 | 109 | public void RespondSuccess(string contentType, int contentLength) 110 | { 111 | string response = "HTTP/1.1 200 OK" + Environment.NewLine + 112 | "Content-Type: " + contentType + Environment.NewLine + 113 | "Content-Length: " + contentLength + Environment.NewLine + 114 | "Connection: close"; 115 | HttpHelper.WriteHttpHeader(response, _stream); 116 | } 117 | 118 | public void Dispose() 119 | { 120 | // do nothing. The network stream will be closed by the WebServer 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /WebSockets/Server/Http/MimeTypes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Linq; 5 | using System.Text; 6 | using System.IO; 7 | using System.Reflection; 8 | using System.Xml; 9 | 10 | namespace WebSockets.Server.Http 11 | { 12 | class MimeTypes : Dictionary 13 | { 14 | public MimeTypes(string webRoot) 15 | { 16 | string configFileName = webRoot + @"\MimeTypes.config"; 17 | if (!File.Exists(configFileName)) 18 | { 19 | throw new FileNotFoundException("Mime Types config file not found: " + configFileName); 20 | } 21 | 22 | try 23 | { 24 | XmlDocument document = new XmlDocument(); 25 | document.Load(configFileName); 26 | foreach (XmlNode node in document.SelectNodes("configuration/system.webServer/staticContent/mimeMap")) 27 | { 28 | string fileExtension = node.Attributes["fileExtension"].Value; 29 | string mimeType = node.Attributes["mimeType"].Value; 30 | this.Add(fileExtension, mimeType); 31 | } 32 | } 33 | catch (Exception ex) 34 | { 35 | throw new ConfigurationErrorsException("Invalid Mime Types configuration file: " + configFileName, ex); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WebSockets/Server/Http/MimeTypesFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Server.Http 7 | { 8 | internal class MimeTypesFactory 9 | { 10 | private static Dictionary _mimeTypes = new Dictionary(); 11 | 12 | public static MimeTypes GetMimeTypes(string webRoot) 13 | { 14 | lock (_mimeTypes) 15 | { 16 | MimeTypes mimeTypes; 17 | if (!_mimeTypes.TryGetValue(webRoot, out mimeTypes)) 18 | { 19 | mimeTypes = new MimeTypes(webRoot); 20 | _mimeTypes.Add(webRoot, mimeTypes); 21 | } 22 | 23 | return mimeTypes; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /WebSockets/Server/IService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Server 7 | { 8 | public interface IService : IDisposable 9 | { 10 | /// 11 | /// Sends data back to the client. This is built using the IConnectionFactory 12 | /// 13 | void Respond(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WebSockets/Server/IServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WebSockets.Server 7 | { 8 | /// 9 | /// Implement this to decide what connection to use based on the http header 10 | /// 11 | public interface IServiceFactory 12 | { 13 | IService CreateInstance(ConnectionDetails connectionDetails); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WebSockets/Server/WebServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Text.RegularExpressions; 10 | using System.Security.Cryptography; 11 | using System.Threading; 12 | using System.Collections; 13 | using System.Diagnostics; 14 | using WebSockets.Exceptions; 15 | using WebSockets.Server; 16 | using WebSockets.Server.Http; 17 | using WebSockets.Common; 18 | using System.Net.Security; 19 | using System.Security.Authentication; 20 | using System.Security.Cryptography.X509Certificates; 21 | 22 | namespace WebSockets 23 | { 24 | public class WebServer : IDisposable 25 | { 26 | // maintain a list of open connections so that we can notify the client if the server shuts down 27 | private readonly List _openConnections; 28 | private readonly IServiceFactory _serviceFactory; 29 | private readonly IWebSocketLogger _logger; 30 | private X509Certificate2 _sslCertificate; 31 | private TcpListener _listener; 32 | private bool _isDisposed = false; 33 | 34 | public WebServer(IServiceFactory serviceFactory, IWebSocketLogger logger) 35 | { 36 | _serviceFactory = serviceFactory; 37 | _logger = logger; 38 | _openConnections = new List(); 39 | } 40 | 41 | public void Listen(int port, X509Certificate2 sslCertificate) 42 | { 43 | try 44 | { 45 | _sslCertificate = sslCertificate; 46 | IPAddress localAddress = IPAddress.Any; 47 | _listener = new TcpListener(localAddress, port); 48 | _listener.Start(); 49 | _logger.Information(this.GetType(), "Server started listening on port {0}", port); 50 | StartAccept(); 51 | } 52 | catch (SocketException ex) 53 | { 54 | string message = string.Format("Error listening on port {0}. Make sure IIS or another application is not running and consuming your port.", port); 55 | throw new ServerListenerSocketException(message, ex); 56 | } 57 | } 58 | 59 | /// 60 | /// Listens on the port specified 61 | /// 62 | public void Listen(int port) 63 | { 64 | Listen(port, null); 65 | } 66 | 67 | /// 68 | /// Gets the first available port and listens on it. Returns the port 69 | /// 70 | public int Listen() 71 | { 72 | IPAddress localAddress = IPAddress.Any; 73 | _listener = new TcpListener(localAddress, 0); 74 | _listener.Start(); 75 | StartAccept(); 76 | int port = ((IPEndPoint) _listener.LocalEndpoint).Port; 77 | _logger.Information(this.GetType(), "Server started listening on port {0}", port); 78 | return port; 79 | } 80 | 81 | private void StartAccept() 82 | { 83 | // this is a non-blocking operation. It will consume a worker thread from the threadpool 84 | _listener.BeginAcceptTcpClient(new AsyncCallback(HandleAsyncConnection), null); 85 | } 86 | 87 | private static ConnectionDetails GetConnectionDetails(Stream stream, TcpClient tcpClient) 88 | { 89 | // read the header and check that it is a GET request 90 | string header = HttpHelper.ReadHttpHeader(stream); 91 | Regex getRegex = new Regex(@"^GET(.*)HTTP\/1\.1", RegexOptions.IgnoreCase); 92 | 93 | Match getRegexMatch = getRegex.Match(header); 94 | if (getRegexMatch.Success) 95 | { 96 | // extract the path attribute from the first line of the header 97 | string path = getRegexMatch.Groups[1].Value.Trim(); 98 | 99 | // check if this is a web socket upgrade request 100 | Regex webSocketUpgradeRegex = new Regex("Upgrade: websocket", RegexOptions.IgnoreCase); 101 | Match webSocketUpgradeRegexMatch = webSocketUpgradeRegex.Match(header); 102 | 103 | if (webSocketUpgradeRegexMatch.Success) 104 | { 105 | return new ConnectionDetails(stream, tcpClient, path, ConnectionType.WebSocket, header); 106 | } 107 | else 108 | { 109 | return new ConnectionDetails(stream, tcpClient, path, ConnectionType.Http, header); 110 | } 111 | } 112 | else 113 | { 114 | return new ConnectionDetails(stream, tcpClient, string.Empty, ConnectionType.Unknown, header); 115 | } 116 | } 117 | 118 | private Stream GetStream(TcpClient tcpClient) 119 | { 120 | Stream stream = tcpClient.GetStream(); 121 | 122 | // we have no ssl certificate 123 | if (_sslCertificate == null) 124 | { 125 | _logger.Information(this.GetType(), "Connection not secure"); 126 | return stream; 127 | } 128 | 129 | try 130 | { 131 | SslStream sslStream = new SslStream(stream, false); 132 | _logger.Information(this.GetType(), "Attempting to secure connection..."); 133 | sslStream.AuthenticateAsServer(_sslCertificate, false, SslProtocols.Tls, true); 134 | _logger.Information(this.GetType(), "Connection successfully secured"); 135 | return sslStream; 136 | } 137 | catch (AuthenticationException e) 138 | { 139 | // TODO: send 401 Unauthorized 140 | throw; 141 | } 142 | } 143 | 144 | private void HandleAsyncConnection(IAsyncResult res) 145 | { 146 | try 147 | { 148 | if (_isDisposed) 149 | { 150 | return; 151 | } 152 | 153 | // this worker thread stays alive until either of the following happens: 154 | // Client sends a close conection request OR 155 | // An unhandled exception is thrown OR 156 | // The server is disposed 157 | using (TcpClient tcpClient = _listener.EndAcceptTcpClient(res)) 158 | { 159 | // we are ready to listen for more connections (on another thread) 160 | StartAccept(); 161 | _logger.Information(this.GetType(), "Server: Connection opened"); 162 | 163 | // get a secure or insecure stream 164 | Stream stream = GetStream(tcpClient); 165 | 166 | // extract the connection details and use those details to build a connection 167 | ConnectionDetails connectionDetails = GetConnectionDetails(stream, tcpClient); 168 | using (IService service = _serviceFactory.CreateInstance(connectionDetails)) 169 | { 170 | try 171 | { 172 | // record the connection so we can close it if something goes wrong 173 | lock (_openConnections) 174 | { 175 | _openConnections.Add(service); 176 | } 177 | 178 | // respond to the http request. 179 | // Take a look at the WebSocketConnection or HttpConnection classes 180 | service.Respond(); 181 | } 182 | finally 183 | { 184 | // forget the connection, we are done with it 185 | lock (_openConnections) 186 | { 187 | _openConnections.Remove(service); 188 | } 189 | } 190 | } 191 | } 192 | 193 | _logger.Information(this.GetType(), "Server: Connection closed"); 194 | } 195 | catch (ObjectDisposedException) 196 | { 197 | // do nothing. This will be thrown if the Listener has been stopped 198 | } 199 | catch (Exception ex) 200 | { 201 | _logger.Error(this.GetType(), ex); 202 | } 203 | } 204 | 205 | private void CloseAllConnections() 206 | { 207 | IDisposable[] openConnections; 208 | 209 | lock (_openConnections) 210 | { 211 | openConnections = _openConnections.ToArray(); 212 | _openConnections.Clear(); 213 | } 214 | 215 | // safely attempt to close each connection 216 | foreach (IDisposable openConnection in openConnections) 217 | { 218 | try 219 | { 220 | openConnection.Dispose(); 221 | } 222 | catch (Exception ex) 223 | { 224 | _logger.Error(this.GetType(), ex); 225 | } 226 | } 227 | } 228 | 229 | public void Dispose() 230 | { 231 | if (!_isDisposed) 232 | { 233 | _isDisposed = true; 234 | 235 | // safely attempt to shut down the listener 236 | try 237 | { 238 | if (_listener != null) 239 | { 240 | if (_listener.Server != null) 241 | { 242 | _listener.Server.Close(); 243 | } 244 | 245 | _listener.Stop(); 246 | } 247 | } 248 | catch (Exception ex) 249 | { 250 | _logger.Error(this.GetType(), ex); 251 | } 252 | 253 | CloseAllConnections(); 254 | _logger.Information(this.GetType(), "Web Server disposed"); 255 | } 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /WebSockets/Server/WebSocket/WebSocketService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using System.Net.Sockets; 7 | using System.Text.RegularExpressions; 8 | using System.Diagnostics; 9 | using System.Threading; 10 | using WebSockets.Common; 11 | using WebSockets.Exceptions; 12 | using WebSockets.Server.Http; 13 | using System.IO; 14 | using WebSockets.Events; 15 | 16 | namespace WebSockets.Server.WebSocket 17 | { 18 | public class WebSocketService : WebSocketBase, IService 19 | { 20 | private readonly Stream _stream; 21 | private readonly string _header; 22 | private readonly IWebSocketLogger _logger; 23 | private readonly TcpClient _tcpClient; 24 | private bool _isDisposed = false; 25 | 26 | public WebSocketService(Stream stream, TcpClient tcpClient, string header, bool noDelay, IWebSocketLogger logger) 27 | : base(logger) 28 | { 29 | _stream = stream; 30 | _header = header; 31 | _logger = logger; 32 | _tcpClient = tcpClient; 33 | 34 | // send requests immediately if true (needed for small low latency packets but not a long stream). 35 | // Basically, dont wait for the buffer to be full before before sending the packet 36 | tcpClient.NoDelay = noDelay; 37 | } 38 | 39 | public void Respond() 40 | { 41 | base.OpenBlocking(_stream, _tcpClient.Client); 42 | } 43 | 44 | protected override void PerformHandshake(Stream stream) 45 | { 46 | string header = _header; 47 | 48 | try 49 | { 50 | Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)"); 51 | Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)"); 52 | 53 | // check the version. Support version 13 and above 54 | const int WebSocketVersion = 13; 55 | int secWebSocketVersion = Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim()); 56 | if (secWebSocketVersion < WebSocketVersion) 57 | { 58 | throw new WebSocketVersionNotSupportedException(string.Format("WebSocket Version {0} not suported. Must be {1} or above", secWebSocketVersion, WebSocketVersion)); 59 | } 60 | 61 | string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim(); 62 | string setWebSocketAccept = base.ComputeSocketAcceptString(secWebSocketKey); 63 | string response = ("HTTP/1.1 101 Switching Protocols" + Environment.NewLine 64 | + "Connection: Upgrade" + Environment.NewLine 65 | + "Upgrade: websocket" + Environment.NewLine 66 | + "Sec-WebSocket-Accept: " + setWebSocketAccept); 67 | 68 | HttpHelper.WriteHttpHeader(response, stream); 69 | _logger.Information(this.GetType(), "Web Socket handshake sent"); 70 | } 71 | catch (WebSocketVersionNotSupportedException ex) 72 | { 73 | string response = "HTTP/1.1 426 Upgrade Required" + Environment.NewLine + "Sec-WebSocket-Version: 13"; 74 | HttpHelper.WriteHttpHeader(response, stream); 75 | throw; 76 | } 77 | catch (Exception ex) 78 | { 79 | HttpHelper.WriteHttpHeader("HTTP/1.1 400 Bad Request", stream); 80 | throw; 81 | } 82 | } 83 | 84 | private static void CloseConnection(Socket socket) 85 | { 86 | socket.Shutdown(SocketShutdown.Both); 87 | socket.Close(); 88 | } 89 | 90 | public virtual void Dispose() 91 | { 92 | // send special web socket close message. Don't close the network stream, it will be disposed later 93 | if (_stream.CanWrite && !_isDisposed) 94 | { 95 | using (MemoryStream stream = new MemoryStream()) 96 | { 97 | // set the close reason to Normal 98 | BinaryReaderWriter.WriteUShort((ushort) WebSocketCloseCode.Normal, stream, false); 99 | 100 | // send close message to client to begin the close handshake 101 | Send(WebSocketOpCode.ConnectionClose, stream.ToArray()); 102 | } 103 | 104 | _isDisposed = true; 105 | _logger.Information(this.GetType(), "Sent web socket close message to client"); 106 | CloseConnection(_tcpClient.Client); 107 | } 108 | } 109 | 110 | protected override void OnConnectionClose(byte[] payload) 111 | { 112 | Send(WebSocketOpCode.ConnectionClose, payload); 113 | _logger.Information(this.GetType(), "Sent response close message to client"); 114 | _isDisposed = true; 115 | 116 | base.OnConnectionClose(payload); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /WebSockets/WebSockets.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C} 9 | Library 10 | Properties 11 | WebSockets 12 | WebSockets 13 | v4.0 14 | 512 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | PreserveNewest 82 | 83 | 84 | 85 | 92 | -------------------------------------------------------------------------------- /WebSocketsCmd/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | C:\Projects\WebGrid\WebGrid1 22 | 23 | 24 | 80 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /WebSocketsCmd/Client/ChatWebSocketClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using WebSockets.Client; 6 | using WebSockets.Common; 7 | 8 | namespace WebSocketsCmd.Client 9 | { 10 | class ChatWebSocketClient : WebSocketClient 11 | { 12 | public ChatWebSocketClient(bool noDelay, IWebSocketLogger logger) : base(noDelay, logger) 13 | { 14 | 15 | } 16 | 17 | public void Send(string text) 18 | { 19 | byte[] buffer = Encoding.UTF8.GetBytes(text); 20 | Send(WebSocketOpCode.TextFrame, buffer); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WebSocketsCmd/CustomConsoleTraceListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Diagnostics; 6 | using System.Threading; 7 | 8 | namespace WebSocketsCmd 9 | { 10 | public class CustomConsoleTraceListener : TraceListener 11 | { 12 | public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args) 13 | { 14 | string message = string.Format(format, args); 15 | 16 | // write the localised date and time but include the time zone in brackets (good for combining logs from different timezones) 17 | TimeSpan utcOffset = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now); 18 | string plusOrMinus = (utcOffset < TimeSpan.Zero) ? "-" : "+"; 19 | string utcHourOffset = utcOffset.TotalHours == 0 ? string.Empty : string.Format(" ({0}{1:hh})", plusOrMinus, utcOffset); 20 | string dateWithOffset = string.Format(@"{0:yyyy/MM/dd HH:mm:ss.fff}{1}", DateTime.Now, utcHourOffset); 21 | 22 | // display the threadid 23 | string log = string.Format(@"{0} [{1}] {2}", dateWithOffset, Thread.CurrentThread.ManagedThreadId, message); 24 | 25 | switch (eventType) 26 | { 27 | case TraceEventType.Critical: 28 | case TraceEventType.Error: 29 | Console.ForegroundColor = ConsoleColor.Red; 30 | Console.WriteLine(log); 31 | Console.ResetColor(); 32 | break; 33 | 34 | case TraceEventType.Warning: 35 | Console.ForegroundColor = ConsoleColor.Yellow; 36 | Console.WriteLine(log); 37 | Console.ResetColor(); 38 | break; 39 | 40 | default: 41 | Console.WriteLine(log); 42 | break; 43 | } 44 | } 45 | 46 | public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message) 47 | { 48 | this.TraceEvent(eventCache, source, eventType, id, message, new object[] {}); 49 | } 50 | 51 | public override void WriteLine(string message) 52 | { 53 | Console.WriteLine(message); 54 | } 55 | 56 | public override void Write(string message) 57 | { 58 | Console.Write(message); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /WebSocketsCmd/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Net; 6 | using WebSockets.Server; 7 | using System.Diagnostics; 8 | using WebSocketsCmd.Client; 9 | using WebSocketsCmd.Properties; 10 | using WebSockets.Client; 11 | using System.Threading.Tasks; 12 | using WebSockets.Common; 13 | using System.Threading; 14 | using WebSockets; 15 | using WebSockets.Events; 16 | using WebSocketsCmd.Server; 17 | using System.IO; 18 | 19 | namespace WebSocketsCmd 20 | { 21 | public class Program 22 | { 23 | private static void TestClient(object state) 24 | { 25 | var logger = (IWebSocketLogger) state; 26 | using (var client = new ChatWebSocketClient(true, logger)) 27 | { 28 | Uri uri = new Uri("ws://localhost/chat"); 29 | client.TextFrame += Client_TextFrame; 30 | client.ConnectionOpened += Client_ConnectionOpened; 31 | 32 | // test the open handshake 33 | client.OpenBlocking(uri); 34 | } 35 | 36 | Trace.TraceInformation("Client finished, press any key"); 37 | Console.ReadKey(); 38 | } 39 | 40 | private static void Client_ConnectionOpened(object sender, EventArgs e) 41 | { 42 | Trace.TraceInformation("Client: Connection Opened"); 43 | var client = (ChatWebSocketClient) sender; 44 | 45 | // test sending a message to the server 46 | client.Send("Hi"); 47 | } 48 | 49 | private static void Client_TextFrame(object sender, TextFrameEventArgs e) 50 | { 51 | Trace.TraceInformation("Client: {0}", e.Text); 52 | var client = (ChatWebSocketClient) sender; 53 | 54 | // lets test the close handshake 55 | client.Dispose(); 56 | } 57 | 58 | private static void Main(string[] args) 59 | { 60 | IWebSocketLogger logger = new WebSocketLogger(); 61 | 62 | try 63 | { 64 | int port = Settings.Default.Port; 65 | string webRoot = Settings.Default.WebRoot; 66 | if (!Directory.Exists(webRoot)) 67 | { 68 | string baseFolder = AppDomain.CurrentDomain.BaseDirectory; 69 | logger.Warning(typeof(Program), "Webroot folder {0} not found. Using application base directory: {1}", webRoot, baseFolder); 70 | webRoot = baseFolder; 71 | } 72 | 73 | // used to decide what to do with incoming connections 74 | ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger); 75 | 76 | using (WebServer server = new WebServer(serviceFactory, logger)) 77 | { 78 | server.Listen(port); 79 | Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient)); 80 | clientThread.IsBackground = false; 81 | clientThread.Start(logger); 82 | Console.ReadKey(); 83 | } 84 | } 85 | catch (Exception ex) 86 | { 87 | logger.Error(typeof(Program), ex); 88 | Console.ReadKey(); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /WebSocketsCmd/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WebSocketsCmd")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebSocketsCmd")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("a718761f-4d28-4477-ba96-3483f93d61a3")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /WebSocketsCmd/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace WebSocketsCmd.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.UserScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("C:\\Projects\\WebGrid\\WebGrid")] 29 | public string WebRoot { 30 | get { 31 | return ((string)(this["WebRoot"])); 32 | } 33 | set { 34 | this["WebRoot"] = value; 35 | } 36 | } 37 | 38 | [global::System.Configuration.UserScopedSettingAttribute()] 39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 40 | [global::System.Configuration.DefaultSettingValueAttribute("80")] 41 | public int Port { 42 | get { 43 | return ((int)(this["Port"])); 44 | } 45 | set { 46 | this["Port"] = value; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /WebSocketsCmd/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | C:\Projects\WebGrid\WebGrid 7 | 8 | 9 | 80 10 | 11 | 12 | -------------------------------------------------------------------------------- /WebSocketsCmd/README.md: -------------------------------------------------------------------------------- 1 |

WebSocket Server in c#

2 | 3 |

Set WebSockets.Cmd as the startup project

4 | 5 |

License

6 | 7 | The MIT License (MIT) 8 |
See LICENCE.txt 9 | 10 |

Introduction

11 | 12 |

A lot of the Web Socket examples out there are for old Web Socket versions and included complicated code (and external libraries) for fall back communication. All modern browsers that anyone cares about (including safari on an iphone) support at least version 13 of the Web Socket protocol so I'd rather not complicate things. This is a bare bones implementation of the web socket protocol in C# with no external libraries involved. You can connect using standard HTML5 JavaScript.

13 | 14 |

This application serves up basic html pages as well as handling WebSocket connections. This may seem confusing but it allows you to send the client the html they need to make a web socket connection and also allows you to share the same port. However, the HttpConnection is very rudimentary. I'm sure it has some glaring security problems. It was just made to make this demo easier to run. Replace it with your own or don't use it.

15 | 16 |

Background

17 | 18 |

There is nothing magical about Web Sockets. The spec is easy to follow and there is no need to use special libraries. At one point, I was even considering somehow communicating with Node.js but that is not necessary. The spec can be a bit fiddly with bits and bytes but this was probably done to keep the overheads low. This is my first CodeProject article and I hope you will find it easy to follow. The following links offer some great advice:

19 | 20 |

Step by step guide

21 | 22 | 25 | 26 |

The official Web Socket spec

27 | 28 | 31 | 32 |

Some useful stuff in C#

33 | 34 | 37 | 38 |

Using the Code

39 | 40 |

NOTE You will get a firewall warning because you are listening on a port. This is normal for any socket based server.

41 |

A good place to put a breakpoint is in the WebServer class in the HandleAsyncConnection function. Note that this is a multithreaded server so you may want to freeze threads if this gets confusing. The console output prints the thread id to make things easier. If you want to skip past all the plumbing, then another good place to start is the Respond function in the WebSocketConnection class. If you are not interested in the inner workings of Web Sockets and just want to use them, then take a look at the OnTextFrame in the ChatWebSocketConnection class. See below.

42 | 43 |

Implementation of a chat web socket connection is as follows:

44 | 45 |
 46 | internal class ChatWebSocketService : WebSocketService
 47 | {
 48 |     private readonly IWebSocketLogger _logger;
 49 | 
 50 |     public ChatWebSocketService(NetworkStream networkStream, TcpClient tcpClient, string header, IWebSocketLogger logger)
 51 |         : base(networkStream, tcpClient, header, true, logger)
 52 |     {
 53 |         _logger = logger;
 54 |     }
 55 | 
 56 |     protected override void OnTextFrame(string text)
 57 |     {
 58 |         string response = "ServerABC: " + text;
 59 |         base.Send(response);
 60 |         _logger.Information(this.GetType(), response);
 61 |     }
 62 | }
63 | 64 |

The factory used to create the connection is as follows:

65 | 66 |
 67 | internal class ServiceFactory : IServiceFactory
 68 | {
 69 |     public ServiceFactory(string webRoot, IWebSocketLogger logger)
 70 |     {
 71 |         _logger = logger;
 72 |         _webRoot = webRoot;
 73 |     }
 74 | 
 75 |     public IService CreateInstance(ConnectionDetails connectionDetails)
 76 |     {
 77 |         switch (connectionDetails.ConnectionType)
 78 |         {
 79 |             case ConnectionType.WebSocket:
 80 |                 // you can support different kinds of web socket connections using a different path
 81 |                 if (connectionDetails.Path == "/chat")
 82 |                 {
 83 |                     return new ChatWebSocketService(connectionDetails.NetworkStream, connectionDetails.TcpClient, connectionDetails.Header, _logger);
 84 |                 }
 85 |                 break;
 86 |             case ConnectionType.Http:
 87 |                 // this path actually refers to the reletive location of some html file or image
 88 |                 return new HttpService(connectionDetails.NetworkStream, connectionDetails.Path, _webRoot, _logger);
 89 |         }
 90 | 
 91 |         return new BadRequestService(connectionDetails.NetworkStream, connectionDetails.Header, _logger);
 92 |     }
 93 | }
94 | 95 |

HTML5 JavaScript used to connect:

96 | 97 |
 98 | // open the connection to the Web Socket server
 99 | var CONNECTION = new WebSocket('ws://localhost/chat');
100 | 
101 | // Log messages from the server
102 | CONNECTION.onmessage = function (e) {
103 |     console.log(e.data);
104 | };
105 |         
106 | CONNECTION.send('Hellow World');
107 | 108 |

Starting the server and the test client:

109 | 110 |
111 | private static void Main(string[] args)
112 | {
113 |     IWebSocketLogger logger = new WebSocketLogger();
114 |                 
115 |     try
116 |     {
117 |         string webRoot = Settings.Default.WebRoot;
118 |         int port = Settings.Default.Port;
119 | 
120 |         // used to decide what to do with incoming connections
121 |         ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger);
122 | 
123 |         using (WebServer server = new WebServer(serviceFactory, logger))
124 |         {
125 |             server.Listen(port);
126 |             Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient));
127 |             clientThread.IsBackground = false;
128 |             clientThread.Start(logger);
129 |             Console.ReadKey();
130 |         }
131 |     }
132 |     catch (Exception ex)
133 |     {
134 |         logger.Error(null, ex);
135 |         Console.ReadKey();
136 |     }
137 | }
138 | 139 |

The test client runs a short self test to make sure that everything is fine. Opening and closing handshakes are tested here.

140 | 141 |

Web Socket Protocol

142 | 143 |

The first thing to realize about the protocol is that it is, in essence, a basic duplex TCP/IP socket connection. The connection starts off with the client connecting to a remote server and sending http header text to that server. The header text asks the web server to upgrade the connection to a web socket connection. This is done as a handshake where the web server responds with an appropriate http text header and from then onwards, the client and server will talk the Web Socket language.

144 | 145 |

Server Handshake

146 | 147 |
148 | Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)");
149 | Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)");
150 | 
151 | // check the version. Support version 13 and above
152 | const int WebSocketVersion = 13;
153 | int secWebSocketVersion = Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim());
154 | if (secWebSocketVersion < WebSocketVersion)
155 | {
156 |     throw new WebSocketVersionNotSupportedException(string.Format("WebSocket Version {0} not suported. Must be {1} or above", secWebSocketVersion, WebSocketVersion));
157 | }
158 | 
159 | string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim();
160 | string setWebSocketAccept = base.ComputeSocketAcceptString(secWebSocketKey);
161 | string response = ("HTTP/1.1 101 Switching Protocols" + Environment.NewLine
162 |                             + "Connection: Upgrade" + Environment.NewLine
163 |                             + "Upgrade: websocket" + Environment.NewLine
164 |                             + "Sec-WebSocket-Accept: " + setWebSocketAccept);
165 | 
166 | HttpHelper.WriteHttpHeader(response, networkStream);
167 | 168 |

This computes the accept string:

169 | 170 |
171 | /// <summary>
172 | /// Combines the key supplied by the client with a guid and returns the sha1 hash of the combination
173 | /// </summary>
174 | public static string ComputeSocketAcceptString(string secWebSocketKey)
175 | {
176 |     // this is a guid as per the web socket spec
177 |     const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
178 | 
179 |     string concatenated = secWebSocketKey + webSocketGuid;
180 |     byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
181 |     byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
182 |     string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
183 |     return secWebSocketAccept;
184 | }
185 | 186 |

Client Handshake

187 |
188 | Uri uri = _uri;
189 | WebSocketFrameReader reader = new WebSocketFrameReader();
190 | Random rand = new Random();
191 | byte[] keyAsBytes = new byte[16];
192 | rand.NextBytes(keyAsBytes);
193 | string secWebSocketKey = Convert.ToBase64String(keyAsBytes);
194 | 
195 | string handshakeHttpRequestTemplate = @"GET {0} HTTP/1.1{4}" +
196 |                                         "Host: {1}:{2}{4}" +
197 |                                         "Upgrade: websocket{4}" +
198 |                                         "Connection: Upgrade{4}" +
199 |                                         "Sec-WebSocket-Key: {3}{4}" +
200 |                                         "Sec-WebSocket-Version: 13{4}{4}";
201 | 
202 | string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate, uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey, Environment.NewLine);
203 | byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
204 | networkStream.Write(httpRequest, 0, httpRequest.Length);
205 | 206 |

Reading and Writing

207 | 208 |

After the handshake as been performed, the server goes into a read loop. The following two class convert a stream of bytes to a web socket frame and visa versa: WebSocketFrameReader and WebSocketFrameWriter.

209 | 210 |
211 | // from WebSocketFrameReader class
212 | public WebSocketFrame Read(NetworkStream stream, Socket socket)
213 | {
214 |     byte byte1;
215 | 
216 |     try
217 |     {
218 |         byte1 = (byte) stream.ReadByte();
219 |     }
220 |     catch (IOException)
221 |     {
222 |         if (socket.Connected)
223 |         {
224 |             throw;
225 |         }
226 |         else
227 |         {
228 |             return null;
229 |         }
230 |     }
231 | 
232 |     // process first byte
233 |     byte finBitFlag = 0x80;
234 |     byte opCodeFlag = 0x0F;
235 |     bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag;
236 |     WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag);
237 | 
238 |     // read and process second byte
239 |     byte byte2 = (byte) stream.ReadByte();
240 |     byte maskFlag = 0x80;
241 |     bool isMaskBitSet = (byte2 & maskFlag) == maskFlag;
242 |     uint len = ReadLength(byte2, stream);
243 |     byte[] decodedPayload;
244 | 
245 |     // use the masking key to decode the data if needed
246 |     if (isMaskBitSet)
247 |     {
248 |         const int maskKeyLen = 4;
249 |         byte[] maskKey = BinaryReaderWriter.ReadExactly(maskKeyLen, stream);
250 |         byte[] encodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
251 |         decodedPayload = new byte[len];
252 | 
253 |         // apply the mask key
254 |         for (int i = 0; i < encodedPayload.Length; i++)
255 |         {
256 |             decodedPayload[i] = (Byte) (encodedPayload[i] ^ maskKey[i%maskKeyLen]);
257 |         }
258 |     }
259 |     else
260 |     {
261 |         decodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
262 |     }
263 | 
264 |     WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, decodedPayload, true);
265 |     return frame;
266 | }
267 | 268 |
269 | // from WebSocketFrameWriter class
270 | public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame)
271 | {
272 |     // best to write everything to a memory stream before we push it onto the wire
273 |     // not really necessary but I like it this way
274 |     using (MemoryStream memoryStream = new MemoryStream())
275 |     {
276 |         byte finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00;
277 |         byte byte1 = (byte)(finBitSetAsByte | (byte)opCode);
278 |         memoryStream.WriteByte(byte1);
279 | 
280 |         // NB, dont set the mask flag. No need to mask data from server to client
281 |         // depending on the size of the length we want to write it as a byte, ushort or ulong
282 |         if (payload.Length < 126)
283 |         {
284 |             byte byte2 = (byte)payload.Length;
285 |             memoryStream.WriteByte(byte2);
286 |         }
287 |         else if (payload.Length <= ushort.MaxValue)
288 |         {
289 |             byte byte2 = 126;
290 |             memoryStream.WriteByte(byte2);
291 |             BinaryReaderWriter.WriteUShort((ushort)payload.Length, memoryStream, false);
292 |         }
293 |         else
294 |         {
295 |             byte byte2 = 127;
296 |             memoryStream.WriteByte(byte2);
297 |             BinaryReaderWriter.WriteULong((ulong)payload.Length, memoryStream, false);
298 |         }
299 | 
300 |         memoryStream.Write(payload, 0, payload.Length);
301 |         byte[] buffer = memoryStream.ToArray();
302 |         _stream.Write(buffer, 0, buffer.Length);
303 |     }            
304 | }
305 | 
306 | 307 | 308 |

Points of Interest

309 | 310 |

Problems with Proxy Servers:
311 | Proxy servers which have not been configured to support Web sockets will not work well with them.
312 | I suggest that you use transport layer security if you want this to work across the wider internet especially from within a corporation.

313 | 314 |

History

315 | 316 |
    317 |
  • Version 1.0 WebSocket
  • 318 |
319 | -------------------------------------------------------------------------------- /WebSocketsCmd/Server/ChatWebSocketService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Diagnostics; 6 | using System.Net.Sockets; 7 | using WebSockets.Server.WebSocket; 8 | using WebSockets.Common; 9 | using System.IO; 10 | 11 | namespace WebSocketsCmd.Server 12 | { 13 | internal class ChatWebSocketService : WebSocketService 14 | { 15 | private readonly IWebSocketLogger _logger; 16 | 17 | public ChatWebSocketService(Stream stream, TcpClient tcpClient, string header, IWebSocketLogger logger) 18 | : base(stream, tcpClient, header, true, logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | protected override void OnTextFrame(string text) 24 | { 25 | string response = "ServerABC: " + text; 26 | base.Send(response); 27 | _logger.Information(this.GetType(), response); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /WebSocketsCmd/Server/ServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using System.Net.Sockets; 9 | using System.Diagnostics; 10 | using WebSockets.Server; 11 | using WebSockets.Server.Http; 12 | using WebSockets.Common; 13 | 14 | namespace WebSocketsCmd.Server 15 | { 16 | internal class ServiceFactory : IServiceFactory 17 | { 18 | private readonly IWebSocketLogger _logger; 19 | private readonly string _webRoot; 20 | 21 | private string GetWebRoot() 22 | { 23 | if (!string.IsNullOrWhiteSpace(_webRoot) && Directory.Exists(_webRoot)) 24 | { 25 | return _webRoot; 26 | } 27 | 28 | return Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase).Replace(@"file:\", string.Empty); 29 | } 30 | 31 | public ServiceFactory(string webRoot, IWebSocketLogger logger) 32 | { 33 | _logger = logger; 34 | _webRoot = string.IsNullOrWhiteSpace(webRoot) ? GetWebRoot() : webRoot; 35 | if (!Directory.Exists(_webRoot)) 36 | { 37 | _logger.Warning(this.GetType(), "Web root not found: {0}", _webRoot); 38 | } 39 | else 40 | { 41 | _logger.Information(this.GetType(), "Web root: {0}", _webRoot); 42 | } 43 | } 44 | 45 | public IService CreateInstance(ConnectionDetails connectionDetails) 46 | { 47 | switch (connectionDetails.ConnectionType) 48 | { 49 | case ConnectionType.WebSocket: 50 | // you can support different kinds of web socket connections using a different path 51 | if (connectionDetails.Path == "/chat") 52 | { 53 | return new ChatWebSocketService(connectionDetails.Stream, connectionDetails.TcpClient, connectionDetails.Header, _logger); 54 | } 55 | break; 56 | case ConnectionType.Http: 57 | // this path actually refers to the reletive location of some html file or image 58 | return new HttpService(connectionDetails.Stream, connectionDetails.Path, _webRoot, _logger); 59 | } 60 | 61 | return new BadRequestService(connectionDetails.Stream, connectionDetails.Header, _logger); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /WebSocketsCmd/WebSocketLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using WebSockets.Common; 6 | using System.Diagnostics; 7 | 8 | namespace WebSocketsCmd 9 | { 10 | internal class WebSocketLogger : IWebSocketLogger 11 | { 12 | public void Information(Type type, string format, params object[] args) 13 | { 14 | Trace.TraceInformation(format, args); 15 | } 16 | 17 | public void Warning(Type type, string format, params object[] args) 18 | { 19 | Trace.TraceWarning(format, args); 20 | } 21 | 22 | public void Error(Type type, string format, params object[] args) 23 | { 24 | Trace.TraceError(format, args); 25 | } 26 | 27 | public void Error(Type type, Exception exception) 28 | { 29 | Error(type, "{0}", exception); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /WebSocketsCmd/WebSocketsCmd.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71} 9 | Exe 10 | Properties 11 | WebSocketsCmd 12 | WebSocketsCmd 13 | v4.0 14 | 15 | 16 | 512 17 | 18 | 19 | x86 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | x86 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | True 55 | True 56 | Settings.settings 57 | 58 | 59 | 60 | 61 | 62 | 63 | PreserveNewest 64 | 65 | 66 | SettingsSingleFileGenerator 67 | Settings.Designer.cs 68 | 69 | 70 | 71 | 72 | PreserveNewest 73 | 74 | 75 | 76 | 77 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C} 78 | WebSockets 79 | 80 | 81 | 82 | 89 | -------------------------------------------------------------------------------- /WebSocketsCmd/client.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Web Socket Demo 10 | 20 | 21 | 22 |
    23 |
    24 | 25 |
    26 | 27 | 28 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /WebSocketsCmd/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninjasource/websocket-server/3fc27cf05077235c9f764728d3ec9e7139845275/WebSocketsCmd/favicon.ico -------------------------------------------------------------------------------- /gitpublish.bat: -------------------------------------------------------------------------------- 1 | git fetch origin 2 | git status 3 | git add . 4 | git commit -m "Version 1.03 Major refactor and added support for c# Client" 5 | git push origin master --------------------------------------------------------------------------------