In the previous article, you successfully built your first TCP echo client and server. That was a monumental step, demonstrating the fundamental ability to establish a connection and exchange a single message. But real-world applications, like chat programs, require continuous, two-way communication.
In this article, we will evolve our echo program into a simple, console-based chat application. This project will reinforce your understanding of the TCP client-server model and introduce techniques for handling persistent connections and continuous data exchange.
Our objective is to create a basic chat application where:
A server listens for a single client connection.
Once connected, both the client and the server can type messages, which are then sent to and displayed by the other party.
The conversation continues in a loop until one party types a "quit" or "exit" command.
For this project, we will keep it simple: the server handles only one client at a time. Multi-client handling will be covered in a later article.
Recall the structure of our echo programs:
Server: socket() -> bind() -> listen() -> accept() -> recv() -> send() -> close()
Client: socket() -> connect() -> send() -> recv() -> close()
The key limitation was that after one `recv()` and one `send()`, the connection would effectively complete its purpose and the server would shut down (or wait for a new connection). For a chat application, we need both sides to continuously send and receive messages. This means wrapping our `send()` and `recv()` calls inside loops.
The server's role is to:
Set up the listening socket and accept a client connection (as before).
Once connected, enter a loop to continuously:
Receive messages from the client.
Display the received messages.
Prompt the server user for input.
Send the server user's message back to the client.
Gracefully shut down when a "quit" command is received.
The client's role is to:
Create its socket and connect to the server (as before).
Once connected, enter a loop to continuously:
Prompt the client user for input.
Send the client user's message to the server.
Receive messages from the server.
Display the received messages.
Gracefully shut down when a "quit" command is received.
The `send()` and `recv()` functions we are using are blocking I/O calls. This means:
`send()` will block (pause your program) until all (or at least some) of the data you want to send has been written to the network buffer.
`recv()` will block until some data is available to be read from the network or the connection is closed/errors occur.
This blocking behavior is convenient for simple sequential communication but becomes a challenge when you need to do multiple things simultaneously (e.g., both send and receive at the same time in a single thread). For now, our loops will alternate between sending and receiving. In a later article (on concurrency), we will explore ways to manage this more sophisticatedly.
For reading user input from the console, `scanf()` can be problematic with strings containing spaces or buffer overflows. `fgets()` is a safer alternative:
char input_buffer[BUFFER_SIZE];
printf("Enter message: ");
fgets(input_buffer, BUFFER_SIZE, stdin);
// Remove trailing newline character if present
input_buffer[strcspn(input_buffer, "\n")] = 0;
`fgets()` reads up to `BUFFER_SIZE - 1` characters or until a newline, including the newline character itself. `strcspn()` can be used to remove this newline for cleaner messaging.
When sending strings over sockets, TCP is a byte stream. It doesn't inherently know where one "message" ends and another begins. By convention, C strings are null-terminated. So, when sending, we send `strlen(message)` bytes, and when receiving, we ensure the received buffer is null-terminated before printing or processing it. This allows the receiver to treat the incoming bytes as a standard C string.
Remember to always allocate `BUFFER_SIZE` and read `BUFFER_SIZE - 1` bytes into it for `recv` to leave space for the null terminator.
Choosing an appropriate `BUFFER_SIZE` is important. Too small, and you might send/receive many small packets, increasing overhead. Too large, and you waste memory. For simple chat, 1024 bytes is usually sufficient.
Pay close attention to the return values of `send()` and `recv()`.
A return value of `0` from `recv()` indicates that the peer (the other end of the connection) has gracefully shut down or closed its writing half. This is your signal to also close the socket.
A return value of `-1` (or `SOCKET_ERROR` on Windows) indicates an actual error, and you should use `perror()` or `WSAGetLastError()` to find out why. Common errors include `ECONNRESET` (connection reset by peer) if the other side abruptly closes.
Here are the complete code listings for our chat server and client.
chat_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- Platform-specific includes and definitions ---
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib") // Link with ws2_32.lib
#define CLOSE_SOCKET closesocket
#define GET_LAST_ERROR WSAGetLastError
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h> // For close
#include <errno.h> // For errno
#define CLOSE_SOCKET close
#define GET_LAST_ERROR() errno
#endif
// ----------------------------------------------------
#define PORT 12345
#define BUFFER_SIZE 1024
int main() {
// --- Windows-specific Winsock initialization ---
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
return 1;
}
#endif
// ----------------------------------------------------
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
char message[BUFFER_SIZE] = {0}; // Buffer for server's outgoing message
// 1. Create socket file descriptor
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) ==
#ifdef _WIN32
INVALID_SOCKET
#else
0
#endif
) {
fprintf(stderr, "socket failed with error: %d\n", GET_LAST_ERROR());
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// Optional: Set socket option to reuse address
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) < 0) {
perror("setsockopt failed"); // Use perror for Linux errors
CLOSE_SOCKET(server_fd);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// Prepare the sockaddr_in structure for binding
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // Listen on all available interfaces
address.sin_port = htons(PORT); // Convert port to network byte order
// 2. Bind the socket to the specified IP and port
if (bind(server_fd, (struct sockaddr *)&address, addrlen) ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "bind failed with error: %d\n", GET_LAST_ERROR());
CLOSE_SOCKET(server_fd);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// 3. Listen for incoming connections (max 3 pending connections)
if (listen(server_fd, 3) < 0) {
fprintf(stderr, "listen failed with error: %d\n", GET_LAST_ERROR());
CLOSE_SOCKET(server_fd);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. Accept an incoming connection (blocking call)
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) ==
#ifdef _WIN32
INVALID_SOCKET
#else
-1
#endif
) {
fprintf(stderr, "accept failed with error: %d\n", GET_LAST_ERROR());
CLOSE_SOCKET(server_fd);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
printf("Connection accepted from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
printf("Type 'quit' or 'exit' to end the chat.\n");
// Main chat loop
while (1) {
// --- Receive message from client ---
int valread = recv(new_socket, buffer, BUFFER_SIZE - 1, 0); // -1 for null termination space
if (valread > 0) {
buffer[valread] = '\0'; // Null-terminate the received data
printf("Client: %s\n", buffer);
// Check for client's exit command
if (strcmp(buffer, "quit") == 0 || strcmp(buffer, "exit") == 0) {
printf("Client requested to end chat. Closing connection.\n");
break;
}
} else if (valread == 0) {
printf("Client disconnected.\n");
break;
} else {
fprintf(stderr, "recv failed with error: %d\n", GET_LAST_ERROR());
break;
}
// --- Send message to client (from server's console input) ---
printf("Server: ");
if (fgets(message, BUFFER_SIZE, stdin) == NULL) {
fprintf(stderr, "Error reading input.\n");
break;
}
message[strcspn(message, "\n")] = 0; // Remove trailing newline
// Check for server's exit command
if (strcmp(message, "quit") == 0 || strcmp(message, "exit") == 0) {
printf("Server requested to end chat. Closing connection.\n");
send(new_socket, message, strlen(message), 0); // Send quit command to client
break;
}
// Send message
if (send(new_socket, message, strlen(message), 0) < 0) {
fprintf(stderr, "send failed with error: %d\n", GET_LAST_ERROR());
break;
}
}
// Close sockets
CLOSE_SOCKET(new_socket);
printf("Client socket closed.\n");
CLOSE_SOCKET(server_fd);
printf("Server listening socket closed.\n");
// --- Windows-specific Winsock cleanup ---
#ifdef _WIN32
WSACleanup();
#endif
// -----------------------------------------
return 0;
}
chat_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- Platform-specific includes and definitions ---
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define CLOSE_SOCKET closesocket
#define GET_LAST_ERROR WSAGetLastError
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define CLOSE_SOCKET close
#define GET_LAST_ERROR() errno
#endif
// ----------------------------------------------------
#define PORT 12345
#define SERVER_IP "127.0.0.1" // Loopback address for local testing
#define BUFFER_SIZE 1024
int main() {
// --- Windows-specific Winsock initialization ---
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
return 1;
}
#endif
// ----------------------------------------------------
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
char message[BUFFER_SIZE] = {0}; // Buffer for client's outgoing message
// 1. Create socket file descriptor
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) ==
#ifdef _WIN32
INVALID_SOCKET
#else
0
#endif
) {
fprintf(stderr, "socket creation failed with error: %d\n", GET_LAST_ERROR());
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// Prepare the server address structure
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// Convert IPv4 address from text to binary form
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
fprintf(stderr, "Invalid address/ Address not supported\n");
CLOSE_SOCKET(sock);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// 2. Connect to the server
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "connection failed with error: %d\n", GET_LAST_ERROR());
CLOSE_SOCKET(sock);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
printf("Connected to server %s:%d\n", SERVER_IP, PORT);
printf("Type 'quit' or 'exit' to end the chat.\n");
// Main chat loop
while (1) {
// --- Send message to server (from client's console input) ---
printf("Client: ");
if (fgets(message, BUFFER_SIZE, stdin) == NULL) {
fprintf(stderr, "Error reading input.\n");
break;
}
message[strcspn(message, "\n")] = 0; // Remove trailing newline
// Check for client's exit command
if (strcmp(message, "quit") == 0 || strcmp(message, "exit") == 0) {
printf("Client requested to end chat. Closing connection.\n");
send(sock, message, strlen(message), 0); // Send quit command to server
break;
}
// Send message
if (send(sock, message, strlen(message), 0) < 0) {
fprintf(stderr, "send failed with error: %d\n", GET_LAST_ERROR());
break;
}
// --- Receive message from server ---
int valread = recv(sock, buffer, BUFFER_SIZE - 1, 0); // -1 for null termination space
if (valread > 0) {
buffer[valread] = '\0'; // Null-terminate the received data
printf("Server: %s\n", buffer);
// Check for server's exit command
if (strcmp(buffer, "quit") == 0 || strcmp(buffer, "exit") == 0) {
printf("Server requested to end chat. Closing connection.\n");
break;
}
} else if (valread == 0) {
printf("Server disconnected.\n");
break;
} else {
fprintf(stderr, "recv failed with error: %d\n", GET_LAST_ERROR());
break;
}
}
// Close socket
CLOSE_SOCKET(sock);
printf("Client socket closed.\n");
// --- Windows-specific Winsock cleanup ---
#ifdef _WIN32
WSACleanup();
#endif
// -----------------------------------------
return 0;
}
To run your chat application:
Compile both programs:
Linux/WSL:
gcc chat_server.c -o chat_server
gcc chat_client.c -o chat_client
Windows (MinGW-w64):
gcc chat_server.c -o chat_server.exe -lws2_32
gcc chat_client.c -o chat_client.exe -lws2_32
Open two separate terminal windows:
In the first terminal, run the server:
# Linux/WSL
./chat_server
# Windows
chat_server.exe
You should see: `Server listening on port 12345...`
In the second terminal, run the client:
# Linux/WSL
./chat_client
# Windows
chat_client.exe
You should see: `Connected to server 127.0.0.1:12345`
Start Chatting!
Now, type messages into the "Client:" prompt in the client terminal, press Enter, and see them appear on the server's terminal. Then, type a response into the "Server:" prompt in the server terminal, and it will appear on the client's terminal.
To end the chat, type `quit` or `exit` on either the client or server side. Both applications should then shut down gracefully.
Example interaction:
Server Terminal:
Server listening on port 12345...
Connection accepted from 127.0.0.1:12345
Type 'quit' or 'exit' to end the chat.
Client: Hello Server!
Server: Hi Client, how are you?
Client: I'm great!
Server: quit
Server requested to end chat. Closing connection.
Client socket closed.
Server listening socket closed.
Client Terminal:
Connected to server 127.0.0.1:12345
Type 'quit' or 'exit' to end the chat.
Client: Hello Server!
Message sent: Hello Server!
Server: Hi Client, how are you?
Client: I'm great!
Message sent: I'm great!
Server: Server requested to end chat. Closing connection.
Client socket closed.
Congratulations! You have successfully built a simple interactive chat application using TCP sockets in C. This project solidifies your understanding of continuous communication, user input handling, and the two-way nature of TCP connections.
While this chat application works, it only handles one client at a time. In the next article, we will explore UDP, a different kind of protocol, and then in Article 5, we will tackle the exciting challenge of building a server that can handle multiple clients concurrently!
How to move your Email accounts from one hosting provider to another without losing any mails?
How to resolve the issue of receiving same email message multiple times when using Outlook?
Self Referential Data Structure in C - create a singly linked list
Mosquito Demystified - interesting facts about mosquitoes
Elements of the C Language - Identifiers, Keywords, Data types and Data objects
How to pass Structure as a parameter to a function in C?
Rajeev Kumar is the primary author of How2Lab. He is a B.Tech. from IIT Kanpur with several years of experience in IT education and Software development. He has taught a wide spectrum of people including fresh young talents, students of premier engineering colleges & management institutes, and IT professionals.
Rajeev has founded Computer Solutions & Web Services Worldwide. He has hands-on experience of building variety of websites and business applications, that include - SaaS based erp & e-commerce systems, and cloud deployed operations management software for health-care, manufacturing and other industries.