using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Web; // ============================================================ // Queue Ticket System — Pure C# Console Application with SSE // No ASP.NET, No Kestrel, No Framework // Just a TcpListener, raw HTTP, and Server-Sent Events. // ============================================================ class Program { // --- Configuration --- static int Port = 8080; // --- Departments --- static Dictionary Departments = new Dictionary { { 1, "General Inquiries" }, { 2, "Payments" }, { 3, "License Services" }, { 4, "Account & Register" } }; // --- Ticket counters per department --- // Department 1 starts at 1000, Department 2 at 2000, etc. static Dictionary NextTicketNumber = new Dictionary { { 1, 1000 }, { 2, 2000 }, { 3, 3000 }, { 4, 4000 } }; // --- Queue: tickets waiting to be called, per department --- static Dictionary> WaitingQueues = new Dictionary> { { 1, new Queue() }, { 2, new Queue() }, { 3, new Queue() }, { 4, new Queue() } }; // --- Call history --- static List CallHistory = new List(); // --- SSE clients --- static List DisplayClients = new List(); static int SseEventId = 0; // --- Lock for thread safety --- static object Lock = new object(); static void Main(string[] args) { TcpListener listener = new TcpListener(IPAddress.Any, Port); listener.Start(); Console.WriteLine($"Queue Ticket System started on port {Port}"); Console.WriteLine($"Display: http://localhost:{Port}/display"); Console.WriteLine($"Terminal: http://localhost:{Port}/terminal"); Console.WriteLine($"Operator: http://localhost:{Port}/operator"); Console.WriteLine(); while (true) { TcpClient client = listener.AcceptTcpClient(); Thread thread = new Thread(() => HandleClient(client)); thread.IsBackground = true; thread.Start(); } } // ============================================================ // Layer 1: HTTP — Read request, route, write response // ============================================================ static void HandleClient(TcpClient client) { try { NetworkStream stream = client.GetStream(); stream.ReadTimeout = 5000; // Wait briefly for data to arrive if (!stream.DataAvailable) { // Give the client a moment to send data System.Threading.Thread.Sleep(500); if (!stream.DataAvailable) return; // nothing came, abandon } // --- Read headers byte-by-byte until \r\n\r\n --- StringBuilder headerBuilder = new StringBuilder(); int prev3 = 0, prev2 = 0, prev1 = 0, current = 0; while (true) { int b = stream.ReadByte(); if (b == -1) return; headerBuilder.Append((char)b); prev3 = prev2; prev2 = prev1; prev1 = current; current = b; if (prev3 == '\r' && prev2 == '\n' && prev1 == '\r' && current == '\n') break; } string headerSection = headerBuilder.ToString(); // --- Parse Content-Length --- int contentLength = 0; string[] headerLines = headerSection.Split(new[] { "\r\n" }, StringSplitOptions.None); foreach (string line in headerLines) { if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) { string val = line.Substring("Content-Length:".Length).Trim(); int.TryParse(val, out contentLength); break; } } // --- Read body --- string body = ""; if (contentLength > 0) { byte[] bodyBuffer = new byte[contentLength]; int totalRead = 0; while (totalRead < contentLength) { int read = stream.Read(bodyBuffer, totalRead, contentLength - totalRead); if (read == 0) break; totalRead += read; } body = Encoding.UTF8.GetString(bodyBuffer, 0, totalRead); } // --- Parse request --- HttpRequest request = ParseHttpRequest(headerSection, body); Console.WriteLine($"{request.Method} {request.Path}"); // --- Route --- string path = request.Path; // SSE endpoints — these hold the connection open if (path == "/sse-display") { HandleSseDisplay(client, stream); return; // don't close — SSE keeps the connection alive } // Normal HTTP endpoints string responseHtml; int statusCode = 200; switch (path) { case "/": case "/display": responseHtml = PageDisplay(); break; case "/terminal": responseHtml = PageTerminal(request); break; case "/operator": responseHtml = PageOperator(request); break; default: responseHtml = PageNotFound(); statusCode = 404; break; } // --- Send HTTP response --- string statusText = statusCode == 200 ? "OK" : "Not Found"; string httpResponse = $"HTTP/1.1 {statusCode} {statusText}\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + $"Content-Length: {Encoding.UTF8.GetByteCount(responseHtml)}\r\n" + "Connection: close\r\n" + "\r\n" + responseHtml; byte[] responseBytes = Encoding.UTF8.GetBytes(httpResponse); stream.Write(responseBytes, 0, responseBytes.Length); stream.Flush(); } catch (Exception ex) { Console.WriteLine("Error: " + ex.Message); } finally { try { client.Close(); } catch { } } } // ============================================================ // SSE — Server-Sent Events // ============================================================ static void HandleSseDisplay(TcpClient client, NetworkStream stream) { try { // Send SSE headers — the response that never ends string headers = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "\r\n"; byte[] headerBytes = Encoding.UTF8.GetBytes(headers); stream.Write(headerBytes, 0, headerBytes.Length); stream.Flush(); // Send the current state immediately so the display is not blank string currentHtml = RenderDisplayBoard(); string initEvent = $"id: {SseEventId}\nevent: display\ndata: {EncodeSSE(currentHtml)}\n\n"; byte[] initBytes = Encoding.UTF8.GetBytes(initEvent); stream.Write(initBytes, 0, initBytes.Length); stream.Flush(); // Register this client SseClient sseClient = new SseClient { Client = client, Stream = stream }; lock (Lock) { DisplayClients.Add(sseClient); } Console.WriteLine("SSE display client connected. Total: " + DisplayClients.Count); // Keep the connection alive — wait until it dies // We detect this by periodically sending a comment (heartbeat) while (true) { Thread.Sleep(15000); try { // SSE comment — keeps the connection alive through proxies byte[] heartbeat = Encoding.UTF8.GetBytes(": heartbeat\n\n"); stream.Write(heartbeat, 0, heartbeat.Length); stream.Flush(); } catch { break; // client disconnected } } } catch { } finally { // Remove from SSE client list lock (Lock) { DisplayClients.RemoveAll(c => c.Client == client); } try { client.Close(); } catch { } Console.WriteLine("SSE display client disconnected. Total: " + DisplayClients.Count); } } static void BroadcastDisplay() { string html = RenderDisplayBoard(); int eventId; List clients; lock (Lock) { SseEventId++; eventId = SseEventId; clients = new List(DisplayClients); } string eventData = $"id: {eventId}\nevent: display\ndata: {EncodeSSE(html)}\n\n"; byte[] eventBytes = Encoding.UTF8.GetBytes(eventData); List deadClients = new List(); foreach (var c in clients) { try { c.Stream.Write(eventBytes, 0, eventBytes.Length); c.Stream.Flush(); } catch { deadClients.Add(c); } } // Clean up dead connections if (deadClients.Count > 0) { lock (Lock) { foreach (var dead in deadClients) { DisplayClients.Remove(dead); try { dead.Client.Close(); } catch { } } } } } // SSE data field cannot contain newlines — encode as single line static string EncodeSSE(string html) { // Replace newlines so the entire HTML fits in one SSE data line // The browser will decode this back return html.Replace("\n", " ").Replace("\r", ""); } // ============================================================ // Render: Display Board HTML (sent via SSE) // ============================================================ static string RenderDisplayBoard() { lock (Lock) { // The most recent call is the "Now Serving" — shown large on the left // The next few recent calls are shown on the right panel QueueCall latest = CallHistory.Count > 0 ? CallHistory[CallHistory.Count - 1] : null; // Get the last few calls for the history panel (excluding the latest) List recent = new List(); for (int i = CallHistory.Count - 2; i >= 0 && recent.Count < 5; i--) { recent.Add(CallHistory[i]); } string mainPanel; if (latest != null) { mainPanel = $@"
{latest.TicketNumber}
Counter {latest.CounterNumber}
{HtmlEncode(GetDeptName(latest.DepartmentId))}
"; } else { mainPanel = @"
----
Waiting
"; } StringBuilder historyHtml = new StringBuilder(); foreach (var call in recent) { historyHtml.Append($@"
{call.TicketNumber} Counter {call.CounterNumber}
"); } return $@"
Now Serving
{mainPanel}
Recent
{historyHtml}
"; } } // ============================================================ // Queue Engine // ============================================================ static int RegisterTicket(int departmentId) { lock (Lock) { if (!NextTicketNumber.ContainsKey(departmentId)) return -1; NextTicketNumber[departmentId]++; int ticket = NextTicketNumber[departmentId]; WaitingQueues[departmentId].Enqueue(ticket); return ticket; } } static int CallNextNumber(int counterNumber, int departmentId) { lock (Lock) { if (!WaitingQueues.ContainsKey(departmentId)) return -1; if (WaitingQueues[departmentId].Count == 0) return -1; int ticket = WaitingQueues[departmentId].Dequeue(); CallHistory.Add(new QueueCall { TicketNumber = ticket, DepartmentId = departmentId, CounterNumber = counterNumber, CalledAt = DateTime.Now }); return ticket; } } static int CallSpecificNumber(int counterNumber, int departmentId, int ticketNumber) { lock (Lock) { if (!WaitingQueues.ContainsKey(departmentId)) return -1; // Remove from queue if present var queue = WaitingQueues[departmentId]; var remaining = new Queue(); bool found = false; while (queue.Count > 0) { int t = queue.Dequeue(); if (t == ticketNumber && !found) { found = true; // skip — we're pulling this one out } else { remaining.Enqueue(t); } } // Rebuild the queue without the called ticket WaitingQueues[departmentId] = remaining; if (!found) return -1; CallHistory.Add(new QueueCall { TicketNumber = ticketNumber, DepartmentId = departmentId, CounterNumber = counterNumber, CalledAt = DateTime.Now }); return ticketNumber; } } // --- Placeholders for future implementation --- static class TicketEngine { public static int Register(int departmentId) { return RegisterTicket(departmentId); } } static class KioskEngine { public static void PrintTicket(int ticketNumber) { // Placeholder: physical ticket printer integration Console.WriteLine($" [Printer] Ticket #{ticketNumber}"); } } static class AudioAnnouncer { public static void Announce(int ticketNumber, int counterNumber) { // Placeholder: text-to-speech / audio file playback Console.WriteLine($" [Audio] Now serving #{ticketNumber} at Counter {counterNumber}"); } } // ============================================================ // Layer 2: Pages // ============================================================ // --- Display Page (the TV screen) --- static string PageDisplay() { return $@" Queue Display
Now Serving
----
Waiting
Recent
Connecting...
"; } // --- Terminal Page (the kiosk where visitors take a number) --- static string PageTerminal(HttpRequest request) { string message = ""; if (request.Method == "POST") { string action = request.GetForm("action"); if (action == "take-number") { int deptId = request.GetFormInt("department-id"); if (Departments.ContainsKey(deptId)) { int ticket = TicketEngine.Register(deptId); KioskEngine.PrintTicket(ticket); message = $@"
Your Number
{ticket}
{HtmlEncode(Departments[deptId])}
Please wait for your number to be called.
"; } } } // Build department buttons StringBuilder deptButtons = new StringBuilder(); foreach (var dept in Departments) { int waiting; lock (Lock) { waiting = WaitingQueues[dept.Key].Count; } deptButtons.Append($@"
"); } return $@" Take a Number

Queue Ticket

Select a service to take a number

{message} {deptButtons} View Display Board "; } // --- Operator Page (the counter staff) --- static string PageOperator(HttpRequest request) { string message = ""; string counterNumber = request.GetForm("counter-number"); string departmentId = request.GetForm("department-id"); bool isSetup = string.IsNullOrEmpty(counterNumber) || counterNumber == "0"; if (request.Method == "POST") { string action = request.GetForm("action"); if (action == "call-next") { int counter = request.GetFormInt("counter-number"); int deptId = request.GetFormInt("department-id"); int ticket = CallNextNumber(counter, deptId); if (ticket > 0) { AudioAnnouncer.Announce(ticket, counter); BroadcastDisplay(); message = $"
Called #{ticket} to Counter {counter}
"; } else { message = "
No tickets waiting in queue.
"; } // Stay in operator mode counterNumber = counter.ToString(); departmentId = deptId.ToString(); isSetup = false; } else if (action == "call-specific") { int counter = request.GetFormInt("counter-number"); int deptId = request.GetFormInt("department-id"); int specificTicket = request.GetFormInt("ticket-number"); int ticket = CallSpecificNumber(counter, deptId, specificTicket); if (ticket > 0) { AudioAnnouncer.Announce(ticket, counter); BroadcastDisplay(); message = $"
Called #{ticket} to Counter {counter}
"; } else { message = $"
Ticket #{specificTicket} not found in queue.
"; } counterNumber = counter.ToString(); departmentId = deptId.ToString(); isSetup = false; } else if (action == "setup") { // Transition from setup to operator mode counterNumber = request.GetForm("counter-number"); departmentId = request.GetForm("department-id"); isSetup = false; } } if (isSetup) { return PageOperatorSetup(); } // --- Operator mode: show call buttons --- int deptIdInt = 0; int.TryParse(departmentId, out deptIdInt); string deptName = GetDeptName(deptIdInt); int waitingCount; string waitingList; lock (Lock) { var queue = WaitingQueues.ContainsKey(deptIdInt) ? WaitingQueues[deptIdInt] : new Queue(); waitingCount = queue.Count; waitingList = string.Join(", ", queue); } return $@" Operator — Counter {HtmlEncode(counterNumber)}

Counter {HtmlEncode(counterNumber)}

{HtmlEncode(deptName)}
{message}
Waiting: {waitingCount}
{(waitingCount > 0 ? waitingList : "—")}
Change Counter "; } // --- Operator Setup Page --- static string PageOperatorSetup() { StringBuilder deptOptions = new StringBuilder(); foreach (var dept in Departments) { deptOptions.Append($""); } return $@" Operator Setup

Operator Setup

"; } // --- 404 Page --- static string PageNotFound() { return @" Not Found

404 — Page Not Found

Display | Terminal | Operator

"; } // ============================================================ // HTTP Parser (same as previous article) // ============================================================ static HttpRequest ParseHttpRequest(string headerSection, string body) { HttpRequest request = new HttpRequest(); string[] lines = headerSection.Split(new[] { "\r\n" }, StringSplitOptions.None); string[] requestLine = lines[0].Split(' '); request.Method = requestLine[0].ToUpper(); request.Path = requestLine.Length > 1 ? requestLine[1] : "/"; for (int i = 1; i < lines.Length; i++) { int colon = lines[i].IndexOf(':'); if (colon > 0) { string key = lines[i].Substring(0, colon).Trim(); string value = lines[i].Substring(colon + 1).Trim(); request.Headers[key] = value; } } int qIndex = request.Path.IndexOf('?'); if (qIndex >= 0) { string queryString = request.Path.Substring(qIndex + 1); request.Path = request.Path.Substring(0, qIndex); ParseFormData(queryString, request.Query); } if (request.Method == "POST" && body.Length > 0) { ParseFormData(body, request.Form); } request.Path = request.Path.ToLower().Trim().TrimEnd('/'); if (request.Path == "") request.Path = "/"; return request; } static void ParseFormData(string data, Dictionary target) { string[] pairs = data.Split('&'); foreach (string pair in pairs) { string[] kv = pair.Split(new[] { '=' }, 2); string key = HttpUtility.UrlDecode(kv[0]); string value = kv.Length > 1 ? HttpUtility.UrlDecode(kv[1]) : ""; target[key] = value; } } // ============================================================ // Utility // ============================================================ static string GetDeptName(int deptId) { return Departments.ContainsKey(deptId) ? Departments[deptId] : "Unknown"; } static string HtmlEncode(string text) { if (string.IsNullOrEmpty(text)) return ""; return text.Replace("&", "&") .Replace("<", "<") .Replace(">", ">") .Replace("\"", """) .Replace("'", "'"); } } // ============================================================ // Models // ============================================================ class QueueCall { public int TicketNumber { get; set; } public int DepartmentId { get; set; } public int CounterNumber { get; set; } public DateTime CalledAt { get; set; } } class SseClient { public TcpClient Client { get; set; } public NetworkStream Stream { get; set; } } class HttpRequest { public string Method { get; set; } = "GET"; public string Path { get; set; } = "/"; public Dictionary Headers { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Query { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Form { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public string GetForm(string key) { return Form.ContainsKey(key) ? Form[key] : ""; } public int GetFormInt(string key) { string val = GetForm(key); int result; int.TryParse(val, out result); return result; } public string GetQuery(string key) { return Query.ContainsKey(key) ? Query[key] : ""; } }