diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index d8f6604..b6d8eb1 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -1,11 +1,12 @@ /* CUSTOM CSS */ -.main h1.name span.clip, .main p.text { +.main h1.name span.clip, +.main p.text { letter-spacing: -0.17rem; - } +} - .main .tagline { +.main .tagline { letter-spacing: -0.045rem; - } +} /** * Colors: Solid diff --git a/docs/roadmap/phase-0/stage-0.md b/docs/roadmap/phase-0/stage-0.md index 4ec8feb..2b58367 100644 --- a/docs/roadmap/phase-0/stage-0.md +++ b/docs/roadmap/phase-0/stage-0.md @@ -14,9 +14,9 @@ Any distribution of Linux can be used. ### C compiler - gcc -Since we will be building the project primarily in C programming language, we will need a C compiler. We will stick with the gcc compiler for this purpose. +Since we will be building the project primarily in C programming language, we will need a C compiler. We will stick with the popular `gcc` compiler for this purpose. -### GDB +### GNU Debugger - GDB Read the section on [GDB](https://nitcbase.github.io/docs/Misc/GDB) to learn how to use the GNU debugger. diff --git a/docs/roadmap/phase-0/stage-1.md b/docs/roadmap/phase-0/stage-1.md index aacbe97..e208948 100644 --- a/docs/roadmap/phase-0/stage-1.md +++ b/docs/roadmap/phase-0/stage-1.md @@ -2,29 +2,31 @@ ## Recap +In Phase 0 overview, + - We learnt about the Client-Server architecture. - We learnt about the TCP protocol. - We learnt how sockets facilitate the communication between clients and servers. ## Introduction -How does a server function? It listens for any connections, i.e. clients that are trying to connect to the server, and based on the request from the client, it does some specific operation. +How does a server function? It listens for any connections, i.e. clients that are trying to connect to the server, and based on the request from the client it does some specific operation. -To be able to listen for connections, it needs **listening sockets**. Listening sockets are bound to a specific IP address and a port. If any client wants to connect to the server, they have to direct their request to this particular IP:port combo that the server is listening to. +To be able to listen for connections, it needs **listening sockets**. Listening \*\*\*\*sockets are bound to a specific IP address (interface) and a port. If any client wants to connect to the server, they have to direct their request to this particular IP:port combo that the server is listening on. -For eg. Let us assume you have a TCP server ‘running’ on your computer on port 8000. Running signifies that the server is ‘listening’ for any connections on port 8000. If a client wants to connect to the server, they would have to direct their request to `:8000`. +For example, let us assume you have a TCP server ‘running’ on your computer on port 8080. Running signifies that the server is ‘listening’ for any connections on port 8080. If a client wants to connect to the server, they would have to direct their request to `:8080`. Using this knowledge, let us build a simple TCP server from the ground up. Since this is just Stage 1, you will be guided throughout the implementation with all the code given in the form of snippets. -Create an illustration of the working of server. - ## Implementation ![implementation.png](/assets/stage-1/implementation.png) -All the code from this stage will be written in `stage1/tcp_server.c`. +Now that we are going to start coding, lets create a `expserver` folder. All files related to our project will go inside it. Create another folder `phase_0` inside `expserver` which will contain the files we will be creating in Phase 0. + +For this stage, as we are creating a tcp server, create a file `expserver/phase_0/tcp_server.c` -The header files required for this stage are given below. +Let’s start by adding all the header includes and defines. The use of each header will be explained as we proceed further. ```c #include @@ -36,15 +38,16 @@ The header files required for this stage are given below. #include #include -#define SERVER_PORT 8080 +#define PORT 8080 #define BUFF_SIZE 10000 +#define MAX_ACCEPT_BACKLOG 5 ``` -For the clients to be able to connect to the server, let us create a listening socket using the `socket()` function from the `` library. +The first step is to create a listening socket for the clients to be able to connect to the server. This is done using the `socket()` function from the `` header. ```c int main() { - // creating a listening socket + // Creating listening sock int listen_sock_fd = socket(AF_INET, SOCK_STREAM, 0); ``` @@ -53,55 +56,58 @@ The `socket()` function creates a socket, and upon successful creation, returns The function takes three arguments: - domain: `AF_INET` - IPv4 -- type: `SOCK_STREAM` - TCP -- protocol: `0` - allows system to choose the appropriate protocol based on domain and type +- type: `SOCK_STREAM` - Socket of type stream (used for TCP) +- protocol: `0` - allows system to choose the appropriate protocol based on domain and type (TCP) ```c - // setting sock opt reuse addr + // Setting sock opt reuse addr int enable = 1; setsockopt(listen_sock_fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); ``` -This is optional but it will help us bypass the `TIME_WAIT` state that the address and port combination enters when the server is shutdown, thus allowing us to reuse the same address and port. +This will help us bypass the `TIME_WAIT` state that the address and port combination enters when the server is shutdown, thus allowing us to reuse the same address and port. -Now that we have created a socket, we need to specify the address that it needs to listen to. For this we will use an object of `struct socketaddr_in` provided by the `` library. +Now that we have created a socket, we need to specify the address that it needs to listen to. For this we will be using an object of `struct sockaddr_in` provided by the `` header. ```c - // creating an object of struct socketaddr_in + // Creating an object of struct socketaddr_in struct sockaddr_in server_addr; - // setting up server addr + // Setting up server addr server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); ``` - `server_addr.sin_family = AF_INET` - socket will use the IPv4 address format -- `server_addr.sin_addr.s_addr = htonl(INADDR_ANY)` - allows the server to accept connections from any client (explain htonl) -- `server_addr.sin_port = htons(PORT)` - sets the port number (explain htons) +- `server_addr.sin_addr.s_addr = htonl(INADDR_ANY)` - INADDR*ANY constant stands for \_any* IP address. When using this for the server address, this lets the server bind to all network interfaces available on the machine. `htonl()` is a function that will convert from host byte order to network byte order for `long` type. +- `server_addr.sin_port = htons(PORT)` - sets the port number. `htons()` converts from host byte order to network byte order for `short` type. ```c - // binding listening sock to port + // Binding listening sock to port bind(listen_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); - // starting to listen + // Starting to listen listen(listen_sock_fd, MAX_ACCEPT_BACKLOG); printf("[INFO] Server listening on port %d\n", PORT); ``` -The `bind()`function, provided by the `` library, will bind the listening socket to the provided port. The `listen()` function marks the socket as a passive socket, ready to accept incoming connection requests. +The `bind()`function, provided by the `` header, will bind the listening socket to the provided port. The `listen()` function marks the socket as a passive socket, ready to accept incoming connection requests. --- +::: danger QUESTION Now that the server is listening on the port, what happens when a client tries to connect to the server? +::: We need to get the address of the clients that are connecting to the server. For this lets create another object of `struct sockaddr_in`. ```c - // creating an object of struct socketaddr_in + // Creating an object of struct socketaddr_in struct sockaddr_in client_addr; int client_addr_len; + // Accept client connection int conn_sock_fd = accept(listen_sock_fd, (struct sockaddr *)&client_addr, &client_addr_len); printf("[INFO] Client connected to server\n"); ``` @@ -115,6 +121,8 @@ Let’s pause for a bit and recap what just happened. - The socket is made to listen for connections on that port using `listen()` function - The socket accepts a connection from a client with the `accept()` function +--- + ### Milestone #1 We can now do a small test and check how the code performs. @@ -133,11 +141,15 @@ To start the server, use the following command: On running the TCP server, it will display the following message: -```Server +```bash [INFO] Server listening on port 8080 ``` -But what/who is going to connect to the server? Since we have not created a TCP client yet, lets use a networking utility tool called _netcat_. netcat is a versatile tool that has a wide range of functionalities including the ability to act as a TCP client. +But what/who is going to connect to the server? Since we have not created a TCP client yet, lets use a networking utility tool called _netcat_. + +::: info +netcat is a versatile tool that has a wide range of functionalities including the ability to act as a TCP client. +::: netcat takes an IP address and a port to connect to. In our case, since the server is running on the same machine, we can use `[localhost](http://localhost)` as the IP address and 8080 as the port number. @@ -149,7 +161,7 @@ nc localhost 8080 When the client connection to the server is successful, it will show the following message: -```Server +```bash [INFO] Server listening on port 8080 [INFO] Client connected to server ``` @@ -166,6 +178,7 @@ Initialize a `char` buffer to store the client message. ```c while (1) { + // Create buffer to store client message char buff[BUFF_SIZE]; memset(buff, 0, BUFF_SIZE); ``` @@ -173,7 +186,7 @@ Initialize a `char` buffer to store the client message. The `memset` function is initialize the value of `buff` to 0. ```c - // read message from client to buffer + // Read message from client to buffer int read_n = recv(conn_sock_fd, buff, sizeof(buff), 0); ``` @@ -182,21 +195,21 @@ The `recv()` function is used to receive data from the connected socket. The rec Let’s take care of some error handling in case of unexpected failure. ```c - // client closed connection or error occurred + // Client closed connection or error occurred if (read_n <= 0) { printf("[INFO] Client disconnected. Closing server\n"); close(conn_sock_fd); exit(1); } - // print message from cilent - printf("[CLIENT MESSAGE] %s\n", buff); + // Print message from client + printf("[CLIENT MESSAGE] %s", buff); ``` `buff` now contains the message sent by the client. The server has to reverse the message string and send it back to the client. Let us write a quick and simple string reversal function to take care of this and place it outside of the main function. ```c -// function to reverse a string +// Function to reverse a string void strrev(char *str) { for (int start = 0, end = strlen(str) - 2; start < end; start++, end--) { char temp = str[start]; @@ -206,13 +219,13 @@ void strrev(char *str) { } ``` -Now that `buff` has the reversed string, its time to send it to the client. We can use the `send()` function provided by the `` library to achieve this. +Now that `buff` has the reversed string, its time to send it to the client. We can use the `send()` function provided by the `` header to achieve this. ```c - // string reverse + // Sting reverse strrev(buff); - // sending client message to server + // Sending reversed string to client send(conn_sock_fd, buff, read_n, 0); } } @@ -222,7 +235,7 @@ Now that `buff` has the reversed string, its time to send it to the client. We c The final code should look like this. -::: details Code +::: details expserver/phase_0/tcp_server.c ```c #include @@ -235,10 +248,10 @@ The final code should look like this. #include #define PORT 8080 -#define BUFF_SIZE 10096 +#define BUFF_SIZE 10000 #define MAX_ACCEPT_BACKLOG 5 -// function to reverse a string +// Function to reverse a string void strrev(char *str) { for (int start = 0, end = strlen(str) - 2; start < end; start++, end--) { char temp = str[start]; @@ -248,62 +261,65 @@ void strrev(char *str) { } int main() { - // creating listening sock + // Creating listening sock int listen_sock_fd = socket(AF_INET, SOCK_STREAM, 0); - // setting sock opt reuse addr + // Setting sock opt reuse addr int enable = 1; setsockopt(listen_sock_fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); - // creating an object of struct socketaddr_in + // Creating an object of struct socketaddr_in struct sockaddr_in server_addr; - // setting up server addr + // Setting up server addr server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); - // binding listening sock to port + // Binding listening sock to port bind(listen_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); - // starting to listen + // Starting to listen listen(listen_sock_fd, MAX_ACCEPT_BACKLOG); printf("[INFO] Server listening on port %d\n", PORT); - // creating an object of struct socketaddr_in + // Creating an object of struct socketaddr_in struct sockaddr_in client_addr; int client_addr_len; + // Accept client connection int conn_sock_fd = accept(listen_sock_fd, (struct sockaddr *)&client_addr, &client_addr_len); printf("[INFO] Client connected to server\n"); while (1) { - // create buffer to store client message + // Create buffer to store client message char buff[BUFF_SIZE]; memset(buff, 0, BUFF_SIZE); - // read message from client to buffer + // Read message from client to buffer int read_n = recv(conn_sock_fd, buff, sizeof(buff), 0); - // client closed connection or error occurred + // Client closed connection or error occurred if (read_n <= 0) { printf("[INFO] Client disconnected. Closing server\n"); close(conn_sock_fd); exit(1); } - // print message from client + // Print message from client printf("[CLIENT MESSAGE] %s", buff); - // sting reverse + // Sting reverse strrev(buff); - // sending client message to server + // Sending reversed string to client send(conn_sock_fd, buff, read_n, 0); } } ``` +--- + ::: ### Milestone #2 @@ -312,26 +328,26 @@ It’s time to test the server! Similar to last time, open 2 terminals, one for Upon successful connection of the client to the server, the server terminal should look like this: -```Server +```bash [INFO] Server listening on port 8080 [INFO] Client connected to server ``` Let us try to send a string from the client terminal: -```Client +```bash hello ``` The server will receive the message sent by the client, and should send a response back to the client with the reversed string. -```Server +```bash [CLIENT MESSAGE] hello ``` The client will receive the reversed string. -```Client +```bash olleh ``` diff --git a/docs/roadmap/phase-0/stage-2.md b/docs/roadmap/phase-0/stage-2.md index d5c3c7c..0829944 100644 --- a/docs/roadmap/phase-0/stage-2.md +++ b/docs/roadmap/phase-0/stage-2.md @@ -10,13 +10,17 @@ Recall the previous stage where we relied on a third-party client, _netcat_, to Expect some code to echo the TCP server implementation, given the similarities in functionality. +::: tip NOTE +Code snippets will contain comments in between with a format like this: `/* todo */`. These are meant to be filled in as you go through the documentation and implement it. +::: + ## Implementation ![implementation.png](/assets/stage-2/implementation.png) -Create a file named `tcp_client.c` in the same directory as `tcp_server.c`. All the code from this stage will be written to it. +Create a file `expserver/phase_0/tcp_client.c` . All the code from this stage will be written to it. -The header files required for this stage are given below. +The header includes and defines required for this stage are given below. ```c #include @@ -36,44 +40,57 @@ How will the client connect to the server? That’s right, with the help of a so ```c int main() { + // Creating listening sock int client_sock_fd = /* create a socket of type SOCK_STREAM */ ``` But where (or which server) does this client connect to? To establish a connection, the client must know the IP address and listening port of the server it intends to connect to. We can use an object of `struct sockaddr_in` for this purpose. ```c - struct sockaddr_in server_addr; + // Creating an object of struct socketaddr_in + struct sockaddr_in server_addr; + + // Setting up server addr server_addr.sin_family = /* fill this */; - server_addr.sin_addr.s_addr = /* fill this */; + server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); server_addr.sin_port = /* fill this */; ``` -Now that we have the client socket and and the server address (IP and port), connect the client to the server with the help of the `connect` function. Use error handling to handle cases of unexpected failure. +::: info +`server_addr.sin_addr.s_addr` is set to the IP address `127.0.0.1`. This IP stands for [localhost](http://localhost) ie. host machine. The `inet_addr()` function will convert string IP to required numeric format. +::: -Refer [this](https://pubs.opengroup.org/onlinepubs/009695399/functions/connect.html) to read up on the `connect` function call. +Now that we have the client socket and and the server address (IP and port), connect the client to the server with the help of the `connect()` function. Use error handling to handle cases of unexpected failure. Refer [this linux man page](https://man7.org/linux/man-pages/man2/connect.2.html) to know more about `connect()` . ```c - // connect to tcp server - if (connect(/* fill this */) { + // Connect to tcp server + if (connect(client_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) { printf("[ERROR] Failed to connect to tcp server\n"); exit(1); - } else + } else { printf("[INFO] Connected to tcp server\n"); + } ``` +--- + ### Milestone #1 -This is a good point to test the code you have written. Compile and start the TCP server that we wrote in Stage 1 and start this TCP client on separate terminals. +This is a good point to test the code you have written. Compile and start the TCP server that we wrote in Stage 1 and this TCP client on separate terminals. + +::: warning +Make sure to run the server before you run the client. +::: The terminal running the TCP server should display the following message: -``` +```bash [INFO] Client connected to server ``` The terminal running the TCP client should display the following message: -``` +```bash [INFO] Connected to tcp server ``` @@ -85,44 +102,48 @@ Now let’s implement the functionality for the client to accept some input from ```c while (1) { + // Get message from client terminal char *line; size_t line_len = 0, read_n; + read_n = /* read a line from the user using getline() */ - read_n = /* read a line from the user using getline() */ - - /* send message to tcp server using send() */ + /* send message to tcp server using send() */ - /* create a char buffer of BUFF_SIZE and memset to 0 */ + /* create a char buffer of BUFF_SIZE and memset to 0 */ - read_n = recv(/* read message sent by server to client into buffer */) + // Read message from client to buffer + read_n = recv(/* read message sent by server to client into buffer */) - /* close the connection if read_n <= 0 */ + /* close the connection and exit if read_n <= 0 */ - // print message from cilent + // Print message from cilent printf("[SERVER MESSAGE] %s\n", buff); - } - return 0; + } + + return 0; } ``` +--- + ### Milestone #2 Again, start the TCP server and TCP client on separate terminals. You should get successful connection message on both terminals. The terminal with the TCP server should display the following message: -```c +```bash [INFO] Client connected to server ``` Type some message in the terminal running the TCP client. -```c +```bash [INFO] Connected to tcp server hello ``` -The server will receive the message sent by the the client. +The server would receive the message sent by the the client. ```bash [CLIENT MESSAGE] hello @@ -130,7 +151,7 @@ The server will receive the message sent by the the client. The client terminal should get a response message (reversed string) from the server as a receipt. -```c +```bash [INFO] Connected to tcp server hello olleh @@ -140,4 +161,4 @@ olleh Congratulations! You have written a TCP client from the ground up which connected with a TCP server with the ability to send and receive messages. -But there is a big drawback with the server from Stage 1. Think about what it can be. You will find the answer to that question in the next stage! +But there is a big drawback with the server from Stage 1. Think about what it can be. You will find the answer to that question in the next stage. diff --git a/docs/roadmap/phase-1/stage-5.md b/docs/roadmap/phase-1/stage-5.md index 80ae996..be70e44 100644 --- a/docs/roadmap/phase-1/stage-5.md +++ b/docs/roadmap/phase-1/stage-5.md @@ -57,7 +57,7 @@ main() `xps.h` will consist of constants and user defined types that are common to all modules and will be used everywhere. Create a file `xps.h` under `expserver` folder and copy the below content to it. -::: details `expserver/xps.h` +::: details expserver/xps.h ```c #ifndef XPS_H @@ -106,7 +106,7 @@ We will be constantly modifying/adding to this file in each stage to accommodate The code below has the contents of the header file for `xps_socket`. Have a look at it and make a copy of it in your codebase. -::: details `expserver/network/xps_socket.h` +::: details expserver/network/xps_socket.h ```c #ifndef XPS_SOCKET_H @@ -163,7 +163,7 @@ Each entity will have a create and destroy function. Create functions are respon Let’s start by writing the `xps_socket_create()` function. -::: details `expserver/network/xps_socket.c` +::: details expserver/network/xps_socket.c ```c xps_socket_t *xps_socket_create(int fd) { @@ -216,7 +216,7 @@ xps_socket_t *xps_socket_create(int fd) { Moving on to the destroy function. When a socket is provided to be destroyed, we have to close the socket and free up the memory. -::: details `expserver/network/xps_socket.c` +::: details expserver/network/xps_socket.c ```c void xps_socket_destroy(xps_socket_t *sock) { @@ -253,7 +253,7 @@ void xps_socket_destroy(xps_socket_t *sock) { The code below has the contents of the header file for `xps_server`. Have a look at it and make a copy of it in your codebase. -::: details `expserver/network/xps_socket.h` +::: details expserver/network/xps_socket.h ```c #ifndef XPS_SERVER_H @@ -287,7 +287,7 @@ Each server instance will have the following: Let’s proceed with writing the functions associated with the server. We have a server create and destroy function. Most of the implementation for `xps_server_create()` was already done by us in the previous stages. -::: details `expserver/network/xps_server.c` +::: details expserver/network/xps_server.c ```c xps_server_t *xps_server_create(int port, xps_server_connection_handler_t connection_cb) { @@ -344,7 +344,7 @@ This destroy function might be a bit tricky on first glance as `xps_server_t` st You might have a question here. What about `server->port` & `server->connection_cb`? Memory needs to be free/deallocated ONLY for members which were dynamically allocated (for eg. using malloc). -::: details `expserver/network/xps_server.c` +::: details expserver/network/xps_server.c ```c void xps_server_destroy(xps_server_t *server) { @@ -379,7 +379,7 @@ The above code snippet shows us how we can destroy objects stored in a `vec` lis Last but not least, we have a short and important function `xps_server_loop_read_handler()` that invokes the callback function that should be called when the server receives a new client connection -::: details `expserver/network/xps_server.c` +::: details expserver/network/xps_server.c ```c void xps_server_loop_read_handler(xps_server_t *server) { @@ -413,7 +413,7 @@ Let’s have a recap of what we have done till now. The code below has the contents of the header file for `xps_client`. Have a look at it and make a copy of it in your codebase. -::: details `expserver/network/xps_client.h` +::: details expserver/network/xps_client.h ```c #ifndef XPS_CLIENT_H @@ -449,7 +449,7 @@ The client will also have functions specifically for creation and destruction. `xps_client_create()` function will create a new client instance with the specified server and socket. -::: details `expserver/network/xps_client.c` +::: details expserver/network/xps_client.c ```c xps_client_t *xps_client_create(xps_server_t *server, xps_socket_t *sock, xps_client_read_handler_t read_cb) { @@ -475,7 +475,7 @@ Recall how in the `xps_server_destroy()` function we used `xps_client_destroy()` The `xps_client_destroy()` function is responsible for releasing the resources associated with a client instance. Each client belongs to a server, and the server may have multiple clients connected to it. When destroying a client, the function iterates through the list of clients in the server to find the specific client instance and set it to `NULL`. -::: details `expserver/network/xps_client.c` +::: details expserver/network/xps_client.c ```c void xps_client_destroy(xps_client_t *client) { @@ -509,7 +509,7 @@ Refer the `handle_client()` function from the previous stage and modify the func If you notice carefully, the return type of `xps_client_read()` is of type `xps_buffer_t`. How would you create one? We have a `xps_buffer_create()` function just for this. It’ll take care of creating the buffer, handle all the potential errors in between and return a buffer of type `xps_buffer_t`. Read more about `xps_buffer` utility [here](https://www.notion.so/xps_buffer-f8bceae3c7c347c0a86441e3c80aaa61?pvs=21). -::: details `expserver/network/xps_client.c` +::: details expserver/network/xps_client.c ```c void xps_client_write(xps_client_t *client, xps_buffer_t *buff) { @@ -531,7 +531,7 @@ xps_buffer_t *xps_client_read(xps_client_t *client) { Create a simple function, `xps_client_loop_read_handler()` to call the client read callback. -::: details `expserver/network/xps_client.c` +::: details expserver/network/xps_client.c ```c void xps_client_loop_read_handler(xps_client_t *client) { @@ -559,7 +559,7 @@ We’ll start with the `main()` function because that is where the execution sta Always try to keep the main function simple and distribute the work into dedicated functions. ::: -::: details `expserver/main.c` +::: details expserver/main.c ```c // Global variables @@ -593,7 +593,7 @@ The implementations of `loop_attach()` and `loop_detach()` remain unchanged from When creating the server with `xps_server_create()`, we provide it with `xps_server_connection_handler` as a callback function. This function gets invoked when a client attempts to connect to the server, essentially replacing `accept_connection()` from the previous stage. It's the same function triggered within `xps_server_loop_read_handler()` in `xps_server.c`. Think about what the callback function will contain before diving into its implementation. -::: details `expserver/main.c` +::: details expserver/main.c ```c void xps_server_connection_handler(xps_server_t *server, int status) { @@ -616,7 +616,7 @@ void xps_server_connection_handler(xps_server_t *server, int status) { `xps_client_read_handler()` \*\*\*\*is the client callback function that we attach to all client instances. This function gets triggered when there is data to be read from the client. Recall its use in `xps_client_loop_read_handler()` in `xps_client.c`. -::: details `expserver/main.c` +::: details expserver/main.c ```c void xps_client_read_handler(xps_client_t *client, int status) { @@ -647,7 +647,7 @@ In case of a server event, you would have to loop through all the server instanc Similarly for client, loop through all the clients in all the servers to find the client with the read event. Call the `xps_client_loop_read_handler()` when the client has been found. -::: details `expserver/main.c` +::: details expserver/main.c ```c void loop_run(int epoll_fd) { @@ -692,7 +692,7 @@ void loop_run(int epoll_fd) { ::: -::: note +::: tip NOTE Take a note of the type casting on client from server’s clients list to `xps_client_t` in the client loop. :::