Websockets
By the end of this lesson you'll understand how a WebSocket upgrades a one-shot HTTP request into a persistent, two-way channel — and you'll be able to accept a connection in ASP.NET Core, run a receive loop that reads and sends framed messages, close the connection cleanly, and decide when to reach for raw WebSockets instead of SignalR.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Ordinary HTTP is like posting letters : you write a request, send it, and wait for a single reply to come back; to ask again, you post a brand-new letter. A WebSocket is a phone call . After a short "Can you hear me? — Yes, go ahead" handshake, the line stays open and both sides can talk whenever they like , in either direction, without hanging up and redialling. That open, two-way line is exactly what you need for chat, live dashboards, multiplayer games, and price tickers — anywhere the server must push data the instant it happens, not only when a client asks.
A WebSocket begins life as an HTTP request — that's how it sails through firewalls and proxies — then "upgrades" into the persistent channel. Use wss:// (TLS) in production exactly as you'd use https:// .
1. Full-Duplex & the Upgrade Handshake
Full-duplex means both ends can send at the same moment, like two people on a phone call talking over each other. Plain HTTP is request/response : the client speaks, the server replies, done. A WebSocket starts as a normal HTTP GET carrying an Upgrade: websocket header; the server answers 101 Switching Protocols , and from that point the same TCP connection is a raw two-way pipe. In ASP.NET Core you turn this on with app.UseWebSockets() , then complete the handshake with AcceptWebSocketAsync() .
Raw WebSocket and ASP.NET Core code can't run in the in-page editor — there's no web server or browser here — so read these worked examples and run them in a real ASP.NET Core project. The runnable 🎯 Your Turn exercises further down model the same message loop in plain C# that does run here.
2. Message Framing & the Receive Loop
WebSocket data travels in frames . Each frame is labelled text ( WebSocketMessageType.Text , UTF-8) or binary ( WebSocketMessageType.Binary , raw bytes — files, images, protobuf). A third type, Close , is a control frame that means "I'm hanging up." Your server runs a receive loop : while the socket is open, ReceiveAsync reads the next frame into a buffer and SendAsync writes one back. A big message can span several frames — result.EndOfMessage tells you when the last chunk has arrived.
Notice the shape: read a frame → decide what it is → send a response → loop . That single loop is the heart of every WebSocket endpoint. Now model it yourself in code that runs right here.
Your turn. A real server reads frames off the network one at a time; below, a Queue<string> stands in for those incoming frames. Loop over them and echo each one back uppercased , the way SendAsync would push a reply. Fill in the three ___ blanks, then run it.
3. The Close Handshake
A WebSocket shouldn't just vanish — both sides agree to hang up. When the peer sends a Close frame, you reply with CloseAsync , passing a close code and a short reason, then break out of the receive loop. The close code is a number that says why the connection ended — 1000 for a normal, expected close, others for errors. Always handle the Close frame; if you don't, the loop reads forever and the connection leaks.
In .NET these map to the WebSocketCloseStatus enum, e.g. WebSocketCloseStatus.NormalClosure is 1000 .
Now handle the close signal yourself. In the queue below, one frame is the literal "close" control message. When you read it, print a closing line and break out of the loop so the frames queued after it are never processed. Fill in the two ___ blanks:
SignalR is built on top of WebSockets. It hands you automatic reconnection, fallback transports (for clients without WebSocket support), message serialisation, and "hubs" with named methods and groups — a lot of plumbing you'd otherwise write by hand.
Reach for SignalR when you're building typical .NET-to-.NET or .NET-to-browser real-time features — chat, notifications, live dashboards — and you want reconnection and groups for free.
Reach for raw WebSockets when you need full control of the wire: a custom binary protocol, a fixed message format a non-.NET client already speaks (a game client, an IoT device, an exchange feed), maximum throughput with minimal overhead, or interop where you can't assume a SignalR client.
Q: How is a WebSocket different from just polling with HTTP?
Polling means the client asks "anything new?" over and over, each time paying full HTTP overhead and learning of changes only on the next poll. A WebSocket keeps one connection open, so the server pushes the instant something happens — lower latency and far less overhead.
Q: Do I have to abandon HTTP to use WebSockets?
No. A WebSocket starts as an HTTP request with an Upgrade header, then switches protocols on the same connection. Your app can serve normal HTTP routes and WebSocket endpoints side by side.
Q: When should I use raw WebSockets instead of SignalR?
Use raw WebSockets when you need a custom or binary protocol, must interoperate with a non-.NET client that speaks a fixed format, or want minimal overhead. Use SignalR when you want reconnection, groups, and transport fallbacks handled for you.
Q: What's a "frame" and why does message framing matter?
A frame is one labelled chunk of WebSocket data (text, binary, or a control frame like Close). A big message can be split across frames, so you must keep reading until EndOfMessage before treating the bytes as a complete message.
1000 is a normal, intentional close. Others signal trouble — 1002 a protocol error, 1009 a message that was too big, 1011 a server error. Sending the right code tells the other side why the line dropped.
No blanks this time — just a brief and an outline. Write the receive loop yourself: reply "pong" to every "ping" , echo any other frame unchanged, and stop the loop when you read "bye" . This is exactly the shape of a real keep-alive protocol running over a WebSocket. Run it and check your output against the expected lines in the comments.
Practice quiz
What does "full-duplex" mean for a WebSocket connection?
- Only the client may send messages
- Messages must alternate strictly request then response
- Both ends can send at the same time, in either direction
- The connection closes after each message
Answer: Both ends can send at the same time, in either direction. Full-duplex means both sides can send whenever they like, like two people on a phone call — unlike HTTP's request/response model.
How does a WebSocket connection begin?
- As a normal HTTP GET carrying an Upgrade: websocket header
- As a UDP datagram
- As a raw TCP socket with no HTTP
- As an FTP transfer
Answer: As a normal HTTP GET carrying an Upgrade: websocket header. A WebSocket starts as an HTTP GET with an Upgrade: websocket header; the server replies 101 Switching Protocols and the same connection becomes a two-way pipe.
In ASP.NET Core, which call completes the upgrade handshake and returns a live WebSocket?
- app.UseWebSockets()
- context.Response.WriteAsync()
- socket.SendAsync()
- context.WebSockets.AcceptWebSocketAsync()
Answer: context.WebSockets.AcceptWebSocketAsync(). UseWebSockets() enables the middleware, but AcceptWebSocketAsync() completes the handshake and hands you the live, full-duplex WebSocket.
Before accepting, what should you check on an incoming request?
- context.Request.IsHttps
- context.WebSockets.IsWebSocketRequest
- socket.State == Open
- result.EndOfMessage
Answer: context.WebSockets.IsWebSocketRequest. Check IsWebSocketRequest; a plain GET is not an upgrade request, so you should return a normal status like 400 rather than calling AcceptWebSocketAsync.
How does WebSocket data travel?
- In frames labelled text, binary, or control (like Close)
- As complete HTTP responses
- Only as UTF-8 strings
- As XML documents
Answer: In frames labelled text, binary, or control (like Close). WebSocket data travels in frames. Each is labelled text (UTF-8), binary (raw bytes), or a control frame such as Close.
In the receive loop, which method reads the next incoming frame?
- socket.SendAsync
- socket.CloseAsync
- socket.ReceiveAsync
- socket.AcceptAsync
Answer: socket.ReceiveAsync. ReceiveAsync reads the next frame into a buffer; SendAsync writes a frame back. The loop runs while socket.State is Open.
What tells you a large message has fully arrived across multiple frames?
- result.MessageType
- result.EndOfMessage is true
- result.Count is zero
- socket.State is Closed
Answer: result.EndOfMessage is true. A big message can span several frames; result.EndOfMessage being true signals the final chunk has arrived so you can treat the bytes as one message.
When the peer sends a Close frame, what should the server do?
- Ignore it and keep reading
- Immediately throw an exception
- Send the frame back unchanged
- Reply with CloseAsync and break out of the receive loop
Answer: Reply with CloseAsync and break out of the receive loop. Honour the Close frame: call CloseAsync with a close code and reason, then break the loop. Ignoring it leaks the connection.
What does WebSocket close code 1000 (NormalClosure) indicate?
- A protocol error
- A normal, intentional clean close
- A message that was too big
- An internal server error
Answer: A normal, intentional clean close. 1000 (NormalClosure) means the connection was closed on purpose in the normal, clean way. Codes like 1002, 1009 and 1011 signal errors.
When should you reach for raw WebSockets instead of SignalR?
- When you want automatic reconnection and groups for free
- For ordinary .NET-to-browser chat
- When you need a custom or binary protocol, or interop with a non-.NET client
- When you want transport fallbacks handled for you
Answer: When you need a custom or binary protocol, or interop with a non-.NET client. SignalR gives reconnection, groups, and fallbacks for typical real-time features. Raw WebSockets are for full wire control: custom/binary protocols, non-.NET clients, or minimal overhead.