private async void StartSending(UInt64 subscriberId, SubscriberRecord subscriber) { try { var lastRevisions = new Dictionary <UInt32, UInt32>(); // {topic} => {revision} // Loop until disposed or closed while (!IsDisposed && subscriber.Socket.State == WebSocketState.Open) { foreach (var topicRecord in TopicRecords) { // If the client is in need of an update if (subscriber.Channels.Contains(topicRecord.Value.Channel) && // Correct channel (!lastRevisions.TryGetValue(topicRecord.Key, out var lastRevision) || lastRevision < topicRecord.Value.Revision)) // There's a new revision { // Send update await subscriber.Socket.SendAsync(topicRecord.Value.Packet, WebSocketMessageType.Binary, true, CancellationToken.None); // Note latest revision sent lastRevisions[topicRecord.Key] = topicRecord.Value.Revision; } } // Wait for next change subscriber.SendLock.WaitOne(); } } catch (ObjectDisposedException) { } catch (Exception e) { Trace.TraceWarning($"Subscriber {subscriberId} TX: {e.Message}"); } }
private async void StartReceiving(UInt64 subscriberId, SubscriberRecord subscriber) { var receiveBuffer = new Byte[512]; // TODO try { while (!IsDisposed && subscriber.Socket.State == WebSocketState.Open) { // Receive message into buffer var receiveResult = await subscriber.Socket.ReceiveAsync(new ArraySegment <Byte>(receiveBuffer), CancellationToken.None); // Close connection if requested if (receiveResult.MessageType == WebSocketMessageType.Close) { await subscriber.Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); continue; } // Close connection on bad message type if (receiveResult.MessageType == WebSocketMessageType.Text) { await subscriber.Socket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "Cannot accept text frame", CancellationToken.None); continue; } // Close connection if message too long if (!receiveResult.EndOfMessage) { await subscriber.Socket.CloseAsync(WebSocketCloseStatus.MessageTooBig, "Message too big", CancellationToken.None); continue; } //await subscriber.Socket.SendAsync(new ArraySegment<Byte>(receiveBuffer, 0, receiveResult.Count), WebSocketMessageType.Binary, receiveResult.EndOfMessage, CancellationToken.None); } } catch (ObjectDisposedException) { // TODO: other exceptions } catch (Exception e) { Trace.TraceWarning($"Subscriber {subscriberId} RX: {e.Message}"); } finally { subscriber.Socket.Dispose(); SubscriberRecords.TryRemove(subscriberId, out subscriber); } }
private void ReceieveThread_OnSpin(Object obj) { var buffer = new Byte[Mtu]; try { while (!IsDisposed) { try { // Remove all expired subscriptions TODO - should be on a timer? var expiry = DateTime.UtcNow.Subtract(KeepAliveInterval).Subtract(KeepAiveGrace); foreach (var ep in SubscriberRecords.Where(a => a.Value.LastAuthorizedAt < expiry).Select(a => a.Key)) { SubscriberRecords.TryRemove(ep, out var record); Trace.WriteLine($"{ep} Subscription expired.", "server-receive"); } // Wait for packet to arrive var endpoint = (EndPoint) new IPEndPoint(IPAddress.Any, 0); var len = Socket.ReceiveFrom(buffer, ref endpoint); if (len < 1) { Trace.WriteLine($"Strange byte count {len}.", "server-receive-warning"); continue; } // Check packet sanity if (len < Constants.CLIENTTXHEADER_LENGTH) { Trace.WriteLine($"{endpoint} Received packet that is too small to be valid. Discarded.", "server-receive"); continue; } if ((len - Constants.CLIENTTXHEADER_LENGTH) % 6 > 0) { Trace.WriteLine($"{endpoint} Received packet is not a valid length. Discarded.", "server-receive"); continue; } // Check version var version = buffer[0]; if (version != Constants.VERSION) { Trace.WriteLine($"{endpoint} Received packet version does not match or is corrupted. Discarded.", "server-receive"); continue; } // Check authorization token var authorizationToken = new Byte[16]; Buffer.BlockCopy(buffer, 1, authorizationToken, 0, authorizationToken.Length); if (!AuthorizationFilter(endpoint, authorizationToken)) { Trace.WriteLine($"{endpoint} Received packet with rejected authorization token. Discarded.", "server-receive"); continue; } // Find subscriber record if (SubscriberRecords.TryGetValue(endpoint, out var subscriberRecord)) { // Record exists, update authorizedAt subscriberRecord.LastAuthorizedAt = DateTime.UtcNow; // Process ACKs var pos = Constants.CLIENTTXHEADER_LENGTH; if (pos == len) { Trace.WriteLine($"{endpoint} Sent keep-alive.", "server-receive"); } while (pos < len) { // Extract topic var topic = BitConverter.ToUInt32(buffer, pos); // Extract revision var revision = BitConverter.ToUInt16(buffer, pos + 4); Trace.WriteLine($"{endpoint} Acknowledged {topic}#{revision}.", "server-receive"); if (TopicRecords.TryGetValue(topic, out var topicRecord)) { if (topicRecord.Revision == revision) { topicRecord.PendingSubscribers = topicRecord.PendingSubscribers.Except(new EndPoint[] { endpoint }).ToArray(); // Replace rather than adding so we don't have a sync issue } } pos += 10; } } else { // Record doesn't exist, created subscriberRecord = SubscriberRecords[endpoint] = new SubscriberRecord() { LastAuthorizedAt = DateTime.UtcNow, }; // Queue sending latest value from all topics foreach (var topicRecord in TopicRecords.Select(a => a.Value)) { topicRecord.PendingSubscribers = topicRecord.PendingSubscribers.Union(new EndPoint[] { endpoint }).ToArray(); // Replace rather than adding so we don't have a sync issue } } } catch (SocketException ex) { if (ex.SocketErrorCode == SocketError.TimedOut) { continue; } Trace.WriteLine($"Socket error {ex.SocketErrorCode}.", "server-receive-warning"); } } } catch (ObjectDisposedException) { } }
private async void AcceptThread_OnSpin(Object obj) { try { // Loop until disposed while (!IsDisposed) { // Wait for inbound request var listenerContext = await Listener.GetContextAsync(); // Respond to options requests if (listenerContext.Request.HttpMethod == "OPTIONS") { listenerContext.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); listenerContext.Response.AddHeader("Access-Control-Allow-Methods", "GET"); listenerContext.Response.AddHeader("Access-Control-Max-Age", "86400"); listenerContext.Response.Close(); continue; } listenerContext.Response.AppendHeader("Access-Control-Allow-Origin", "*"); // Handle requests for client if (listenerContext.Request.Url.PathAndQuery == "/client.ts") { listenerContext.Response.StatusCode = 200; listenerContext.Response.StatusDescription = "OK"; listenerContext.Response.ContentType = "application/typescript"; listenerContext.Response.Close(ClientTypeScript, false); continue; } if (listenerContext.Request.Url.PathAndQuery == "/client.js") { listenerContext.Response.StatusCode = 200; listenerContext.Response.StatusDescription = "OK"; listenerContext.Response.ContentType = "application/javascript"; listenerContext.Response.Close(ClientJavaScript, false); continue; } // Favicon if (listenerContext.Request.Url.PathAndQuery == "/favicon.ico") { listenerContext.Response.StatusCode = 404; listenerContext.Response.StatusDescription = "Not found"; listenerContext.Response.Close(ClientJavaScript, false); continue; } // Reject if not a websocket request if (!listenerContext.Request.IsWebSocketRequest) { listenerContext.Response.StatusCode = 426; listenerContext.Response.StatusDescription = "WebSocket required"; listenerContext.Response.Close(); continue; } // Get channel list var channelsStrings = listenerContext.Request.QueryString["channels"]; if (String.IsNullOrWhiteSpace(channelsStrings)) { listenerContext.Response.StatusCode = 400; listenerContext.Response.StatusDescription = "Missing channels"; listenerContext.Response.Close(); continue; } var channels = new List <UInt32>(); foreach (var channelString in channelsStrings.Split(',')) { if (!UInt32.TryParse(channelString, out var channel)) { listenerContext.Response.StatusCode = 400; listenerContext.Response.StatusDescription = $"Bad channel {channelString}"; listenerContext.Response.Close(); continue; } channels.Add(channel); } // TODO: reject if not secure? // TODO: authentication // Upgrade to web sockets WebSocketContext webSocketContext = null; try { webSocketContext = await listenerContext.AcceptWebSocketAsync(SubProtocol, KeepAliveInterval); } catch (Exception e) { listenerContext.Response.StatusCode = 400; listenerContext.Response.StatusDescription = e.Message; // TODO: bad idea? listenerContext.Response.Close(); return; } // Create subscriber record var subscriberId = (UInt64)(Interlocked.Increment(ref NextSubscriberId) - Int64.MinValue); var subscriber = SubscriberRecords[subscriberId] = new SubscriberRecord() { Socket = webSocketContext.WebSocket, Channels = channels.ToArray() }; Task.Run(async() => StartReceiving(subscriberId, subscriber)); // TODO: Clean this up Task.Run(async() => StartSending(subscriberId, subscriber)); } } catch (ObjectDisposedException) { } }