C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
212: Socket Programming Fundamentals in C
I've seen this exact bug in countless code reviews: a developer creates a struct containing some data, calls send() once to push that struct over the wire, and then calls recv() once on the other end, expecting the exact same struct to pop out. When it works on their local machine (localhost), they think they've mastered networking. Then they deploy it to a real network with latency and packet loss, and suddenly the program crashes or reads garbage data. They assume send() and recv() act like atomic "message" deliveries.
The Myth: "send()" and "recv()" Handle Whole Messages
The misconception is that TCP is a packet-based protocol where one send() call equals one recv() call. It's a tempting way to think about it because it's how high-level APIs often feel. Let's look at why this is dangerous. Imagine you're sending a 1KB status report from a remote sensor:
struct StatusReport {
int sensor_id;
float temperature;
char message[1012];
};
// The "Naive" approach
struct StatusReport report = {1, 22.5, "All systems nominal"};
send(sockfd, &report, sizeof(report), 0);
You might think the receiver just needs to call recv(sockfd, &buffer, sizeof(buffer), 0) once. But TCP is a stream protocol. It doesn't care about your struct boundaries. Depending on network congestion or OS buffer sizes, the receiver might get the first 10 bytes, then 200 bytes, then the rest. If you only call recv() once, you've only read a fragment of your data, and your program will try to interpret a half-finished struct as valid data. That's a recipe for a segfault.
The Reality: TCP is a Continuous Stream of Bytes
To get your data reliably, you have to stop thinking about "messages" and start thinking about "streams." You are essentially reading from a pipe that never tells you where one logical record ends and the next begins. I always tell my mentees: never assume a single recv() call returns everything you sent.
The fix is simple but mandatory: you must wrap your recv() call in a loop. You keep reading until you've received the exact number of bytes you expect. I usually write a small helper function for this. It looks something like this:
ssize_t read_full(int fd, void *buf, size_t len) {
size_t total_read = 0;
char *ptr = (char *)buf;
while (total_read < len) {
ssize_t n = recv(fd, ptr + total_read, len - total_read, 0);
if (n <= 0) return n; // Error or connection closed
total_read += n;
}
return total_read;
}
Orchestrating the Connection Handshake
Before you can stream bytes, you need a connection. If you're writing the server, you aren't just "opening a file." You're setting up a listening post. I like to think of this as a four-stage process: socket(), bind(), listen(), and accept().
- socket(): You tell the OS, "I want a communication endpoint." For TCP, you'll use
SOCK_STREAM. - bind(): You claim a specific port (like 8080). Without this, the OS doesn't know which incoming packets belong to your program.
- listen(): You put the socket in "passive mode," telling the OS to queue up incoming connection requests.
- accept(): This is the blocking call. Your program pauses here until a client actually connects. Crucially,
accept()returns a new socket file descriptor specifically for that one client, leaving the original listening socket free to wait for more people.
Defining a Protocol to Prevent Data Fragmentation
Since TCP is a stream, how does the receiver know how many bytes to wait for in the first place? You can't just call read_full() if you don't know the length of the coming message. This is where you define a simple application-layer protocol.
The most common pattern I use is the "Length Header." Before sending the actual data, you send a fixed-size integer (usually 4 bytes) that specifies the length of the payload. The receiver reads exactly 4 bytes first, parses the length, and then calls read_full() for that specific amount. It's a bit of extra overhead, but it's the only way to ensure your data doesn't bleed into the next message in the stream.
📋 Practical Task
Build a TCP-based System Uptime Reporter
Your task is to create two programs: a Server (the reporter) and a Client (the monitor).
The Requirements:
- The Server must listen on port 9000.
- When a client connects, the server should gather the system uptime (you can use a hardcoded string or read
/proc/uptimeon Linux) and send it to the client. - Crucially: You must implement the "Length Header" protocol. The server should first send the length of the uptime string as a 4-byte integer, followed by the string itself.
- The Client must connect to the server, read the 4-byte length header, and then use a loop to ensure it receives the entire string before printing it to the console.
- The server should close the connection immediately after sending the report.
Testing your work: Try sending a very long string (e.g., 5000 characters) as the uptime report to verify that your client-side loop is actually handling fragmented data correctly and not just getting lucky with small packets.
There are no comments for now.