- Implement a server
- Use nc (netcat) to test communication between clients connected to it
- ???
- Profit!
TLDR: This exam tests your knowledge of FD_SET (sockets) and sockaddr_in structure - binding, listening, accepting, receiving and sending data. It's about establishing TCP connections using IPv4.
NEW TO SOCKETS? Don't worry, a socket is simply a way to give clients their own "parking place" in your server. Think of it as a File Descriptor (like the ones you use with
write()
in C).
Alternatively, sockets are like IDs with these extra steps:
- Special Creation - Different sockets have different standards (address family: TCP/UDP and IP type: IPv4/IPv6)
- Binding socket to a specific IP Address and port
- ...
- (The rest is the "standard server package" handled server-side and client-side: listen, connect, close, send, etc.)
TCP uses stream-like connections - thus the constant is SOCK_STREAM
Address Family is AF_INET for IPv4 (AF_INET6 for IPv6)
- fd_set: Data structure for managing file descriptors (sockets). You don't directly manipulate
fd_set
- instead use these macros:
FD_ZERO(&set)
: Clears all file descriptors from the setFD_SET(fd, &set)
: Adds a file descriptor to the setFD_CLR(fd, &set)
: Removes a file descriptor from the setFD_ISSET(fd, &set)
: Checks if a file descriptor is in the set
- struct sockaddr_in: Imported structure that stores address family, port, and IPv4 address ... continue. You must learn how to configure these three attributes!
We'll define attribute
len
to STORE sizeof sockaddr_in!! (helps withaccept()
)
It must be stored in the SPECIAL typesocklen_t
!! The len MUST BE STORED in SOCKLEN_T
- struct sockaddr: Contains ONLY the family (
sa_family
) and address data. We use it to typecast serveraddress only! Allows correctlyaccept()
andbind()
the serveraddress - accept and bind don't work with sockaddr_in
💡 check out the code now to feel less lost
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
// Define a structure to store client information
typedef struct s_client
{
int id;
char msg[100000];
} t_client;
// Global variables for managing clients and file descriptors
// We're gonna be buffering the messages - improves efficiency, prevents blocking
t_client clients[2048];
fd_set read_set, write_set, current;
int maxfd = 0, gid = 0;
char send_buffer[120000], recv_buffer[120000];
// Error handling function
void err(char *msg)
{
if (msg)
write(2, msg, strlen(msg));
else
write(2, "Fatal error", 11);
write(2, "\n", 1);
exit(1);
}
// Function to send a message to all clients except the specified one
void send_to_all(int except)
{
for (int fd = 0; fd <= maxfd; fd++)
{
if (FD_ISSET(fd, &write_set) && fd != except)
if (send(fd, send_buffer, strlen(send_buffer), 0) == -1)
err(NULL);
}
}
The above portion is setting up our structure, the client, socket variables, and int and char helpers
!Note the variables are global :)!
The
send_to_all
function is checking if the fd IS in the set (prevents failure)
.. and then uses
send()
to send the full buffered string to the client connected on the specified socket (this loops)
💡 The except helps us not send the message to ourselves
need to know: AF_INET == IPv4 denotation; ´SOCK_STREAM´ == TCP denotation !!
Here's what else you need to know to implement the Mini Server successfully:
socket(domain, type, protocol)
: Creates a new socket- domain: AF_INET for IPv4
- type: SOCK_STREAM for TCP
- protocol: Usually 0 for default
- Returns: socket file descriptor or -1 on error
htons(port)
: Host-to-Network Short - converts port number to network byte orderhtonl(addr)
: Host-to-Network Long - converts IP address to network byte orderINADDR_ANY
: Special constant that allows the server to accept connections on any interface
bind(sockfd, addr, addrlen)
: Binds a socket to an address and portlisten(sockfd, backlog)
: Marks socket as passive, waiting for connections- backlog: Maximum length of pending connections queue
accept(sockfd, addr, addrlen)
: Accepts a connection, creates a new socket- Returns a new file descriptor for the accepted connection
send(sockfd, buf, len, flags)
: Sends data through socketrecv(sockfd, buf, len, flags)
: Receives data from socket- Returns number of bytes received or 0 on connection closed or -1 on error
select(nfds, readfds, writefds, exceptfds, timeout)
: Monitors multiple file descriptors- nfds: Highest-numbered fd plus 1
- readfds: Set of descriptors to check for reading
- writefds: Set of descriptors to check for writing
- exceptfds: Set of descriptors to check for exceptions
- timeout: Maximum time to wait (NULL for blocking)
- Returns: Number of ready descriptors or -1 on error
- Initialize socket: Create server socket with
socket()
- Set up server address: Configure sockaddr_in structure with family, address, port
- Bind and listen: Bind socket to address and start listening
- Set up fd_set: Initialize file descriptor sets
- Main loop:
- Use
select()
to monitor sockets - Accept new connections
- Handle data from existing connections
- Broadcast messages to all clients
- Manage client disconnections
- Use
- Always check return values of socket functions
- Remember to handle client disconnection properly
- Use proper byte ordering functions (htons, htonl)
- Make sure to initialize all data structures (bzero or memset)
- Correctly manage your file descriptor sets
- Keep track of the highest fd for select()
- Buffer overflow protection when receiving data
- Properly format messages before broadcasting
The provided code is a complete implementation that handles:
- New client connections
- Client messages (including buffering partial messages until newline)
- Client disconnections
- Broadcasting messages to all connected clients
To test your server:
- Compile with
gcc -o server mini_server.c
- Run with a port number:
./server 4242
- Connect with netcat:
nc localhost 4242
- Open another terminal and connect again to test multi-client functionality
int main(int ac, char **av)
{
// Check for correct number of arguments
if (ac != 2)
err("Wrong number of arguments");
// STEP 2: Set up server socket storage, the special use SOCKADDR_IN struct.
// Create a socket file descriptor (serverfd) with IPv4 address family (AF_INET) and TCP protocol (SOCK_STREAM)
struct sockaddr_in serveraddr;
socklen_t len = sizeof(struct sockaddr_in);
int serverfd = socket(AF_INET, SOCK_STREAM, 0); // this is available in main.c btw
if (serverfd == -1) err(NULL);
maxfd = serverfd;
// FD_SET STUFF!!! File Descriptor Sets Initialization:
// clients
// current fd_set, serverfd and serveraddress
FD_ZERO(¤t);
FD_SET(serverfd, ¤t);
bzero(clients, sizeof(clients));
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(atoi(av[1]));
// --> STEP 4: Server Address Configuration ☝️
...
htonl(uint32_t hostlong)
: Converts the unsigned integer from host byte order to network byte order... ❗s_addr is 32-bit and repr. IPv4 Address
htons(uint16_t hostshort)
: Converts the unsigned SHORT integer hostshort from host byte order to network byte order.. ❗sin_port is 16-bit and represents the port number❗
INADDR_ANY
is a macro that expands to the IP address0.0.0.0.
...
// BIND (typecast serveraddr_in) &
// LISTEN (listen(serverfd, 100))
// ... and ERROR HANDLING for -1 fatal errors
if (bind(serverfd, (const struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1 || listen(serverfd, 100) == -1)
err(NULL);
// you are assigning the address information stored in the `serveraddr` structure to the server socket identified by `serverfd`.
// STEP 6: IMPLEMENT the Main server loop YAY LETSGO!
// bind --> listen --> select --> accept --> recv --> close --> (or process)
while (1)
{
// Set up file descriptor sets for select()
// we write the master set ' current' as in current fds, into the temporary read and write sets...
read_set = write_set = current;
// run select to filter for active (or writeable) fds only.. and thus efficiently wait until any socket has any activity (either n.conn. or socket has data to read)
if (select(maxfd + 1, &read_set, &write_set, 0, 0) == -1) continue;
// it will filter read_set to only have those fds with data to read, closed connections or new connections to accept and the write_set for those fds that be written to without blocking (aka it unoccupied)
// Check all file descriptors for activity
for (int fd = 0; fd <= maxfd; fd++)
{
if (FD_ISSET(fd, &read_set))
{
if (fd == serverfd)
{
// Accept new client connection
int clientfd = accept(serverfd, (struct sockaddr *)&serveraddr, &len);
if (clientfd == -1) continue;
if (clientfd > maxfd) maxfd = clientfd;
clients[clientfd].id = gid++;
FD_SET(clientfd, ¤t);
sprintf(send_buffer, "server: client %d just arrived\n", clients[clientfd].id);
send_to_all(clientfd);
break;
}
else
{
// Handle client message
int ret = recv(fd, recv_buffer, sizeof(recv_buffer), 0);
if (ret <= 0)
{
// Client disconnected
sprintf(send_buffer, "server: client %d just left\n", clients[fd].id);
send_to_all(fd);
FD_CLR(fd, ¤t);
close(fd);
bzero(clients[fd].msg, strlen(clients[fd].msg));
break;
}
else
{
// Process received message
for (int i = 0, j = strlen(clients[fd].msg); i < ret; i++, j++)
{
clients[fd].msg[j] = recv_buffer[i];
if (clients[fd].msg[j] == '\n')
{
clients[fd].msg[j] = '\0';
sprintf(send_buffer, "client %d: %s\n", clients[fd].id, clients[fd].msg);
send_to_all(fd);
bzero(clients[fd].msg, strlen(clients[fd].msg));
j = -1;
}
}
}
}
}
}
}
return (0);
}
These comments provide a high-level overview of the purpose of each section in the code, making it easier to understand the structure and functionality of the server program.
Learn about the code you use:
#include <unistd.h>
write, close and select
#include <sys/socket.h>
socket, accept, listen, send, recv and bind
#include <string.h>
strstr, strlen, strcpy, strcat, memset and bzero
#include <stdlib.h>
malloc, realloc, free, calloc, atoi and exit
#include <stdio.h>
sprintf
Write a program that will listen for client to connect on a certain port on 127.0.0.1 and will let clients to speak with each other. This program will take as first argument the port to bind to.
- If no argument is given, it should write in stderr
"Wrong number of arguments"
followed by a \n and exit with status 1 - If a System Calls returns an error before the program start accepting connection, it should write in stderr "Fatal error" followed by a \n and exit with status 1
- If you cant allocate memory it should write in stderr "Fatal error" followed by a \n and exit with status 1
- Your program must be non-blocking but client can be lazy and if they don't read your message you must NOT disconnect them...
- Your program must not contains #define preproc
- Your program must only listen to 127.0.0.1
The fd that you will receive will already be set to make 'recv' or 'send' to block if select hasn't be called before calling them, but will not block otherwise.
- the client will be given an id. the first client will receive the id 0 and each new client will received the last client id + 1
- %d will be replace by this number
- a message is sent to all the client that was connected to the server: "server: client %d just arrived\n"
- message will only be printable characters, no need to check
- a single message can contains multiple \n
- when the server receive a message, it must resend it to all the other client with "client %d: " before every line!
- a message is sent to all the client that was connected to the server: "server: client %d just left\n"
Memory or fd leaks are forbidden
Practice the exam just like you would in the real exam using this tool - https://github.com/JCluzet/42_EXAM