websocket-server – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 <h1>WebSocket Server in c#</h1>
2  
3 <p>Set <code>WebSockets.Cmd</code> as the startup project</p>
4  
5 <h2>License</h2>
6  
7 The MIT License (MIT)
8 <br/>See LICENCE.txt
9  
10 <h2>Introduction</h2>
11  
12 <p>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 <strong>version 13 of the Web Socket protocol </strong>so I&#39;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.</p>
13  
14 <p>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 <code>HttpConnection</code> is very rudimentary. I&#39;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&#39;t use it.</p>
15  
16 <h2>Background</h2>
17  
18 <p>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:</p>
19  
20 <p>Step by step guide</p>
21  
22 <ul>
23 <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers">https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers</a></li>
24 </ul>
25  
26 <p>The official Web Socket spec</p>
27  
28 <ul>
29 <li><a href="http://tools.ietf.org/html/rfc6455">http://tools.ietf.org/html/rfc6455</a></li>
30 </ul>
31  
32 <p>Some useful stuff in C#</p>
33  
34 <ul>
35 <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server">https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server</a></li>
36 </ul>
37  
38 <h2>Using the Code</h2>
39  
40 <p>NOTE You will get a firewall warning because you are listening on a port. This is normal for any socket based server.<p>
41 <p>A good place to put a breakpoint is in the <code>WebServer</code> class in the <code>HandleAsyncConnection</code> function. Note that this is a multithreaded server so&nbsp;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 <code>Respond</code> function in the <code>WebSocketConnection</code> 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 <code>OnTextFrame</code> in the <code>ChatWebSocketConnection</code> class. See below.</p>
42  
43 <p>Implementation of a chat web socket connection is as follows:</p>
44  
45 <pre lang="cs">
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 }</pre>
63  
64 <p>The factory used to create the connection is as follows:</p>
65  
66 <pre lang="cs">
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 }</pre>
94  
95 <p>HTML5 JavaScript used to connect:</p>
96  
97 <pre lang="jscript">
98 // open the connection to the Web Socket server
99 var CONNECTION = new WebSocket(&#39;ws://localhost/chat&#39;);
100  
101 // Log messages from the server
102 CONNECTION.onmessage = function (e) {
103 console.log(e.data);
104 };
105  
106 CONNECTION.send(&#39;Hellow World&#39;);</pre>
107  
108 <p>Starting the server and the test client: </p>
109  
110 <pre lang="cs">
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 }</pre>
138  
139 <p>The test client runs a short self test to make sure that everything is fine. Opening and closing handshakes are tested here. </p>
140  
141 <h2>Web Socket Protocol</h2>
142  
143 <p>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.</p>
144  
145 <h3>Server Handshake</h3>
146  
147 <pre lang="cs">
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);</pre>
167  
168 <p>This computes the <code>accept string</code>:</p>
169  
170 <pre lang="cs">
171 /// &lt;summary&gt;
172 /// Combines the key supplied by the client with a guid and returns the sha1 hash of the combination
173 /// &lt;/summary&gt;
174 public static string ComputeSocketAcceptString(string secWebSocketKey)
175 {
176 // this is a guid as per the web socket spec
177 const string webSocketGuid = &quot;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&quot;;
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 }</pre>
185  
186 <h3>Client Handshake</h3>
187 <pre lang="cs">
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);</pre>
205  
206 <h3>Reading and Writing</h3>
207  
208 <p>After the handshake as been performed, the server goes into a <code>read</code> loop. The following two class convert a stream of bytes to a web socket frame and visa versa: <code>WebSocketFrameReader</code> and <code>WebSocketFrameWriter</code>. </p>
209  
210 <pre lang="cs">
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 }</pre>
267  
268 <pre lang="cs">
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 </pre>
306  
307  
308 <h2>Points of Interest</h2>
309  
310 <p>Problems with Proxy Servers:<br />
311 Proxy servers which have not been configured to support Web sockets will not work well with them.<br />
312 I suggest that you use transport layer security if you want this to work across the wider internet especially from within a corporation.</p>
313  
314 <h2>History</h2>
315  
316 <ul>
317 <li>Version 1.01 WebSocket</li>
318 <li>Version 1.02 Fixed endianness bug with length field</li>
319 <li>Version 1.03 Major refactor and added support for c# Client</li>
320 </ul>