Build a TCP Chat App in C
Build a TCP client-server chat application in C using the Winsock API.
Introduction
30 Second Summary
Every time you send a message, stream a video, or load a webpage, two programs are talking to each other across a network. That conversation follows rules and patterns that most developers never see.
In this project, you will build a two-program chat application from scratch in C using the Winsock API. You will learn how networked programs actually communicate at the lowest level by sending messages between two terminal windows in real-time.
What You'll Build
A TCP chat system consisting of a server that listens on a port and a client that connects to it, allowing both sides to send and receive text messages over your local network.
By the end of this project, you'll have:
- A working TCP server in C that accepts a client connection and exchanges messages interactively.
- A working TCP client in C that connects to the server and lets you type messages back and forth.
- A clear mental model of the socket lifecycle and how TCP's connection-oriented design moves data reliably between programs.
- Secret Mission: Upgrade your server to handle multiple clients simultaneously using I/O multiplexing.
Are there any prerequisites?
You should have basic-to-intermediate C knowledge, including pointers, structs, loops, and standard library usage. All networking tools are built into Windows, so you do not need to install anything beyond a C compiler.
Before We Start
Before We Start...
Set Up Your Project
Every networked application on Windows starts with the same ritual: initializing the Winsock API.
In this step, you'll create both source files (server.c and client.c) with the initialization boilerplate, then compile them to confirm your build environment works.
In this step, get ready to:
- Create server.c with Winsock initialization boilerplate.
- Create client.c with the same Winsock boilerplate plus a server address constant.
- Compile both files to verify your build environment is set up correctly.
Create the server file
The Winsock boilerplate handles three things: preventing header conflicts, including the right libraries, and calling WSAStartup to initialize the networking subsystem.
- In your text editor, create a new file called server.c inside your project folder.
- Paste the following code into server.c:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
int main(void)
{
WSADATA wsaData;
int iResult;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
WSACleanup();
return 0;
}
What does this code do?
- WIN32_LEAN_AND_MEAN is defined before windows.h to prevent the older Winsock 1.1 headers from being pulled in. Without it, you get conflicts with the Winsock 2 headers.
- winsock2.h and ws2tcpip.h are the required headers for Winsock 2 TCP/IP programming.
- WSAStartup(MAKEWORD(2, 2), &wsaData) requests version 2.2 of Winsock and initializes the networking library. Every Winsock program must call this before using any socket functions.
- WSACleanup() releases the Winsock resources when the program finishes. For now, this is a placeholder. You'll add the real networking logic between these two calls in the next step.
- Save server.c.
- Compile it by running this command in your terminal (make sure you're inside your project folder):
gcc server.c -o server.exe -lws2_32
If compilation succeeds, you'll see no output and a new server.exe file in your folder.
- Confirm the file was created by running:
dir server.exe
You should see server.exe listed with its file size.
Seeing 'undefined reference to WSAStartup'?
This means the Winsock library isn't linked. Make sure -lws2_32 comes AFTER server.c in the command. GCC processes arguments left to right, so the library flag must follow the source file.
Help me fix my GCC compilation error for Winsock
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file before moving on.
ⓧ I'd like to double check the full code
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
int main(void)
{
WSADATA wsaData;
int iResult;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
WSACleanup();
return 0;
}
Create the client file
The client needs the same Winsock initialization as the server. It also needs a constant for the server's address and the string.h header for comparing user input later.
- Create a new file called client.c in the same project folder.
- Paste the following code into client.c:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <string.h>
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
#define SERVER_ADDR "127.0.0.1"
int main(void)
{
WSADATA wsaData;
int iResult;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
WSACleanup();
return 0;
}
What's different from the server?
- SERVER_ADDR is set to 127.0.0.1 (localhost). This tells the client to connect to a server running on the same machine.
- string.h is included because the client will later use strcmp() to check if the user typed "quit" to exit.
- Save client.c.
- Compile it by running:
gcc client.c -o client.exe -lws2_32
Again, no output means success. You should now have client.exe in your folder alongside server.exe.
- Confirm both executables exist by running:
dir *.exe
You should see both server.exe and client.exe listed.
Compilation failed?
- Make sure you're running the command from inside your project folder where client.c lives.
- Check that -lws2_32 is at the end of the command, not before the source file.
- Verify there are no typos in the #include lines. The angle brackets and header names must match exactly.
Help me fix my client.c compilation error
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file before moving on.
ⓧ I'd like to double check the full code
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <string.h>
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
#define SERVER_ADDR "127.0.0.1"
int main(void)
{
WSADATA wsaData;
int iResult;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
WSACleanup();
return 0;
}
Your build environment is ready and both files compile cleanly. Next up, you'll add the real networking logic to the server: creating a socket, binding it to a port, and listening for incoming connections.
Build the Server
Your project is set up and both files compile cleanly. Now it's time to build the actual server that listens for connections and chats with a client.
A TCP server follows a specific lifecycle: create a socket, bind it to a port, listen for incoming connections, accept one, and then exchange data. In this step, you'll implement each stage of that lifecycle in server.c.
In this step, get ready to:
- Create a listening socket bound to port 27015.
- Accept an incoming client connection.
- Implement a send/receive chat loop.
Set up the listening socket
Before your server can accept connections, it needs to create a socket and bind it to a network address and port. The Winsock API uses getaddrinfo to resolve the address, then socket(), bind(), and listen() to prepare it for incoming connections.
- In server.c, replace everything inside your main() function (between the opening { and closing }) with the following code:
WSADATA wsaData;
int iResult;
SOCKET ListenSocket = INVALID_SOCKET;
SOCKET ClientSocket = INVALID_SOCKET;
struct addrinfo *result = NULL;
struct addrinfo hints;
char recvbuf[DEFAULT_BUFLEN];
int recvbuflen = DEFAULT_BUFLEN;
char sendbuf[DEFAULT_BUFLEN];
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
// Resolve the server address and port
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
// Create a socket for the server to listen on
ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ListenSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
// Bind the socket
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result);
// Listen for incoming connections
iResult = listen(ListenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
printf("listen failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Server is running on port %s\n", DEFAULT_PORT);
printf("Waiting for a client to connect...\n");
What does this code do?
This code sets up the full listening socket lifecycle:
- The hints struct tells getaddrinfo what kind of socket you want: IPv4 (AF_INET), TCP stream (SOCK_STREAM), and AI_PASSIVE so the server listens on all available network interfaces.
- getaddrinfo resolves the local address and port into a structure that socket() and bind() can use.
- bind() attaches the socket to port 27015 so the OS knows to route traffic on that port to your program.
- listen() with SOMAXCONN marks the socket as ready to accept connections, with the maximum backlog the system allows.
You won't see a change yet. The next chunk adds the accept step that completes the connection flow.
Seeing red squiggly lines in your editor?
If your editor shows errors on SOCKET or INVALID_SOCKET, that is normal. The file is incomplete (no return statement yet) so your editor may flag it. You will add the remaining code in the next two chunks.
Help me understand the socket setup errors in my server code.
Accept a client connection
With the socket listening, you need to call accept() to wait for a client to connect. This call blocks (pauses your program) until someone connects.
- In server.c, add the following code directly below the printf("Waiting for a client to connect...\n"); line:
// Accept a client socket
ClientSocket = accept(ListenSocket, NULL, NULL);
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
// No longer need the listening socket
closesocket(ListenSocket);
printf("Client connected! Start chatting.\n\n");
What does this code do?
- accept() blocks until a client connects, then returns a NEW socket (ClientSocket) specifically for talking to that client.
- Once a client is connected, the ListenSocket is no longer needed. Closing it means the server stops accepting new connections and focuses on this single client.
You won't see a change yet. The next chunk adds the chat loop so the server can actually exchange messages.
Add the chat loop
The chat loop is where the real communication happens. The server will wait for a message from the client, print it, prompt you for a reply, and send that reply back.
- In server.c, add the following code directly below the printf("Client connected! Start chatting.\n\n"); line:
// Chat loop
do {
// Receive message from client
iResult = recv(ClientSocket, recvbuf, recvbuflen - 1, 0);
if (iResult > 0) {
recvbuf[iResult] = '\0';
printf("Client: %s", recvbuf);
// Get server reply
printf("You: ");
fgets(sendbuf, DEFAULT_BUFLEN, stdin);
// Send reply to client
iResult = send(ClientSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
} else if (iResult == 0) {
printf("Client disconnected.\n");
} else {
printf("recv failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
} while (iResult > 0);
// Shutdown and cleanup
iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
}
closesocket(ClientSocket);
WSACleanup();
return 0;
What does this code do?
- recv() reads bytes from the client into recvbuf. It returns the number of bytes received, 0 if the client disconnected, or a negative value on error.
- recvbuf[iResult] = '\0' manually null-terminates the received data. recv() does NOT add a null terminator automatically, so without this line you would print garbage characters.
- fgets() reads your typed reply from the terminal, and send() transmits it back to the client.
- shutdown(ClientSocket, SD_SEND) signals to the client that the server is done sending. Then closesocket() and WSACleanup() release all resources.
- Save server.c.
- Compile the server by running:
gcc server.c -o server.exe -lws2_32
- Run the server by running:
server.exe
You should see two lines printed: Server is running on port 27015 followed by Waiting for a client to connect.... The program then pauses, waiting for a client.
- Press Ctrl+C to stop the server for now.
Seeing a compile error?
- If you see undefined reference to errors, make sure -lws2_32 comes AFTER server.c in the command. GCC requires the library flag at the end.
- If you see a syntax error, check that the do { ... } while (iResult > 0); loop has a semicolon after the closing parenthesis.
- If the server prints an address-in-use error when you run it, another process is using port 27015. Wait a moment and try again.
Help me fix my server.c compile or runtime error.
✔️ Awesome, I've got everything!
Great. Double check you have saved server.c and that it compiles without errors.
ⓧ I'd like to double check the full code
Here is the complete server.c at this point:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
int main(void)
{
WSADATA wsaData;
int iResult;
SOCKET ListenSocket = INVALID_SOCKET;
SOCKET ClientSocket = INVALID_SOCKET;
struct addrinfo *result = NULL;
struct addrinfo hints;
char recvbuf[DEFAULT_BUFLEN];
int recvbuflen = DEFAULT_BUFLEN;
char sendbuf[DEFAULT_BUFLEN];
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
// Resolve the server address and port
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
// Create a socket for the server to listen on
ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ListenSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
// Bind the socket
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result);
// Listen for incoming connections
iResult = listen(ListenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
printf("listen failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Server is running on port %s\n", DEFAULT_PORT);
printf("Waiting for a client to connect...\n");
// Accept a client socket
ClientSocket = accept(ListenSocket, NULL, NULL);
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
// No longer need the listening socket
closesocket(ListenSocket);
printf("Client connected! Start chatting.\n\n");
// Chat loop
do {
// Receive message from client
iResult = recv(ClientSocket, recvbuf, recvbuflen - 1, 0);
if (iResult > 0) {
recvbuf[iResult] = '\0';
printf("Client: %s", recvbuf);
// Get server reply
printf("You: ");
fgets(sendbuf, DEFAULT_BUFLEN, stdin);
// Send reply to client
iResult = send(ClientSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
} else if (iResult == 0) {
printf("Client disconnected.\n");
} else {
printf("recv failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
} while (iResult > 0);
// Shutdown and cleanup
iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
}
closesocket(ClientSocket);
WSACleanup();
return 0;
}
Your server is ready and waiting. Next up, you'll build the client that connects to it so you can start chatting between two terminals.
Build the Client
Your server is ready and waiting for connections. Now you need something to connect to it.
The client's job is the mirror image of the server: resolve an address, create a socket, and connect. Once connected, it enters a chat loop where you type messages and see replies from the server in real time.
In this step, get ready to:
- Connect to the server using a socket and the Winsock connect() function.
- Build an interactive chat loop that sends and receives messages.
- Add graceful shutdown and cleanup logic.
Connect to the server
The client needs to find the server's address, create a socket, and call connect() to establish the TCP connection. Unlike the server (which used AF_INET for IPv4 only), the client uses AF_UNSPEC to accept any address family. This lets it try both IPv4 and IPv6 addresses until one succeeds.
- In client.c, replace everything inside the main() function (between the opening { and closing }) with the following connection code:
WSADATA wsaData;
SOCKET ConnectSocket = INVALID_SOCKET;
struct addrinfo *result = NULL;
struct addrinfo *ptr = NULL;
struct addrinfo hints;
char sendbuf[DEFAULT_BUFLEN];
char recvbuf[DEFAULT_BUFLEN];
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
// Resolve the server address and port
iResult = getaddrinfo(SERVER_ADDR, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
// Attempt to connect to an address until one succeeds
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
// Create a socket for connecting to the server
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (ConnectSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// Connect to server
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
continue;
}
break;
}
freeaddrinfo(result);
if (ConnectSocket == INVALID_SOCKET) {
printf("Unable to connect to server!\n");
WSACleanup();
return 1;
}
printf("Connected to server! Start chatting. Type \"quit\" to exit.\n\n");
What does this code do?
This code handles everything needed to establish a connection to the server:
- AF_UNSPEC in the hints tells getaddrinfo() to return both IPv4 and IPv6 addresses. The server might be reachable on either.
- The for loop tries each address returned by getaddrinfo(). If connect() fails on one address, it closes that socket and tries the next.
- Once a connection succeeds, the loop breaks. If all addresses fail, ConnectSocket stays INVALID_SOCKET and the program exits with an error.
Why loop over addresses instead of connecting directly?
A hostname can resolve to multiple addresses (IPv4, IPv6, or both). By looping, your client gracefully handles whichever protocol the server is listening on. This is the same pattern the official Winsock documentation recommends for robust clients.
- Save client.c.
You won't be able to compile yet because the function has no return statement at the end. The next substep adds the chat loop, and then cleanup completes the function.
Add the chat loop
The chat loop is where the interactive conversation happens. The client prompts you for input, sends it to the server, then waits for the server's reply. Typing "quit" ends the session gracefully.
- In client.c, add the following chat loop directly below the printf("Connected to server!... line:
// Chat loop
while (1) {
// Get user input
printf("You: ");
fgets(sendbuf, DEFAULT_BUFLEN, stdin);
// Check if user wants to quit
if (strcmp(sendbuf, "quit\n") == 0) {
printf("Disconnecting...\n");
break;
}
// Send message to server
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
// Receive reply from server
iResult = recv(ConnectSocket, recvbuf, recvbuflen - 1, 0);
if (iResult > 0) {
recvbuf[iResult] = '\0';
printf("Server: %s", recvbuf);
} else if (iResult == 0) {
printf("Server closed the connection.\n");
break;
} else {
printf("recv failed with error: %d\n", WSAGetLastError());
break;
}
}
What does this code do?
- fgets() reads a line from the keyboard (including the newline character). This is why the quit check compares against "quit\n" with the trailing newline.
- send() pushes the typed message to the server over the TCP connection.
- recv() blocks until the server sends a reply. The returned byte count does NOT include a null terminator, so we manually add '\0' before printing.
- If recv() returns 0, the server closed its side of the connection. A negative return means an error occurred.
- Save client.c.
The chat logic is complete. The next substep adds the cleanup code that lets the program compile and exit cleanly.
Add cleanup
After the chat loop ends (either by typing "quit" or a server disconnect), the client needs to shut down the connection and release all Winsock resources.
- In client.c, add the following cleanup code directly below the chat loop's closing }:
// Shutdown and cleanup
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
}
closesocket(ConnectSocket);
WSACleanup();
return 0;
What does this code do?
- shutdown(ConnectSocket, SD_SEND) signals to the server that this client is done sending data. The SD_SEND constant disables the send side while still allowing any final receives.
- closesocket() releases the socket handle back to the operating system.
- WSACleanup() terminates use of the Winsock library, matching the WSAStartup() call at the top.
- Save client.c.
- Compile the client by running:
gcc client.c -o client.exe -lws2_32
- You should see no errors. If it compiles successfully, a new client.exe file appears in your project folder.
Seeing undefined reference errors?
- Make sure -lws2_32 comes after client.c in the command. GCC processes arguments left to right, so the library flag must appear after the source file.
- Check for typos in function names like closesocket (one word, no underscore) and WSACleanup (capital letters matter).
- Verify that #include <string.h> is present at the top of your file. Without it, strcmp and strlen will be undefined.
Why won't my client.c compile with GCC on Windows?
✔️ Awesome, I've got everything!
Great. Double check you've saved client.c and that client.exe compiled without errors.
ⓧ I'd like to double check the full code
Here is the complete client.c file. Compare it against yours:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <string.h>
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
#define SERVER_ADDR "127.0.0.1"
int main(void)
{
WSADATA wsaData;
SOCKET ConnectSocket = INVALID_SOCKET;
struct addrinfo *result = NULL;
struct addrinfo *ptr = NULL;
struct addrinfo hints;
char sendbuf[DEFAULT_BUFLEN];
char recvbuf[DEFAULT_BUFLEN];
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
// Resolve the server address and port
iResult = getaddrinfo(SERVER_ADDR, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
// Attempt to connect to an address until one succeeds
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
// Create a socket for connecting to the server
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (ConnectSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// Connect to server
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
continue;
}
break;
}
freeaddrinfo(result);
if (ConnectSocket == INVALID_SOCKET) {
printf("Unable to connect to server!\n");
WSACleanup();
return 1;
}
printf("Connected to server! Start chatting. Type \"quit\" to exit.\n\n");
// Chat loop
while (1) {
// Get user input
printf("You: ");
fgets(sendbuf, DEFAULT_BUFLEN, stdin);
// Check if user wants to quit
if (strcmp(sendbuf, "quit\n") == 0) {
printf("Disconnecting...\n");
break;
}
// Send message to server
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
// Receive reply from server
iResult = recv(ConnectSocket, recvbuf, recvbuflen - 1, 0);
if (iResult > 0) {
recvbuf[iResult] = '\0';
printf("Server: %s", recvbuf);
} else if (iResult == 0) {
printf("Server closed the connection.\n");
break;
} else {
printf("recv failed with error: %d\n", WSAGetLastError());
break;
}
}
// Shutdown and cleanup
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
}
closesocket(ConnectSocket);
WSACleanup();
return 0;
}
Both sides of the chat are now built. Next up, you'll compile both programs and test a live conversation between two terminal windows.
Test Your Chat Application
Your server and client code are complete. Both programs know how to set up sockets, exchange data, and clean up after themselves.
Now it's time to see TCP in action. You'll compile both programs into executables, then run them side by side in two terminal windows and watch messages flow between them in real-time.
In this step, get ready to:
- Compile server.c and client.c into executables.
- Run the server and client in separate terminal windows.
- Exchange messages and disconnect gracefully.
Compile both programs
Before running your programs, you need to compile them into Windows executables. The -lws2_32 flag links the Winsock library so your code can use networking functions.
- Compile the server by running this command:
gcc server.c -o server.exe -lws2_32
- Compile the client by running this command:
gcc client.c -o client.exe -lws2_32
- Confirm both executables exist by running:
dir *.exe
You should see server.exe and client.exe listed in the output.
Seeing undefined reference errors?
Make sure -lws2_32 comes AFTER the source file in the command. GCC processes arguments left to right, so the library must follow the file that uses it.
If you see syntax errors, double-check your code against the full files from the previous steps.
Help me troubleshoot my compile errors.
Run the server
The server needs to be running and listening before the client can connect to it. You'll start the server first, then open a second terminal for the client.
- In your current terminal, start the server by running:
server.exe
You should see this output, and then the terminal appears to freeze:
Server is running on port 27015
Waiting for a client to connect...
Why does the terminal freeze?
The server is blocking on the accept() call. It's waiting for a client to connect. This is normal behavior for a TCP server. It will stay frozen until a client connects.
Windows Firewall prompt
Windows may show a firewall dialog asking whether to allow server.exe to communicate on the network. Click Allow access to continue. This appears because the server binds to a network port.
Seeing a bind failed error?
This means port 27015 is already in use by another program. Close any previous instances of server.exe that might still be running, then try again.
Help me fix this bind error.
Run the client and chat
With the server waiting for a connection, you'll now open a second terminal and connect to it with the client.
- Open a new terminal window. Leave the server terminal open and running.
- In the new terminal, navigate to the same project folder where your .exe files are located.
- Start the client by running:
client.exe
You should see this in the client terminal:
Connected to server! Start chatting. Type "quit" to exit.
You:
Over in the server terminal, you should see the message change to:
Client connected! Start chatting.
What just happened?
The TCP three-way handshake completed. Your client called connect() and the server's accept() unblocked. Both programs are now connected through a reliable byte stream.
- In the client terminal, type Hello server! and press Enter.
- Switch to the server terminal. You should see the message appear prefixed with Client: and a You: prompt waiting for your reply.
- Type a reply in the server terminal, such as Hey client! and press Enter.
- Switch back to the client terminal. You should see the server's reply prefixed with Server: and a new You: prompt ready for your next message.
Keep sending messages back and forth. Each message you type in one window appears in the other.
- When you're done chatting, type quit in the client terminal and press Enter.
The client prints Disconnecting... and exits. The server prints Client disconnected. and also exits.
Why does the server also exit?
This single-client server is designed to handle one conversation and then shut down. When the client disconnects, recv() returns 0, which signals that the other side closed the connection. The do...while loop exits and the server runs its cleanup code.
In the Secret Mission, you'll upgrade the server to keep running and handle multiple clients at once.
Client says Unable to connect to server?
Make sure server.exe is still running in the other terminal. The server must be listening before the client tries to connect.
If you closed the server accidentally, stop the client with Ctrl+C, restart the server first, then run the client again.
Help me debug this connection issue.
Secret mission
Handle Multiple Clients with select()
Your server can only talk to one client at a time. Real chat servers handle dozens of connections simultaneously. Upgrade your server to use I/O multiplexing with select() so multiple clients can connect and chat with each other at the same time.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future reference or delete them entirely.
Resources you used:
- Project folder on your Desktop containing server.c, client.c, server.exe, and client.exe.
Keep everything running
No action needed. Choose this if you want to keep experimenting with your chat application or use it as a reference for future networking projects.
- Your source files and executables will stay on your Desktop until you manually delete them.
- GCC (MinGW) remains installed on your system for future C projects.
Pause - I'll come back to this later
Since this project has no running processes or background services, pausing is the same as keeping. Your files sit idle on disk until you need them.
- If server.exe is still running in a terminal window, press Ctrl+C to stop it.
- Close any terminal windows you opened for testing.
- Your project folder remains on the Desktop ready for when you return.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes your source code and compiled executables permanently.
- If server.exe is still running, press Ctrl+C in its terminal window to stop it.
- Delete the project folder from your Desktop by running this command in a new terminal window:
cd %USERPROFILE%\Desktop
rmdir /s /q tcp-chat
Folder name different?
Replace tcp-chat with whatever you named your project folder in Step 1.
- Confirm the folder is gone by checking your Desktop. You should no longer see the project folder listed.
Nice Work!
Nice Work!
You just built a networked chat application from scratch in C. Two programs on your machine are now talking to each other over TCP, sending and receiving bytes through the Winsock API.
You've learned how to:
- Build a TCP server that creates a socket, binds to a port, listens for incoming connections, and accepts clients.
- Build a TCP client that resolves the server address, connects, and exchanges messages interactively in real-time.
- Navigate the complete socket lifecycle: WSAStartup, getaddrinfo, socket, bind, listen, accept, connect, send, recv, shutdown, closesocket, and WSACleanup.
- Upgraded the server to handle multiple simultaneous clients using select() for I/O multiplexing.
Ready to quiz yourself?