C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
132: Building a Basic Chat Client with TCP
When you first start diving into network programming, it's incredibly tempting to think of a TCP connection as a series of distinct "messages." You imagine that if the server calls Send("Hello!"), the client will magically trigger a Receive() event that hands over exactly the string "Hello!".
The Myth of the "Message Packet" vs. The Reality of the Byte Stream
Here is the cold, hard truth: TCP has no concept of a "message." It is a stream. I like to describe it as a water pipe. If the server pours a gallon of water (data) into the pipe, the client doesn't necessarily receive it as one gallon. They might receive it as ten small splashes, or one giant wave. If you send three short chat messages in rapid succession, the client might read all three of them in a single Read() call, smashed together into one long string.
If you just call stream.Read() into a buffer, you'll eventually run into a bug where your chat messages merge together, and you'll be scratching your head wondering why "Hi" and "How are you?" became "HiHow are you?". To fix this, we have to implement our own "framing." The simplest way for a chat client is to use a delimiter—like a newline character (\n)—to signal where one message ends and the next begins.
Using StreamReader and StreamWriter to Handle Framing
While you can manually manage byte arrays, that's a recipe for headaches. In C#, we wrap the NetworkStream in a StreamReader and StreamWriter. These classes allow us to use ReadLine() and WriteLine(), which handle the delimiter logic for us. It turns that raw stream of bytes back into something that feels like a conversation.
using System.Net.Sockets;
using System.IO;
// Connect to the server
TcpClient client = new TcpClient("127.0.0.1", 8080);
NetworkStream stream = client.GetStream();
StreamReader reader = new StreamReader(stream);
StreamWriter writer = new StreamWriter(stream) { AutoFlush = true };
// Sending a message is now intuitive
writer.WriteLine("Hey server, I'm online!");
// Reading a message waits until a newline character is encountered
string response = reader.ReadLine();
Console.WriteLine($"Server says: {response}");
Notice that AutoFlush = true. I can't stress this enough: without flushing, the StreamWriter will buffer your text internally to save performance, and your message will just sit in memory instead of actually traveling across the wire. Your client will be waiting for a response to a message the server never even received.
Solving the "Blocking Read" with Asynchronous Tasks
If you run the code above in a simple loop, you'll hit a wall. reader.ReadLine() is a blocking call. It stops everything. Your program will just sit there, frozen, waiting for the server to speak. You won't be able to type a message to send because the thread is stuck waiting to receive.
To build a real chat client, you need to do two things at once: listen for incoming messages and wait for user input. We handle this by offloading the "listener" to a separate Task. This allows the main thread to stay open for Console.ReadLine() while the background task handles the network traffic.
// Start a background task to constantly listen for messages
Task.Run(() =>
{
try
{
while (true)
{
string message = reader.ReadLine();
if (message == null) break; // Server disconnected
Console.WriteLine($"\n[Incoming]: {message}");
Console.Write("> "); // Keep the prompt visible
}
}
catch (IOException)
{
Console.WriteLine("Disconnected from server.");
}
});
// The main thread stays here, letting the user type
while (true)
{
Console.Write("> ");
string input = Console.ReadLine();
if (string.IsNullOrEmpty(input)) continue;
writer.WriteLine(input);
}
I've added a little \n[Incoming] and > logic there. It's a bit clunky in a console app—your typing might get interrupted by an incoming message—but it's the fundamental pattern for any bidirectional communication tool.
📋 Practical Task
Exercise: Implementing a Chat Client with Username Prefixing
Your goal is to modify the basic TCP client logic to support a "Session Identity." Instead of just sending raw text, the client should prompt the user for a nickname upon startup and automatically prefix every outgoing message with that nickname.
Requirements:
- When the program starts, use
Console.ReadLine()to ask the user: "Enter your nickname: ". - Store this nickname in a variable.
- Modify the sending loop so that if the user types "Hello", the
StreamWriteractually sends"[Nickname]: Hello"to the server. - Ensure the background listening Task remains active so the user can still see incoming messages from other clients while they are typing.
- Handle the scenario where the server closes the connection gracefully by exiting the program.
There are no comments for now.