In this tutorial, we will explore how to create a chat room using the C programming language. We will start by implementing the server, which will allow multiple clients to connect and exchange messages in real time. By following this step-by-step guide, you will gain a better understanding of networking in C and learn how to build a simple chat application.
Full code can be found at https://github.com/lets-learn-it/c-learning/tree/master/16-networking/02-chat-room-PROJECT
Setting up server
The server will handle client connections, message reception, and message broadcasting.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/time.h>
#include <errno.h>
#define MAX_CLIENTS 4
int main(int argc, char const *argv[]) {
int mastersockfd, connfds[MAX_CLIENTS];
int activeconnections = 0;
// Rest of the code...
}
The code starts by including necessary header files and defining the maximum number of clients (MAX_CLIENTS
) that can connect to the server. We are initializing mastersockfd
which will listen for new connections and connfds
is an array of all live TCP connections.
We will create a socket using the AF_INET
address family (IPv4) and SOCK_STREAM
protocol (TCP). It returns a file descriptor for the socket. If the socket creation fails, an error message is printed, and the program exits.
if((mastersockfd = socket(AF_INET, SOCK_STREAM, 0)) <= 0) {
perror("error while creating socket...");
exit(1);
}
Next, we configure the server's address using the sockaddr_in
structure. We set the address family, and port number (8081 in this example), and bind it to any available network interface (INADDR_ANY
).
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8081);
serv_addr.sin_addr.s_addr = INADDR_ANY;
To ensure that the socket can be reused immediately after the server is stopped, we set the SO_REUSEADDR
option to use the setsockopt
function. More can be found on https://stackoverflow.com/questions/2208581/socket-listen-doesnt-unbind-in-c-under-linux
int opt = 1;
setsockopt(mastersockfd, SOL_SOCKET, SO_REUSEADDR, (void *) &opt, sizeof(opt));
The bind
function binds the socket to the server's address. If the binding fails, an error message is printed, and the program exits.
if(bind(mastersockfd, (struct sockaddr *) &serv_addr, addrlen) < 0) {
perror("bind failed...");
exit(1);
}
We use the listen
function to listen for incoming client connections on the socket. The second argument (3 in this case) specifies the maximum number of pending connections that can be queued.
if(listen(mastersockfd, 3) < 0) {
perror("Listen failed ...");
exit(1);
}
Asynchronous I/O Multiplexing
As we will be dealing with multiple clients at the same time, How can we know which connection is ready with new data? We want to be notified when the connection is ready with new data or ready for reading. This capability is called I/O multiplexing. and is provided by select
and poll
functions. More on this can be read at https://notes.shichao.io/unp/ch6/.
We will be using select
for working with asynchronous I/O multiplexing. To begin with, we need to define the necessary variables. The fd_set
structure is used to hold the file descriptors. We declare readfds
as an fd_set
to keep track of the file descriptors available for reading. We also define variables max_fd
and readyfds
.
fd_set readfds;
int max_fd, readyfds;
In infinite while
loop, we will continuously check for incoming connections and messages from clients. At the start of each iteration, we clear the readfds
set using FD_ZERO
to remove any previously set file descriptors. Next, we add the master socket (mastersockfd
) to the readfds
set using FD_SET
. The master socket is the socket that listens for new client connections. We also update the max_fd
variable with the maximum file descriptor value. We iterate through the array of active client sockets (connfds
) and add each valid socket to the readfds
set using FD_SET
. We also update the max_fd
value if necessary.
FD_ZERO(&readfds);
// add mastersockfd
FD_SET(mastersockfd, &readfds);
max_fd = mastersockfd;
for(int i=0;i < activeconnections;i++) {
if(connfds[i] != 0)
FD_SET(connfds[i], &readfds);
if(connfds[i] > max_fd)
max_fd = connfds[i];
}
We call the select
function to wait for activity on the file descriptors. It monitors the file descriptors specified in the readfds
set and blocks until any of them become available for reading. The max_fd + 1
parameter ensures that select
checks all file descriptors from 0 to max_fd
. If the select
call returns a negative value (readyfds < 0
) and the error is not due to an interrupted system call (errno != EINTR
), an error message is displayed.
readyfds = select(max_fd + 1, &readfds, NULL, NULL, NULL);
if ((readyfds < 0) && (errno!=EINTR)) {
printf("select error");
}
We check if the master socket is part of the readfds
set using FD_ISSET
. If so, it means a new client is attempting to connect. We accept the connection using the accept
function and store the new socket descriptor in the connfds
array. We also print the client's IP address using inet_ntoa
and increment the activeconnections
counter.
if(FD_ISSET(mastersockfd, &readfds)) {
if((connfds[activeconnections] = accept(mastersockfd, (struct sockaddr *) &clientIPs[activeconnections], (socklen_t *) &addrlen)) < 0) {
perror("accept error...");
exit(1);
}
fprintf(stdout, "New connection from %s\n", inet_ntoa(clientIPs[activeconnections].sin_addr));
activeconnections++;
}
For each active client socket, we check if it is part of the readfds
set using FD_ISSET
. If the socket is ready for reading, we clear the input and output buffers using memset
. We read data from the client using the read
function. If the return value is 0, it means the connection was closed normally, and if it is -1, an error occurred. We handle these cases by printing an error message, marking the connection as closed, and closing the socket. We then continue to the next iteration of the loop.
If the read operation is successful, we retrieve the client's IP address from clientIPs
using inet_ntoa
and store it in the output buffer. We also print the client's IP address and the received message to the console. We concatenate the client's IP address and the message into the output buffer. Then, we iterate through all active connections and write the message to each client except the sender, using the write
function.
for(int i=0;i < activeconnections; i++) {
// check if connection is active and it is ready to read
if(connfds[i] != 0 && FD_ISSET(connfds[i], &readfds)) {
// clear buffer
memset(inBuffer[i], 0, 1024);
memset(outBuffer[i], 0, 1024);
// read returns 0 if connection closed normally
// and -1 if error
if(read(connfds[i], inBuffer[i], 1024) <= 0) {
fprintf(stderr, "%s (code: %d)\n", strerror(errno), errno);
strncpy(outBuffer[i], inet_ntoa(clientIPs[i].sin_addr), INET_ADDRSTRLEN);
fprintf(stderr, "Host %s disconnected\n", outBuffer[i]);
close(connfds[i]);
connfds[i] = 0;
continue;
}
// get client ip
strncpy(outBuffer[i], inet_ntoa(clientIPs[i].sin_addr), INET_ADDRSTRLEN);
fprintf(stdout, "%s: %s", outBuffer[i], inBuffer[i]);
strcat(outBuffer[i], " : ");
strcat(outBuffer[i], inBuffer[i]);
for(int j=0;j<activeconnections;j++) {
if(connfds[j] != 0 && i != j) {
write(connfds[j], outBuffer[i], strlen(outBuffer[i]));
}
}
}
}
Setting up client
Firstly, we define a function called readline
that reads input from the user until a specified end-of-character is encountered. This function will be used to read input from the user and send it to the server.
int readline(char *buffer, int maxchars, char eoc) {
int n = 0;
while(n < maxchars) {
buffer[n] = getc(stdin);
if(buffer[n] == eoc)
break;
n++;
}
return n;
}
Moving on to the main function, we declare some variables and initialize them. sockfd
represents the socket file descriptor, serv_addr
is a structure that holds the server's address information, and sendline
and recvline
are character arrays to store the messages to be sent and received, respectively.
int sockfd;
struct sockaddr_in serv_addr;
char sendline[1024], recvline[1024];
We set up the server address by assigning the address family (AF_INET
) and the port number (8081
) to the serv_addr
structure. Additionally, we convert the server IP address from a string format to binary using the inet_pton
function. If the conversion fails, an error message is displayed, and the program exits.
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8081);
if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) {
perror("addre");
exit(-1);
}
To establish a connection with the server, we use the connect
function. It takes the socket file descriptor (sockfd
), the server address structure, and its size as arguments. If the connection fails, an error message is printed, and the program exits.
if(connect(sockfd, (struct sockaddr *) &serv_addr, sizeof serv_addr) < 0) {
perror("connect error...");
exit(1);
}
We set up a loop that continuously listens for input from the user and messages from the server. The fd_set
data structure is used to monitor the file descriptors for activity. In our case, we monitor the user input (0
) and the socket (sockfd
) for readability.
Within the loop, we call select
to check for any active file descriptors. If select
returns a value less than 0
and the error is not due to interruption (EINTR
), an error message is printed.
If the user input (stdin
) is ready for reading, we call the readline
function to read the input from the user and store it in the sendline
buffer. Then, we use the write
function to send the input to the server.
If the socket is ready for reading, we use the read
function to receive data from the server and store it in the recvline
buffer. Finally, we print the received data to the standard output using fprintf
.
fd_set waitfds;
int readyfds;
while(1) {
FD_ZERO(&waitfds);
// add mastersockfd
FD_SET(sockfd, &waitfds);
FD_SET(0, &waitfds);
memset(recvline, 0, 1024);
memset(sendline, 0, 1024);
// connfd will be always largest
readyfds = select(sockfd + 1, &waitfds, NULL, NULL, NULL);
if ((readyfds < 0) && (errno!=EINTR)) {
printf("select error");
}
// if stdin ready, read it and send
if(FD_ISSET(0, &waitfds)) {
readline(sendline, 1024, '\n');
write(sockfd, sendline, strlen(sendline));
}
// if socket ready, read it and print
if(FD_ISSET(sockfd, &waitfds)) {
read(sockfd, recvline, 1024);
fprintf(stdout, "%s", recvline);
}
}