From a0d3d12efa6ea2073991ab238dcfd083d4ebcbaf Mon Sep 17 00:00:00 2001 From: Aadhavan Date: Wed, 10 Apr 2024 21:04:55 +0530 Subject: [PATCH] changes after review --- docs/.vitepress/config.mjs | 40 +- docs/roadmap/index.md | 3 +- docs/roadmap/phase-0/stage-2.md | 28 +- docs/roadmap/phase-0/stage-4.md | 374 +----------- docs/roadmap/phase-0/stage-5.md | 387 +++++++++++++ docs/roadmap/phase-1/stage-10.md | 1 + docs/roadmap/phase-1/stage-5.md | 734 ----------------------- docs/roadmap/phase-1/stage-6.md | 965 +++++++++++++++++++------------ docs/roadmap/phase-1/stage-7.md | 498 +++++++++++++++- docs/roadmap/phase-1/stage-8.md | 2 +- docs/roadmap/phase-1/stage-9.md | 2 +- docs/roadmap/phase-2/stage-10.md | 1 - docs/roadmap/phase-2/stage-11.md | 2 +- docs/roadmap/phase-2/stage-12.md | 2 +- docs/roadmap/phase-2/stage-13.md | 2 +- docs/roadmap/phase-2/stage-14.md | 1 + docs/roadmap/phase-3/stage-14.md | 1 - docs/roadmap/phase-3/stage-15.md | 2 +- docs/roadmap/phase-3/stage-16.md | 2 +- docs/roadmap/phase-3/stage-17.md | 2 +- docs/roadmap/phase-3/stage-18.md | 2 +- docs/roadmap/phase-3/stage-19.md | 1 + docs/roadmap/phase-4/stage-19.md | 1 - docs/roadmap/phase-4/stage-20.md | 2 +- docs/roadmap/phase-4/stage-21.md | 2 +- docs/roadmap/phase-4/stage-22.md | 1 + 26 files changed, 1545 insertions(+), 1513 deletions(-) create mode 100644 docs/roadmap/phase-0/stage-5.md create mode 100644 docs/roadmap/phase-1/stage-10.md delete mode 100644 docs/roadmap/phase-1/stage-5.md delete mode 100644 docs/roadmap/phase-2/stage-10.md create mode 100644 docs/roadmap/phase-2/stage-14.md delete mode 100644 docs/roadmap/phase-3/stage-14.md create mode 100644 docs/roadmap/phase-3/stage-19.md delete mode 100644 docs/roadmap/phase-4/stage-19.md create mode 100644 docs/roadmap/phase-4/stage-22.md diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index ed82361..f937d64 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -92,9 +92,13 @@ export default defineConfig({ link: '/roadmap/phase-0/stage-3', }, { - text: 'Stage 4: TCP proxy', + text: 'Stage 4: UDP server with multi-threading', link: '/roadmap/phase-0/stage-4', }, + { + text: 'Stage 5: TCP proxy', + link: '/roadmap/phase-0/stage-5', + }, ], }, { @@ -106,23 +110,23 @@ export default defineConfig({ link: '/roadmap/phase-1/', }, { - text: 'Stage 5: Server & Client module', + text: 'Stage 6: Server & Client module', link: '/roadmap/phase-1/stage-5', }, { - text: 'Stage 6: Core & Loop module', + text: 'Stage 7: Core & Loop module', link: '/roadmap/phase-1/stage-6', }, { - text: 'Stage 7: TCP module', + text: 'Stage 8: TCP module', link: '/roadmap/phase-1/stage-7', }, { - text: 'Stage 8: Upstream module', + text: 'Stage 9: Upstream module', link: '/roadmap/phase-1/stage-8', }, { - text: 'Stage 9: File module', + text: 'Stage 10: File module', link: '/roadmap/phase-1/stage-9', }, ], @@ -136,19 +140,19 @@ export default defineConfig({ link: '/roadmap/phase-2/', }, { - text: 'Stage 10: HTTP parser', + text: 'Stage 11: HTTP parser', link: '/roadmap/phase-2/stage-10', }, { - text: 'Stage 11: HTTP req & HTTP res', + text: 'Stage 12: HTTP req & HTTP res', link: '/roadmap/phase-2/stage-11', }, { - text: 'Stage 12: Config & Session module', + text: 'Stage 13: Config & Session module', link: '/roadmap/phase-2/stage-12', }, { - text: 'Stage 13: HTTP Spec', + text: 'Stage 14: HTTP Spec', link: '/roadmap/phase-2/stage-13', }, ], @@ -162,23 +166,23 @@ export default defineConfig({ link: '/roadmap/phase-3/', }, { - text: 'Stage 14: IP whitelist & blacklist', + text: 'Stage 15: IP whitelist & blacklist', link: '/roadmap/phase-3/stage-14', }, { - text: 'Stage 15: Directory browsing', + text: 'Stage 16: Directory browsing', link: '/roadmap/phase-3/stage-15', }, { - text: 'Stage 16: Compression', + text: 'Stage 17: Compression', link: '/roadmap/phase-3/stage-16', }, { - text: 'Stage 17: Load balancing', + text: 'Stage 18: Load balancing', link: '/roadmap/phase-3/stage-17', }, { - text: 'Stage 18: Rate limiting & timeouts', + text: 'Stage 19: Rate limiting & timeouts', link: '/roadmap/phase-3/stage-18', }, ], @@ -192,15 +196,15 @@ export default defineConfig({ link: '/roadmap/phase-4/', }, { - text: 'Stage 19: TLS', + text: 'Stage 20: TLS', link: '/roadmap/phase-4/stage-19', }, { - text: 'Stage 20: Caching', + text: 'Stage 21: Caching', link: '/roadmap/phase-4/stage-20', }, { - text: 'Stage 21: Multiprocess Architecture', + text: 'Stage 22: Multiprocess Architecture', link: '/roadmap/phase-4/stage-21', }, ], diff --git a/docs/roadmap/index.md b/docs/roadmap/index.md index 62f1db9..044bf84 100644 --- a/docs/roadmap/index.md +++ b/docs/roadmap/index.md @@ -23,7 +23,8 @@ The eXpServer project comprises 22 stages, organized into 5 phases. Prior to the - [Stage 1: TCP Server](phase-0/stage-1) - [Stage 2: TCP Client](phase-0/stage-2) - [Stage 3: Linux epoll](phase-0/stage-3) -- [Stage 4: TCP Proxy](phase-0/stage-4) +- [Stage 4: UDP with Multi-threading](phase-0/stage-4) +- [Stage 5: TCP Proxy](phase-0/stage-5) ### Phase 1: Building the core of eXpServer by creating reusable modules diff --git a/docs/roadmap/phase-0/stage-2.md b/docs/roadmap/phase-0/stage-2.md index dff2dec..8dff2fb 100644 --- a/docs/roadmap/phase-0/stage-2.md +++ b/docs/roadmap/phase-0/stage-2.md @@ -168,9 +168,9 @@ hello olleh ``` -## Exercises +## Experiments -### Exercise #1 +### Experiment #1 What would happen when multiple clients try and connect to the same server? Let us test it out! @@ -188,23 +188,27 @@ Meanwhile, client #1 remains connected. To verify the connection, we can send an Think of why this is happening. We will fix it in the next stage. -### Exercise #2 +### Experiment #2 -Did you notice how the server closed when the client disconnected? +Did you notice what happened when you closed the connected client instance? The sever also terminated with it. But what if the server wants to keep serving other clients? -TODO +Modify the code such that the server does not terminate immedietly after a client disconnects. -Ask them to fix the issue - -Test cases to test the code +::: info HINT +Changes are needed where the server accepts the client connection using `accept()`. +::: ## Conclusion 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. -There are two methods to solve the above problem: +Recall the problem from [Experiment #1](/roadmap/phase-0/stage-2#experiment-1). This limitation can be fixed using using two different methods: + +1. With [multi-threading]() +2. With [epoll](https://en.wikipedia.org/wiki/Epoll) + +In web severs like [Apache](https://en.wikipedia.org/wiki/Apache_HTTP_Server), multi-threading was used for serving clients simultaneously. Each incoming client request is typically assigned to a separate thread, allowing the server to serve multiple clients concurrently without blocking or slowing down other requests. -1. With threading -2. With epoll +Whereas in [Nginx](https://en.wikipedia.org/wiki/Nginx), a more recent web server compared to Apache, uses an event-driven architecture, which relies on epoll. Instead of creating a new thread for every new connection, a single thread is sufficient to handle multiple clients simultaneously. -In the next stage we will work with threading. +In the next stage we will build a server with multi-threading to solve the cuncurrency issue. diff --git a/docs/roadmap/phase-0/stage-4.md b/docs/roadmap/phase-0/stage-4.md index ab4a20e..907c529 100644 --- a/docs/roadmap/phase-0/stage-4.md +++ b/docs/roadmap/phase-0/stage-4.md @@ -1,381 +1,17 @@ -# Stage 4: TCP Proxy +# Stage 4: UDP with Multi-threading ## Recap -- In the previous stage, we modified our TCP server code to handle multiple clients simultaneously using epoll +- We covered ## Learning Objectives -- We will combine the functionalities of a TCP server from [Stage 1](/roadmap/phase-0/stage-1) and client from [Stage 3](/roadmap/phase-0/stage-3) to make a TCP [proxy](https://en.wikipedia.org/wiki/Proxy_server) which will relay communication between a web browser and a python file server. - -## Introduction - -[Proxy](https://en.wikipedia.org/wiki/Proxy_server) is a intermediary which sits in between a client and an [upstream server](https://en.wikipedia.org/wiki/Upstream_server) and relays communication between them. When a client makes a request to access a resource (such as a website or a file), it connects to the proxy server instead of directly connecting to the target server. The proxy server then forwards the client's request to the target server, retrieves the response, and sends it back to the client. - -In this stage, our client will be a web browser and upstream server will be a python file server serving a folder on our local hard drive. Instead of the web browser directly connecting with the python file server, it makes a connection to the proxy which in turn will connect to the python server to relay the request from the browser. - -![tcp-proxy.png](/assets/stage-4/tcp-proxy.png) - -Before we get into the implementation of the proxy, lets have a look at what we are trying to achieve. We will start by running a python file server. - -Open a terminal and navigate to the folder you want to serve. Run the following command below to start a simple python file server: - -This command starts [Python's inbuilt HTTP server module](https://docs.python.org/3/library/http.server.html) which will serve the files in the folder it started in. - -```bash -python -m http.server 3000 -``` - -Now that the local file server is running on port 3000, we can connect to it using a browser by going to `localhost:3000`. - -![python-server.png](/assets/stage-4/python-server.png) - -Right now, the client (web browser), is directly accessing the file server. Our goal is to modify the TCP server code from Stage 3 to turn it into a TCP proxy server, so that all the communication between the client and upstream server goes though the proxy. - -## Implementation - -![implementation.png](/assets/stage-4/implementation.png) - -There will be few major changes in the structure of the code from previous stage where we wrote the entire implementation in `main()`. Thus, for this stage, we recommended working on a new separate file; let’s call it `tcp_proxy.c`. - -In addition to the previous definitions in `tcp_server.c`, add a global definition at the top of the file for the upstream port number that we will be serving the python file server from. - -::: tip NOTE -Add this to global definitions - -```c -#define UPSTREAM_PORT 3000 -``` - -::: - -We’ll start with encapsulation of the code written in the previous stage by placing them in different functions. Copy over the code from `tcp_server.c` and place it in the appropriate functions: - -```c -int create_loop() { - /* return new epoll instance */ -} - -void loop_attach(int epoll_fd, int fd, int events) { - /* attach fd to epoll */ -} - -int create_server() { - /* create listening socket and return it */ -} - -void loop_run(int epoll_fd) { - /* infinite loop and processing epoll events */ -} -``` - -Let’s focus on `loop_run(int epoll_fd)` now. In the previous stage, we had epoll events from two sources; the listen socket and the connection socket. Now there will be another socket that we will be adding to our epoll called as the **upstream socket**. - -The python file server is the upstream server in our case. When a user connects to the TCP proxy server to access files from the upstream server, the TCP proxy server will open a connection to the upstream server. All the communication sent to the proxy by the client will be relayed to the file server, and similarly data sent by the file server to the proxy (intended for the client) will be sent through this connection. - -The figure below illustrates the three different events that could occur in epoll, and how we should handle each one of them: - -![events.png](/assets/stage-4/events.png) - -```c -void loop_run(int epoll_fd) { - while (1) { - printf("[DEBUG] Epoll wait\n"); - - /* epoll wait */ - - for (...) { - if (/* event is on listen socket*/) - accept_connection(); // we will implement this later - else if (/* event is on connection socket */) - handle_client(); // we will implement this later - else if (/* event is on upstream socket */) - handle_upstream(); // we will implement this later - - } - } -} -``` - -Since we are aiming for concurrency, for each new client that connects to the proxy server, we need to create a new upstream link to connect with the file server. How can we effectively monitor the association between clients and their respective upstream links in scenarios where there are multiple clients? - -This is where [**route tables**](https://en.wikipedia.org/wiki/Routing_table) come into play. We store the connection socket FD and its corresponding upstream socket FD in a pair wise manner. - -Here are some global variables that could come handy: - -::: tip NOTE -Add this to global variables: - -```c -int listen_sock_fd, epoll_fd; -struct epoll_event events[MAX_EPOLL_EVENTS]; -int route_table[MAX_SOCKS][2], route_table_size = 0; -``` - -::: - - - -Now that we have that, we are ready to start accepting connection; so lets write the `accept_connection()` function. - -### `accept_connection()` - -`accept_connection()` takes `listen_sock_fd` as a param and do the following - -- Accept the client connection and create the connection socket FD `conn_sock_fd` -- Add the connection socket to epoll to monitor for events using `epoll_ctl()` -- Open up a connection to the upstream server using `connect_upstream()`, and add it to the epoll -- An entry will be added to the route table with the `conn_sock_fd` and it's corresponding `upstream_sock_fd` - -```c -void accept_connection(int listen_sock_fd) { - - int conn_sock_fd = /* accept client connection */ - - /* add conn_sock_fd to loop using loop_attach() */ - - // create connection to upstream server - int upstream_sock_fd = connect_upstream(); - - /* add upstream_sock_fd to loop using loop_attach() */ - - // add conn_sock_fd and upstream_sock_fd to routing table - route_table[route_table_size][0] = /* fill this */ - route_table[route_table_size][1] = /* fill this */ - route_table_size += 1; - -} -``` - -Try and implement the function `connect_upstream()` to create a connection to the upstream server. - -```c -int connect_upstream() { - - int upstream_sock_fd = /* create a upstrem socket */ - - struct sockaddr_in upstream_addr; - /* add upstream server details */ - - connect(/* connect to upstream server */); - - return upstream_sock_fd; - -} -``` - -### Milestone #1 - -Quick recap! - -- There are three different events that the proxy has to handle: - - Event on the listen socket when a client tries to connect with the proxy (intended to communicate with the upstream server) - `accept_connection()` - - Event on the connection socket when client sends message to proxy (intended for the upstream server) - `handle_client()` - - Event on the upstream socket when the upstream server sends message to proxy (intended for the client) - `handle_upstream()` - ---- - -Now that we have accepted the clients, we need to handle. We will create the `handle_client()` function to receive the messages from the client, and send it to the upstream server. - -### `handle_client()` - -This implementation is similar to how we handled clients in the previous stages with a few changes. - -```c -void handle_client(int conn_sock_fd) { - - int read_n = /* read message from client to buffer using recv */ - - // client closed connection or error occurred - if (read_n <= 0) { - close(conn_sock_fd); - return; - } - - /* print client message (helpful during milestone#2) */ - - /* find the right upstream socket from the route table */ - - // sending client message to upstream - int bytes_written = 0; - int message_len = read_n; - while (bytes_written < message_len) { - int n = send(/* found upstream socket */, buff + bytes_written, message_len - bytes_written, 0); - bytes_written += n; - } - -} -``` - -Find the upstream socket associated with `conn_sock_fd` and route table and start sending the message that we received from the client. - -::: tip NOTE -There is a noticeable change to the `send()` function compared to the last stage. The drawback with the old approach will appear when we are sending large amounts of data due to limitations in buffer size and network conditions. We will look into this in an exercise at the end. - -The new approach allows us to handle large data by sending it in smaller chunks, and lets us handle any errors. -::: +- We will implement --- -When there are messages to be read from the upstream server, the `handle_upstream()` function kicks in and takes over. - -### `handle_upstream()` - -This function will be strikingly similar to `handle_client()` with few obvious changes. `handle_upstream()` will be responsible to receive messages from the upstream, find the matching connection, and send the message to the client. - -```c -void handle_upstream(int upstream_sock_fd) { - - int read_n = /* read message from upstream to buffer using recv */ - - // Upstream closed connection or error occurred - if (read_n <= 0) { - close(upstream_sock_fd); - return; - } - - /* find the right client socket from the route table */ - - /* send upstream message to client */ - -} -``` - ---- - -With encapsulation of code into multiple functions, our main function will look very minimal and it will be responsible for setting up and making the proxy run. - -```c -int main() { - - listen_socket_fd = /* create server using server_create() */ - - epoll_fd = /* create loop instance using loop_create() */ - - /* attach server to event loop using loop_attach() */ - - /* start event loop with loop_run() */ - -} -``` - -At the end, our code will have a structure similar to this: - -::: details expserver/tcp_proxy.c - -```c -/* includes, defines and global variables */ +::: tip PRE-REQUISITE READING -/* any helper functions you might write */ +- Read about -int connect_upstream() { - /* connect to upstream server */ -} - -void accept_connection(int listen_sock_fd) { - /* accept client connection */ -} - -void handle_client(int conn_sock_fd) { - /* handle client */ -} - -void handle_upstream(int upstream_sock_fd) { - /* handle upstream */ -} - -int create_loop() { - /* return new epoll instance */ -} - -void loop_attach(int epoll_fd, int fd, int events) { - /* attach fd to epoll */ -} - -int create_server() { - /* create listening socket and return it */ -} - -void loop_run(int epoll_fd) { - /* infinite loop and for loop*/ -} - -int main() { - /* initialize proxy */ -} -``` - -::: - -::: warning -There is no restriction to just these functions. Feel free to create additional helper functions as needed to suit your requirements. ::: - ---- - -### Milestone #2 - -To test our code, we will essentially try to replicate what we saw in the introduction, i.e. a Python file server. But there, our browser being the client, had a direct connection to the file server. - -Right now, we want our browser to connect to the proxy server, which in turn will proxy the request to the python server. - -Start the python file server to serve the `expserver/` directory: - -```bash -python -m http.server 3000 -``` - -The file server will start with the following message if successful: - -```bash -Serving HTTP on 0.0.0.0 port 3000 (http://0.0.0.0:3000/) ... -``` - -Compile and run `tcp_server.c`. The terminal should output the following: - -```bash -[INFO] Server listening on port 8080 -[DEBUG] Epoll wait -``` - -Now, the fileserver is active on port `3000` and the proxy is running on port `8080`. In the introduction demo, we connected to the file server directly by accessing `localhost:3000` from our browser (client). This time, we’ll connect to the proxy by looking up `localhost:8080` in the browser. - -Both links should lead to the same file server; they're just different paths. If this works as expected, it indicates that our proxy is functioning perfectly! - -If you included a `printf` statement in `handle_client()` to print the client message, you would get a [HTTP](https://nl.wikipedia.org/wiki/Hypertext_Transfer_Protocol) request message in the proxy terminal when the client visits `localhost:8080`. - -```bash -[CLIENT MESSAGE] GET / HTTP/1.1 -Host: localhost:8080 -Connection: keep-alive -sec-ch-ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122" -sec-ch-ua-mobile: ?0 -sec-ch-ua-platform: "Windows" -Upgrade-Insecure-Requests: 1 -User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0 -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 -Sec-Fetch-Site: none -Sec-Fetch-Mode: navigate -Sec-Fetch-User: ?1 -Sec-Fetch-Dest: document -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.9 -``` - -The proxy should go back to the `epoll_wait` state and wait for more events. - -```bash -[DEBUG] Epoll wait -``` - -Keep testing the code by navigating across the file sever, and opening files. Make sure the proxy does not exit out of the program. - -## Conclusion - -This marks the end of Phase 0. - -The learning doesn't stop here though as in the next phase, as we’ll start building eXpServer. Phase 0 laid the foundation as to what is about to come next. Read more about Phase 1 [here](/roadmap/phase-1/). diff --git a/docs/roadmap/phase-0/stage-5.md b/docs/roadmap/phase-0/stage-5.md new file mode 100644 index 0000000..d1b3253 --- /dev/null +++ b/docs/roadmap/phase-0/stage-5.md @@ -0,0 +1,387 @@ +# Stage 5: TCP Proxy + +## Recap + +- In the previous stage, we modified our TCP server code to handle multiple clients simultaneously using epoll + +## Learning Objectives + +- We will combine the functionalities of a TCP server from [Stage 1](/roadmap/phase-0/stage-1) and client from [Stage 3](/roadmap/phase-0/stage-3) to make a TCP [proxy](https://en.wikipedia.org/wiki/Proxy_server) which will relay communication between a web browser and a python file server. + +## Introduction + +[Proxy](https://en.wikipedia.org/wiki/Proxy_server) is a intermediary which sits in between a client and an [upstream server](https://en.wikipedia.org/wiki/Upstream_server) and relays communication between them. When a client makes a request to access a resource (such as a website or a file), it connects to the proxy server instead of directly connecting to the target server. The proxy server then forwards the client's request to the target server, retrieves the response, and sends it back to the client. + +In this stage, our client will be a web browser and upstream server will be a python file server serving a folder on our local hard drive. Instead of the web browser directly connecting with the python file server, it makes a connection to the proxy which in turn will connect to the python server to relay the request from the browser. + +![tcp-proxy.png](/assets/stage-4/tcp-proxy.png) + +Before we get into the implementation of the proxy, lets have a look at what we are trying to achieve. We will start by running a python file server. + +Open a terminal and navigate to the folder you want to serve. Run the following command below to start a simple python file server: + +This command starts [Python's inbuilt HTTP server module](https://docs.python.org/3/library/http.server.html) which will serve the files in the folder it started in. + +```bash +python -m http.server 3000 +``` + +Now that the local file server is running on port 3000, we can connect to it using a browser by going to `localhost:3000`. + +![python-server.png](/assets/stage-4/python-server.png) + +Right now, the client (web browser), is directly accessing the file server. Our goal is to modify the TCP server code from Stage 3 to turn it into a TCP proxy server, so that all the communication between the client and upstream server goes though the proxy. + +## Implementation + +![implementation.png](/assets/stage-4/implementation.png) + +There will be few major changes in the structure of the code from previous stage where we wrote the entire implementation in `main()`. Thus, for this stage, we recommended working on a new separate file; let’s call it `tcp_proxy.c`. + +In addition to the previous definitions in `tcp_server.c`, add a global definition at the top of the file for the upstream port number that we will be serving the python file server from. + +::: tip NOTE +Add this to global definitions + +```c +#define UPSTREAM_PORT 3000 +``` + +::: + +We’ll start with encapsulation of the code written in the previous stage by placing them in different functions. Copy over the code from `tcp_server.c` and place it in the appropriate functions: + +```c +int create_loop() { + /* return new epoll instance */ +} + +void loop_attach(int epoll_fd, int fd, int events) { + /* attach fd to epoll */ +} + +int create_server() { + /* create listening socket and return it */ +} + +void loop_run(int epoll_fd) { + /* infinite loop and processing epoll events */ +} +``` + +Let’s focus on `loop_run(int epoll_fd)` now. In the previous stage, we had epoll events from two sources; the listen socket and the connection socket. Now there will be another socket that we will be adding to our epoll called as the **upstream socket**. + +The python file server is the upstream server in our case. When a user connects to the TCP proxy server to access files from the upstream server, the TCP proxy server will open a connection to the upstream server. All the communication sent to the proxy by the client will be relayed to the file server, and similarly data sent by the file server to the proxy (intended for the client) will be sent through this connection. + +The figure below illustrates the three different events that could occur in epoll, and how we should handle each one of them: + +![events.png](/assets/stage-4/events.png) + +```c +void loop_run(int epoll_fd) { + while (1) { + printf("[DEBUG] Epoll wait\n"); + + /* epoll wait */ + + for (...) { + if (/* event is on listen socket*/) + accept_connection(); // we will implement this later + else if (/* event is on connection socket */) + handle_client(); // we will implement this later + else if (/* event is on upstream socket */) + handle_upstream(); // we will implement this later + + } + } +} +``` + +Since we are aiming for concurrency, for each new client that connects to the proxy server, we need to create a new upstream link to connect with the file server. How can we effectively monitor the association between clients and their respective upstream links in scenarios where there are multiple clients? + +This is where [**route tables**](https://en.wikipedia.org/wiki/Routing_table) come into play. We store the connection socket FD and its corresponding upstream socket FD in a pair wise manner. + +Here are some global variables that could come handy: + +::: tip NOTE +Add this to global variables: + +```c +int listen_sock_fd, epoll_fd; +struct epoll_event events[MAX_EPOLL_EVENTS]; +int route_table[MAX_SOCKS][2], route_table_size = 0; +``` + +::: + + + +Now that we have that, we are ready to start accepting connection; so lets write the `accept_connection()` function. + +### `accept_connection()` + +`accept_connection()` takes `listen_sock_fd` as a param and do the following + +- Accept the client connection and create the connection socket FD `conn_sock_fd` +- Add the connection socket to epoll to monitor for events using `epoll_ctl()` +- Open up a connection to the upstream server using `connect_upstream()`, and add it to the epoll +- An entry will be added to the route table with the `conn_sock_fd` and it's corresponding `upstream_sock_fd` + +```c +void accept_connection(int listen_sock_fd) { + + int conn_sock_fd = /* accept client connection */ + + /* add conn_sock_fd to loop using loop_attach() */ + + // create connection to upstream server + int upstream_sock_fd = connect_upstream(); + + /* add upstream_sock_fd to loop using loop_attach() */ + + // add conn_sock_fd and upstream_sock_fd to routing table + route_table[route_table_size][0] = /* fill this */ + route_table[route_table_size][1] = /* fill this */ + route_table_size += 1; + +} +``` + +Try and implement the function `connect_upstream()` to create a connection to the upstream server. + +```c +int connect_upstream() { + + int upstream_sock_fd = /* create a upstrem socket */ + + struct sockaddr_in upstream_addr; + /* add upstream server details */ + + connect(/* connect to upstream server */); + + return upstream_sock_fd; + +} +``` + +### Milestone #1 + +Quick recap! + +- There are three different events that the proxy has to handle: + - Event on the listen socket when a client tries to connect with the proxy (intended to communicate with the upstream server) - `accept_connection()` + - Event on the connection socket when client sends message to proxy (intended for the upstream server) - `handle_client()` + - Event on the upstream socket when the upstream server sends message to proxy (intended for the client) - `handle_upstream()` + +--- + +Now that we have accepted the clients, we need to handle. We will create the `handle_client()` function to receive the messages from the client, and send it to the upstream server. + +### `handle_client()` + +This implementation is similar to how we handled clients in the previous stages with a few changes. + +```c +void handle_client(int conn_sock_fd) { + + int read_n = /* read message from client to buffer using recv */ + + // client closed connection or error occurred + if (read_n <= 0) { + close(conn_sock_fd); + return; + } + + /* print client message (helpful during milestone#2) */ + + /* find the right upstream socket from the route table */ + + // sending client message to upstream + int bytes_written = 0; + int message_len = read_n; + while (bytes_written < message_len) { + int n = send(/* found upstream socket */, buff + bytes_written, message_len - bytes_written, 0); + bytes_written += n; + } + +} +``` + +Find the upstream socket associated with `conn_sock_fd` and route table and start sending the message that we received from the client. + +::: tip NOTE +There is a noticeable change to the `send()` function compared to the last stage. The drawback with the old approach will appear when we are sending large amounts of data due to limitations in buffer size and network conditions. We will look into this in an exercise at the end. + +The new approach allows us to handle large data by sending it in smaller chunks, and lets us handle any errors. +::: + +--- + +When there are messages to be read from the upstream server, the `handle_upstream()` function kicks in and takes over. + +### `handle_upstream()` + +This function will be strikingly similar to `handle_client()` with few obvious changes. `handle_upstream()` will be responsible to receive messages from the upstream, find the matching connection, and send the message to the client. + +```c +void handle_upstream(int upstream_sock_fd) { + + int read_n = /* read message from upstream to buffer using recv */ + + // Upstream closed connection or error occurred + if (read_n <= 0) { + close(upstream_sock_fd); + return; + } + + /* find the right client socket from the route table */ + + /* send upstream message to client */ + +} +``` + +--- + +With encapsulation of code into multiple functions, our main function will look very minimal and it will be responsible for setting up and making the proxy run. + +```c +int main() { + + listen_socket_fd = /* create server using server_create() */ + + epoll_fd = /* create loop instance using loop_create() */ + + /* attach server to event loop using loop_attach() */ + + /* start event loop with loop_run() */ + +} +``` + +At the end, our code will have a structure similar to this: + +::: details expserver/tcp_proxy.c + +```c +/* includes, defines and global variables */ + +/* any helper functions you might write */ + +int connect_upstream() { + /* connect to upstream server */ +} + +void accept_connection(int listen_sock_fd) { + /* accept client connection */ +} + +void handle_client(int conn_sock_fd) { + /* handle client */ +} + +void handle_upstream(int upstream_sock_fd) { + /* handle upstream */ +} + +int create_loop() { + /* return new epoll instance */ +} + +void loop_attach(int epoll_fd, int fd, int events) { + /* attach fd to epoll */ +} + +int create_server() { + /* create listening socket and return it */ +} + +void loop_run(int epoll_fd) { + /* infinite loop and for loop*/ +} + +int main() { + /* initialize proxy */ +} +``` + +::: + +::: warning +There is no restriction to just these functions. Feel free to create additional helper functions as needed to suit your requirements. +::: + +--- + +### Milestone #2 + +To test our code, we will essentially try to replicate what we saw in the introduction, i.e. a Python file server. But there, our browser being the client, had a direct connection to the file server. + +Right now, we want our browser to connect to the proxy server, which in turn will proxy the request to the python server. + +Start the python file server to serve the `expserver/` directory: + +```bash +python -m http.server 3000 +``` + +The file server will start with the following message if successful: + +```bash +Serving HTTP on 0.0.0.0 port 3000 (http://0.0.0.0:3000/) ... +``` + +Compile and run `tcp_server.c`. The terminal should output the following: + +```bash +[INFO] Server listening on port 8080 +[DEBUG] Epoll wait +``` + +Now, the fileserver is active on port `3000` and the proxy is running on port `8080`. In the introduction demo, we connected to the file server directly by accessing `localhost:3000` from our browser (client). This time, we’ll connect to the proxy by looking up `localhost:8080` in the browser. + +Both links should lead to the same file server; they're just different paths. If this works as expected, it indicates that our proxy is functioning perfectly! + +If you included a `printf` statement in `handle_client()` to print the client message, you would get a [HTTP](https://nl.wikipedia.org/wiki/Hypertext_Transfer_Protocol) request message in the proxy terminal when the client visits `localhost:8080`. + +```bash +[CLIENT MESSAGE] GET / HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +sec-ch-ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122" +sec-ch-ua-mobile: ?0 +sec-ch-ua-platform: "Windows" +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 +Sec-Fetch-Site: none +Sec-Fetch-Mode: navigate +Sec-Fetch-User: ?1 +Sec-Fetch-Dest: document +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.9 +``` + +The proxy should go back to the `epoll_wait` state and wait for more events. + +```bash +[DEBUG] Epoll wait +``` + +Keep testing the code by navigating across the file sever, and opening files. Make sure the proxy does not exit out of the program. + +## Experiments + +### Experiment #1 + +In our `handle_client()` function, we modified the send() functionality. Try switching this out for the old approach that we used in Stage 3 and test if everything works properly. + +## Conclusion + +This marks the end of Phase 0. + +The learning doesn't stop here though as in the next phase, as we’ll start building eXpServer. Phase 0 laid the foundation as to what is about to come next. Read more about Phase 1 [here](/roadmap/phase-1/). diff --git a/docs/roadmap/phase-1/stage-10.md b/docs/roadmap/phase-1/stage-10.md new file mode 100644 index 0000000..b080418 --- /dev/null +++ b/docs/roadmap/phase-1/stage-10.md @@ -0,0 +1 @@ +# Stage 10: File Module diff --git a/docs/roadmap/phase-1/stage-5.md b/docs/roadmap/phase-1/stage-5.md deleted file mode 100644 index ede2087..0000000 --- a/docs/roadmap/phase-1/stage-5.md +++ /dev/null @@ -1,734 +0,0 @@ -# Stage 5: Server & Client Modules - -## Recap - -- We learnt about `xps_buffer` -- We learnt how to use `xps_loggger` -- We learnt how to use the `vec` library - -## Introduction - -We begin building eXpServer with the server and client modules. Create a folder in the `expserver` directory and name it `network`. - -In Phase 0, our codebase was compact and self-contained. Functions were developed within individual files and utilized exclusively within those contexts. However, as our project will grow in size and complexity, there will be a need to share code components across various files. - -In C programming, header files (`.h`) serve this purpose by declaring functions, data structures, and constants shared across multiple source code files. They contain function prototypes, type definitions, and pre-processor directives. - -Almost all header files will be given to you. This will act as a blueprint to the functions that will have to implemented. As mentioned before, header files will also have user defined data structures that you will be using to build eXpServer. - -### File structure - -![filestructure.png](/assets/stage-5/filestructure.png) - -To build the server and client modules, we will be working with several files as indicated in the file structure above. - -The `main.c` file will be starting point of the code. It will be responsible for starting the server and loop. - -Each `.c` file that we work on (apart from `main.c`) will have an associated header file. The code for these `.h` files will be given to you before we work with it. - -In the previous phase we were limited to one server instance listening on one particular port. In reality, a web server can be configured to listen on multiple ports. This limitation will be addressed in this stage. By the end of this stage we would have modularised the server and client code. And by doing this we can easily spin up multiple servers listening on various ports. - -Here is the overall call stack for Stage 5. This is provide an overview of what the code does. It shows what function calls happen and in what order it happens. - -```text -main() - loop_create() - xps_server_create() - xps_socket_create() - loop_attach() - loop_run() - epoll_wait() - xps_server_loop_read_handler() - xps_server_connection_handler() - accept() - xps_socket_create() - xps_client_create() - loop_attach() - xps_client_loop_read_handler() - xps_client_read_handler() - xps_client_read() - recv() - strrev() - xps_client_write() - send() -``` - -Don’t worry if this is very confusing now. We’ll break it down slowly and build it step by step. - -## Implementation - -The figure below gives a high level view of what we’ll be doing in this stage: - -![implementation.png](/assets/stage-5/implementation.png) - -`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 - -```c -#ifndef XPS_H -#define XPS_H - -#define DEFAULT_BACKLOG 64 -#define MAX_EPOLL_EVENTS 16 -#define DEFAULT_BUFFER_SIZE 100000 - -// Error constants -#define OK 0 -#define E_FAIL -1 -#define E_AGAIN -2 -#define E_CLOSE -3 -#define E_NOTFOUND -4 -#define E_PERMISSION -5 -#define E_EOF -6 - -// Types -typedef unsigned char u_char; -typedef unsigned int u_int; - -struct xps_buffer_s; -struct xps_socket_s; -struct xps_server_s; -struct xps_client_s; - -typedef struct xps_buffer_s xps_buffer_t; -typedef struct xps_socket_s xps_socket_t; -typedef struct xps_server_s xps_server_t; -typedef struct xps_client_s xps_client_t; - -typedef void (*xps_server_connection_handler_t)(xps_server_t *server, int status); -typedef void (*xps_client_read_handler_t)(xps_client_t *client, int status); - -#endif -``` - -::: - -We will be constantly modifying/adding to this file in each stage to accommodate for newer types and constants. - -### `xps_socket.h & xps_socket.c` - -`xps_socket` module will deal with all things to do with sockets, i.e. creation, deletion etc. - -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 - -```c -#ifndef XPS_SOCKET_H -#define XPS_SOCKET_H - -struct xps_socket_s { - int fd; -}; - -/** - * @brief Creates a socket instance. - * - * @param fd File descriptor of the socket. If negative, a new socket will be created. - * @return Pointer to the created socket instance, or NULL if an error occurs. - */ -xps_socket_t *xps_socket_create(int fd); - -/** - * @brief Destroys a socket instance. - * - * This function closes the socket associated with the specified socket instance and frees its memory. - * - * @param sock pointer to the socket instance to destroy. - */ -void xps_socket_destroy(xps_socket_t *sock); - -#endif -``` - -::: - -Now that we have the function signature, we can work on its implementation. Create a file named `xps_socket.c` under the network folder. - -There are two ways in which a program interacts with sockets when performing I/O operations: - -1. **Blocking I/O** - 1. When a program performs a read or write operation on a socket, the program waits until the operation is completed before moving on to execute the next line of code - 2. If there is no data available to read from the socket, a read operation will block (i.e., pause execution) until data arrives -2. **Non-blocking I/O** - 1. When a program performs a read or write operation on a socket, the program continues execution immediately without waiting for the operation to complete - 2. Non-blocking I/O allows the program to perform other tasks while waiting for I/O operations to complete - -If you are familiar with JavaScript, you may associate this with synchronous and asynchronous functions. - -Making a socket non-blocking is crucial as it prevents deadlocks, provides concurrency and allows for better use of system resources. To make a socket non-blocking, we will use the `fcntl()` function provided by the `fcntl.h` header file. This function is used for manipulation of file descriptors. Read more about them [here](https://man7.org/linux/man-pages/man2/fcntl.2.html). - -::: warning -While handling errors, do not forget to close the `fd`. -::: - -As we move forward, the pattern of create and destroy functions will be repeated. But what are create and destroy functions you may ask. - -Each entity will have a create and destroy function. Create functions are responsible for creating an instance of the entity, allocating it memory and assigning it initial values. Destroy function is called on an entity instance when it is no longer needed. This will free the memory and do some module specific chores. - -Let’s start by writing the `xps_socket_create()` function. - -::: details expserver/network/xps_socket.c - -```c -xps_socket_t *xps_socket_create(int fd) { - // If fd is negative, create a tcp socket - if (fd < 0) { - logger(LOG_DEBUG, "xps_socket_create()", "no socket fd provided. creating new socket"); - fd = /* create new socket using socket() */ - } - - // Error creating socket - if (fd < 0) { - logger(LOG_ERROR, "xps_socket_create()", "failed to create socket"); - return NULL; - } - - // Making socket non blocking using fcntl() - int flags = /* get socket flags */ - if (flags < 0) { - logger(LOG_DEBUG, "xps_socket_create()", - "failed to make socket non blocking. failed to get flags"); - perror("Error message"); - close(fd); - return NULL; - } - - if (fcntl(/* set fd as non blocking (append 'non-blocking flag' to flags) */) < -1) { - logger(LOG_DEBUG, "xps_socket_create()", - "failed to make socket non blocking. failed to set flags"); - perror("Error message"); - close(fd); - return NULL; - } - - // Alloc memory for socket instance - xps_socket_t *sock = (xps_socket_t *)malloc(sizeof(xps_socket_t)); - if (sock == NULL) { - logger(LOG_ERROR, "xps_socket_create()", - "failed to alloc memory for socket instance. malloc() returned NULL"); - close(fd); - return NULL; - } - - sock->fd = fd; - - return sock; -} -``` - -::: - -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 - -```c -void xps_socket_destroy(xps_socket_t *sock) { - if (sock == NULL) { - logger(LOG_ERROR, "xps_socket_destory()", "failed to destroy socket. sock is NULL"); - return; - } - - close(sock->fd); - free(sock); - - logger(LOG_DEBUG, "xps_socket_destroy()", "destroyed socket"); -} -``` - -::: - -**Create function pattern** - -- Check for errors in params. Exit by undoing previous steps and returning NULL -- Allocate memory for instance -- Assign values -- Log each event - -**Destroy function pattern** - -- Check for errors in params -- Free memory and do other chores -- Log each event - -### `xps_server.h & xps_server.c` - -`xps_server` module will deal with all things to do with servers, i.e. creation, deletion etc. - -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 - -```c -#ifndef XPS_SERVER_H -#define XPS_SERVER_H - -#include "../lib/vec/vec.h" -#include "../xps.h" - -struct xps_server_s { - int port; - xps_socket_t *sock; - vec_void_t clients; - xps_server_connection_handler_t connection_cb; -}; - -xps_server_t *xps_server_create(int port, xps_server_connection_handler_t connection_cb); -void xps_server_destroy(xps_server_t *server); -void xps_server_loop_read_handler(xps_server_t *server); - -#endif -``` - -::: - -Each server instance will have the following: - -- `int port`: an integer port number -- `xps_socket_t *sock`: pointer to an instance of socket associated with the server -- `vec_void_t clients`: list of clients handled by the server -- `xps_server_connection_handler_t connection_cb`: a callback function called when the server receives a new client connection - -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 - -```c -xps_server_t *xps_server_create(int port, xps_server_connection_handler_t connection_cb) { - - /* check if port is a valid port number */ - - /* setup server address */ - - xps_socket_t *sock = /* create socket instance using xps_socket_create() */ - if (sock == NULL) { - logger(LOG_ERROR, "xps_server_create()", - "failed to create socket instance. xps_socket_create() returned NULL"); - return NULL; - } - - // setsockopt() for reusing ports - const int enable = 1; - if (setsockopt(sock->fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) { - logger(LOG_ERROR, "xps_server_create()", "failed setsockopt(). reusing ports"); - perror("Error message"); - xps_socket_destroy(sock); - return NULL; - } - - /* bind socket to port */ - - /* start listening on port */ - - xps_server_t *server = /* allocate memory for server instance using malloc */ - if(server == NULL) { - logger(LOG_ERROR, "xps_server_create()", "failed to server instance. malloc() returned NULL"); - xps_socket_destroy(sock); - return NULL; - } - - server->port = /* fill this */ - server->sock = /* fill this */ - server->connection_cb = /* fill this */ - vec_init(&(server->clients)); - - logger(LOG_DEBUG, "xps_server_create()", "created server on port %d", port); - - return server; - -} -``` - -::: - -This destroy function might be a bit tricky on first glance as `xps_server_t` structure has members of different types. Let’s break it down: - -- `server→sock` can be destroyed using `xps_socket_destroy()` -- `server→clients` should be destroyed individually by iterating through each one of them. Code for this section will be provided as we are yet to do the client code. We destroy each client using `xps_client_destroy()` which will be implemented in the upcoming section. - -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 - -```c -void xps_server_destroy(xps_server_t *server) { - - /* validate params */ - - /* destory socket using xps_socket_destroy() */ - - // Destory clients - for (int i = 0; i < server->clients.length; i++) { - xps_client_t *client = (xps_client_t *)(server->clients.data[i]); - if (client != NULL) - xps_client_destroy(client); // we will implement this later - } - - vec_deinit(&(server->clients)); - - int port = server->port; - - /* free server */ - - logger(LOG_DEBUG, "xps_loop_destroy()", "destroyed server on port %d", port); - -} -``` - -::: - -The above code snippet shows us how we can destroy objects stored in a `vec` list. - -`server→clients` is a `vec` list of type `vec_void_t`. It is filled with all the clients that are associated with the server. Each client instance will be of type `xps_client_t`. So, while iterating through the list, we first have to typecast the void instance to `xps_client_t`. - -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 - -```c -void xps_server_loop_read_handler(xps_server_t *server) { - server->connection_cb(server, OK); -} -``` - -::: - -You might be thinking, why this function is necessary. You will be seeing it used by the event loop to notify the server of read events on its listening socket. Even though it is possible for event loop to directly invoke the `connection_cb()` function, the wrapper `xps_server_loop_read_handler()` is primarily a convention used for consistency purposes as other modules also has similar functions that require some processing other than just invoking the callback. - -`connection_cb()`’s implementation will be done in the `main.c` file. It takes the server and a integer status as its parameters. The possible values of status is defined in `xps.h`. When we progress further in the course, status codes will be used everywhere. But right now, we only utilize `OK`. - -Diagram here - ---- - -### Milestone #1 - -Let’s have a recap of what we have done till now. - -- We now have a socket module that will take care of creating, destroying and making the sockets non blocking. -- We also have a `xps_server_create()` function that will take a `port` number and a `connection_cb()` function, and create an instance of a server that will be listening on the given port. -- We also have the `xps_server_destroy()` function which can close the socket and free the memory allocated when the server is no longer in use. - ---- - -### `xps_client.h & xps_client.c` - -`xps_client` module will deal with all things to do with clients, i.e. creation, deletion, writing etc. - -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 - -```c -#ifndef XPS_CLIENT_H -#define XPS_CLIENT_H - -#include "../xps.h" - -struct xps_client_s { - xps_socket_t *sock; - xps_server_t *server; - xps_client_read_handler_t read_cb; -}; - -xps_client_t *xps_client_create(xps_server_t *server, xps_socket_t *sock, - xps_client_read_handler_t read_cb); -void xps_client_destroy(xps_client_t *client); -void xps_client_write(xps_client_t *client, xps_buffer_t *buff); -xps_buffer_t *xps_client_read(xps_client_t *client); -void xps_client_loop_read_handler(xps_client_t *client); - -#endif -``` - -::: - -Each client instance will have the following: - -- `xps_socket_t *sock`: pointer to an instance of socket associated with the client -- `xps_server_t *server`: server that the client belongs to -- `xps_client_read_handler_t read_cb`: a callback function called when there is something available to read from the client - -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 - -```c -xps_client_t *xps_client_create(xps_server_t *server, xps_socket_t *sock, xps_client_read_handler_t read_cb) { - - /* validate params */ - - xps_client_t *client = /* allocate memory for client instance using malloc*/ - - /* populate client object */ - - return client; - -} -``` - -::: - -::: warning -Moving forward, the documentation or code snippets may not specify error-handling locations. It will be our duty to anticipate potential error points and manage them accordingly. Remember to handle errors proactively and utilise the logger utility extensively. -::: - -Recall how in the `xps_server_destroy()` function we used `xps_client_destroy()` to destroy individual clients by passing the client object to it. We will implement this now. - -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 - -```c -void xps_client_destroy(xps_client_t *client) { - - /* validate params */ - - for (int i = 0; i < client->server->clients.length; i++) { - xps_client_t *curr = (xps_client_t *)(client->server->clients.data[i]); - if (/* fill this */) { - client->server->clients.data[i] = NULL; - break; - } - } - - /* destroy socket */ - - /* free client */ - -} -``` - -::: - -::: danger QUESTION -Why do you think it is necessary to set NULL in server’s client list while destroying the client? Why can’t we just remove the item from the list instead of setting it to NULL? -::: - -Now that we have a client instance, we need a way (functions) to communicate (read and write) with it. This is where `xps_client_read()` and `xps_client_write()` come in. - -Refer the `handle_client()` function from the previous stage and modify the functions below. - -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](/guides/references/xps_buffer). - -::: details expserver/network/xps_client.c - -```c -void xps_client_write(xps_client_t *client, xps_buffer_t *buff) { - - /* send message to the client using send */ - -} - -xps_buffer_t *xps_client_read(xps_client_t *client) { - - /* read message from client to buffer using recv */ - - /* return a buffer instance type xps_buffer_t if success or NULL if error */ - -} -``` - -::: - -Create a simple function, `xps_client_loop_read_handler()` to call the client read callback. - -::: details expserver/network/xps_client.c - -```c -void xps_client_loop_read_handler(xps_client_t *client) { - - /* fill this */ - -} -``` - -::: - -### `main.c` - -In `main.c`, we'll initialize eXpServer, utilizing some functions from the previous stage. In this stage, our main function will contain event loop related functions only. - -As we implemented a proxy last time, code related to upstream wont be relevant as of now. We will get to that when we work on the upstream module in Stage 9. - -We’ll start with the `main()` function because that is where the execution starts. What might be the `main()` function’s responsibilities? - -- To create a loop instance - `loop_create()` -- To start server(s) - `xps_server_create()` -- To start the event loop - `loop_run()` - -::: tip -Always try to keep the main function simple and distribute the work into dedicated functions. -::: - -::: details expserver/main.c - -```c -// Global variables -int epoll_fd; -struct epoll_event events[MAX_EPOLL_EVENTS]; - -xps_server_t *servers[10]; -int n_servers = 0; - -int main() { - - /* create loop using loop_create()*/ - - // Create servers on ports 8001, 8002, 8003, 8004 - n_servers = 4; - for (int i = 0; i < n_servers; i++) { - int port = 8001 + i; - servers[i] = xps_server_create(port, **xps_server_connection_handler**); - loop_attach(epoll_fd, servers[i]->sock->fd, EPOLLIN); - logger(LOG_INFO, "main()", "Server listening on port %d", port); - } - - /* start event loop */ - -} -``` - -::: - -The implementations of `loop_attach()` and `loop_detach()` remain unchanged from the previous stage. - -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 - -```c -void xps_server_connection_handler(xps_server_t *server, int status) { - - /* accept connection */ - - xps_socket_t *sock = /* create socket instance using xps_socket_create() */ - - xps_client_t *client = xps_client_create(server, sock, **xps_client_read_handler**); - - // Adding client to clients list of server - vec_push(&(server->clients), (void *)client); - - /* attach client to loop using loop_attach() */ - -} -``` - -::: - -`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 - -```c -void xps_client_read_handler(xps_client_t *client, int status) { - - xps_buffer_t *buff = /* read message from client using xps_client_read() */ - - /* error handling (detach client fd from loop and destory client) */ - - // Add null terminator to end of message string - buff->data[buff->len] = 0; - - printf("%s", buff->data); - - /* reverse message */ - - /* send reversed message to client using xps_client_write() */ - -} -``` - -::: - -Now that we have all the functions, we can start writing the `loop_run()` function which is responsible for catching all events and calling the corresponding callbacks. The callback functions (handlers) that we have written so far will start to make sense as we implement `loop_run()` . - -Similar to `loop_run()` in last stage, we get events from the server and the client. Identifying if it is a server or a client event is the tricky part now. - -In case of a server event, you would have to loop through all the server instances and find the server with the matching socket fd. When the server is found, call the `xps_server_loop_read_handler()` to take care of the event. - -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 - -```c -void loop_run(int epoll_fd) { - - while (1) { - - /* epoll wait */ - - for (/* loop though epoll events */) { - - int curr_fd = events[i].data.fd; - - // Check if event is on a server - for (/* loop through servers */) { - if (servers[i]->sock->fd == curr_fd) { - - // server with event found - - /* fill this */ - - } - } - - // Check if event is on a client - for (/* loop through servers */) { - for (/* loop through clients of this server */) { - xps_client_t *curr_client = (xps_client_t *)(servers[i]->clients.data[j]); - if (curr_client != NULL && curr_fd == curr_client->sock->fd) { - - // client with event found - - /* fill this */ - - } - } - } - - } - -} -``` - -::: - -::: tip NOTE -Take a note of the type casting on client from server’s clients list to `xps_client_t` in the client loop. -::: - -### Milestone #2 - -Time to test the code! - -The following command can be used to compile all the code in stage 5: - -```bash -gcc -g -o xps main.c network/xps_socket.c network/xps_server.c network/xps_client.c lib/vec/vec.c utils/xps_buffer.c utils/xps_logger.c -``` - -As we have a lot of files to compile this time around, we suggest using a script file to make the process easier. You will find a `build.sh` file in the `expserver/` directory. Copy over the command and use the following to run the script: - -```bash -bash build.sh -``` - -Running the output file `./xps` should give you the following output: - -```bash -[INFO] main() : Server listening on port 8001 -[INFO] main() : Server listening on port 8002 -[INFO] main() : Server listening on port 8003 -[INFO] main() : Server listening on port 8004 -``` - -Don’t worry if your code does the produce the expected output in the first attempt. Debugging and figuring out the error is as important as writing the code itself. The `xps_logger` utility and GDB are your best friends here. - -## Conclusion - -That’s it! We understand that this stage was longer and complex that the ones before. In the next stage, we will work on the core and loop modules; arguably the two most important modules for the functioning of eXpServer. diff --git a/docs/roadmap/phase-1/stage-6.md b/docs/roadmap/phase-1/stage-6.md index f866dc2..8dde34d 100644 --- a/docs/roadmap/phase-1/stage-6.md +++ b/docs/roadmap/phase-1/stage-6.md @@ -1,497 +1,734 @@ -# Stage 6: Core & Loop Modules +# Stage 6: Server & Client Modules ## Recap -- We created server and client modules +- We learnt about `xps_buffer` +- We learnt how to use `xps_loggger` +- We learnt how to use the `vec` library ## Introduction -In this stage, we will be modularizing the code further by creating two new modules: +We begin building eXpServer with the server and client modules. Create a folder in the `expserver` directory and name it `network`. -1. Core module -2. Loop module +In Phase 0, our codebase was compact and self-contained. Functions were developed within individual files and utilized exclusively within those contexts. However, as our project will grow in size and complexity, there will be a need to share code components across various files. -The core module, as its name implies, will act as the central hub of the server, where all components will be connected to, including the loop. The `main()` function in `main.c` will create an instance of core and subsequently ‘start’ it. The core should take care of everything else from that point onwards. +In C programming, header files (`.h`) serve this purpose by declaring functions, data structures, and constants shared across multiple source code files. They contain function prototypes, type definitions, and pre-processor directives. -You can probably predict what the loop module will contain. +Almost all header files will be given to you. This will act as a blueprint to the functions that will have to implemented. As mentioned before, header files will also have user defined data structures that you will be using to build eXpServer. -New modules comes with new structures, thus `xps_core_s` and `xps_loop_s` have been added to core and loop header files. +### File structure + +![filestructure.png](/assets/stage-5/filestructure.png) + +To build the server and client modules, we will be working with several files as indicated in the file structure above. + +The `main.c` file will be starting point of the code. It will be responsible for starting the server and loop. + +Each `.c` file that we work on (apart from `main.c`) will have an associated header file. The code for these `.h` files will be given to you before we work with it. + +In the previous phase we were limited to one server instance listening on one particular port. In reality, a web server can be configured to listen on multiple ports. This limitation will be addressed in this stage. By the end of this stage we would have modularised the server and client code. And by doing this we can easily spin up multiple servers listening on various ports. + +Here is the overall call stack for Stage 5. This is provide an overview of what the code does. It shows what function calls happen and in what order it happens. + +```text +main() + loop_create() + xps_server_create() + xps_socket_create() + loop_attach() + loop_run() + epoll_wait() + xps_server_loop_read_handler() + xps_server_connection_handler() + accept() + xps_socket_create() + xps_client_create() + loop_attach() + xps_client_loop_read_handler() + xps_client_read_handler() + xps_client_read() + recv() + strrev() + xps_client_write() + send() +``` + +Don’t worry if this is very confusing now. We’ll break it down slowly and build it step by step. + +## Implementation + +The figure below gives a high level view of what we’ll be doing in this stage: + +![implementation.png](/assets/stage-5/implementation.png) + +`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 ```c -// From xps_core.h -struct xps_core_s { - xps_loop_t *loop; - vec_void_t servers; -}; +#ifndef XPS_H +#define XPS_H + +#define DEFAULT_BACKLOG 64 +#define MAX_EPOLL_EVENTS 16 +#define DEFAULT_BUFFER_SIZE 100000 + +// Error constants +#define OK 0 +#define E_FAIL -1 +#define E_AGAIN -2 +#define E_CLOSE -3 +#define E_NOTFOUND -4 +#define E_PERMISSION -5 +#define E_EOF -6 + +// Types +typedef unsigned char u_char; +typedef unsigned int u_int; + +struct xps_buffer_s; +struct xps_socket_s; +struct xps_server_s; +struct xps_client_s; + +typedef struct xps_buffer_s xps_buffer_t; +typedef struct xps_socket_s xps_socket_t; +typedef struct xps_server_s xps_server_t; +typedef struct xps_client_s xps_client_t; + +typedef void (*xps_server_connection_handler_t)(xps_server_t *server, int status); +typedef void (*xps_client_read_handler_t)(xps_client_t *client, int status); + +#endif +``` + +::: + +We will be constantly modifying/adding to this file in each stage to accommodate for newer types and constants. + +### `xps_socket.h & xps_socket.c` + +`xps_socket` module will deal with all things to do with sockets, i.e. creation, deletion etc. + +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. -// From xps_loop.h -struct xps_loop_s { - int epoll_fd; - struct epoll_event events[MAX_EPOLL_EVENTS]; - vec_void_t handles; +::: details expserver/network/xps_socket.h + +```c +#ifndef XPS_SOCKET_H +#define XPS_SOCKET_H + +struct xps_socket_s { + int fd; }; + +/** + * @brief Creates a socket instance. + * + * @param fd File descriptor of the socket. If negative, a new socket will be created. + * @return Pointer to the created socket instance, or NULL if an error occurs. + */ +xps_socket_t *xps_socket_create(int fd); + +/** + * @brief Destroys a socket instance. + * + * This function closes the socket associated with the specified socket instance and frees its memory. + * + * @param sock pointer to the socket instance to destroy. + */ +void xps_socket_destroy(xps_socket_t *sock); + +#endif ``` -### File structure +::: -![filestructure.png](/assets/stage-6/filestructure.png) +Now that we have the function signature, we can work on its implementation. Create a file named `xps_socket.c` under the network folder. -Find below the updated `xps.h` file. New additions to the file are indicated in green: +There are two ways in which a program interacts with sockets when performing I/O operations: -- expserver/xps.h +1. **Blocking I/O** + 1. When a program performs a read or write operation on a socket, the program waits until the operation is completed before moving on to execute the next line of code + 2. If there is no data available to read from the socket, a read operation will block (i.e., pause execution) until data arrives +2. **Non-blocking I/O** + 1. When a program performs a read or write operation on a socket, the program continues execution immediately without waiting for the operation to complete + 2. Non-blocking I/O allows the program to perform other tasks while waiting for I/O operations to complete - ```c - #ifndef XPS_H - #define XPS_H +If you are familiar with JavaScript, you may associate this with synchronous and asynchronous functions. - #define DEFAULT_BACKLOG 64 - #define MAX_EPOLL_EVENTS 16 - #define DEFAULT_BUFFER_SIZE 100000 +Making a socket non-blocking is crucial as it prevents deadlocks, provides concurrency and allows for better use of system resources. To make a socket non-blocking, we will use the `fcntl()` function provided by the `fcntl.h` header file. This function is used for manipulation of file descriptors. Read more about them [here](https://man7.org/linux/man-pages/man2/fcntl.2.html). - // Error constants - #define OK 0 - #define E_FAIL -1 - #define E_AGAIN -2 - #define E_CLOSE -3 - #define E_NOTFOUND -4 - #define E_PERMISSION -5 - #define E_EOF -6 +::: warning +While handling errors, do not forget to close the `fd`. +::: - // Types - typedef unsigned char u_char; - typedef unsigned int u_int; +As we move forward, the pattern of create and destroy functions will be repeated. But what are create and destroy functions you may ask. - typedef enum { // [!code ++] - HANDLE_TCP_CLIENT, // [!code ++] - HANDLE_TCP_SERVER, // [!code ++] - } xps_loop_handle_type_t; // [!code ++] +Each entity will have a create and destroy function. Create functions are responsible for creating an instance of the entity, allocating it memory and assigning it initial values. Destroy function is called on an entity instance when it is no longer needed. This will free the memory and do some module specific chores. - struct xps_buffer_s; - struct xps_socket_s; - struct xps_server_s; - struct xps_client_s; - struct xps_core_s; // [!code ++] - struct xps_loop_s; // [!code ++] - struct xps_loop_handle_s; // [!code ++] +Let’s start by writing the `xps_socket_create()` function. - typedef struct xps_buffer_s xps_buffer_t; - typedef struct xps_socket_s xps_socket_t; - typedef struct xps_server_s xps_server_t; - typedef struct xps_client_s xps_client_t; - typedef struct xps_core_s xps_core_t; // [!code ++] - typedef struct xps_loop_s xps_loop_t; // [!code ++] - typedef struct xps_loop_handle_s xps_loop_handle_t; // [!code ++] +::: details expserver/network/xps_socket.c - typedef void (*xps_server_connection_handler_t)(xps_server_t *server, int status); - typedef void (*xps_client_read_handler_t)(xps_client_t *client, int status); +```c +xps_socket_t *xps_socket_create(int fd) { + // If fd is negative, create a tcp socket + if (fd < 0) { + logger(LOG_DEBUG, "xps_socket_create()", "no socket fd provided. creating new socket"); + fd = /* create new socket using socket() */ + } - #endif - ``` + // Error creating socket + if (fd < 0) { + logger(LOG_ERROR, "xps_socket_create()", "failed to create socket"); + return NULL; + } -> ::: tip NOTE -> You are free to modify the code in any file you want, including the `xps.h` file. The documentation is just a here to guide and nudge you in the right direction and not restrict you in any way. Feel free to add new functions, global variables, constants, structs. Just make sure you retain the content you write when you are copying code snippets like above. -> ::: + // Making socket non blocking using fcntl() + int flags = /* get socket flags */ + if (flags < 0) { + logger(LOG_DEBUG, "xps_socket_create()", + "failed to make socket non blocking. failed to get flags"); + perror("Error message"); + close(fd); + return NULL; + } -## Implementation + if (fcntl(/* set fd as non blocking (append 'non-blocking flag' to flags) */) < -1) { + logger(LOG_DEBUG, "xps_socket_create()", + "failed to make socket non blocking. failed to set flags"); + perror("Error message"); + close(fd); + return NULL; + } -Let’s have a clear picture before we move forward with the code. + // Alloc memory for socket instance + xps_socket_t *sock = (xps_socket_t *)malloc(sizeof(xps_socket_t)); + if (sock == NULL) { + logger(LOG_ERROR, "xps_socket_create()", + "failed to alloc memory for socket instance. malloc() returned NULL"); + close(fd); + return NULL; + } -![core.png](/assets/stage-6/core.png) + sock->fd = fd; -- The main function will create an instance of a core, and start it by providing the number of listening sockets. -- The core will be responsible for spinning up the servers and starting the loop. -- The loop module will contain the epoll, and have functions related to creating and destroying the loop instance, starting it, attaching and detaching events to loop, handling the events, etc. -- **A loop belongs to a core.** So when we create a loop instance, attach it to the core instance. + return sock; +} +``` -> ::: tip NOTE -> Each server instance will have its own listening socket. We know that we can spin up multiple servers, i.e. multiple listen sockets. From now onwards, these collection of servers running on different ports will be collectively called the **server**. -> ::: +::: -Let’s start with the loop module and move onto the core module. +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. -### `xps_loop.h & xps_loop.c` +::: details expserver/network/xps_socket.c + +```c +void xps_socket_destroy(xps_socket_t *sock) { + if (sock == NULL) { + logger(LOG_ERROR, "xps_socket_destory()", "failed to destroy socket. sock is NULL"); + return; + } -Recall in the last stage, we had code related to the epoll in the `main.c` file. Let’s make a module for it now. + close(sock->fd); + free(sock); -The code below has the contents of the header file for `xps_loop`. Have a look at it and make a copy of it in your codebase. + logger(LOG_DEBUG, "xps_socket_destroy()", "destroyed socket"); +} +``` -- expserver/core/xps_loop.h +::: - ```c - #ifndef XPS_LOOP_H - #define XPS_LOOP_H +**Create function pattern** - #include +- Check for errors in params. Exit by undoing previous steps and returning NULL +- Allocate memory for instance +- Assign values +- Log each event - #include "../lib/vec/vec.h" - #include "../xps.h" +**Destroy function pattern** - struct xps_loop_handle_s { - int fd; - void *item; - xps_loop_handle_type_t type; - }; +- Check for errors in params +- Free memory and do other chores +- Log each event - struct xps_loop_s { - int epoll_fd; - struct epoll_event events[MAX_EPOLL_EVENTS]; - vec_void_t handles; - }; +### `xps_server.h & xps_server.c` - xps_loop_t *xps_loop_create(); - void xps_loop_destroy(xps_loop_t *loop); - int xps_loop_attach(xps_loop_t *loop, xps_loop_handle_type_t type, void *item); - int xps_loop_detach(xps_loop_t *loop, xps_loop_handle_type_t type, int fd); - void xps_loop_run(xps_loop_t *loop); +`xps_server` module will deal with all things to do with servers, i.e. creation, deletion etc. - #endif - ``` +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. -Did you notice something new in `xps_loop_s` i.e. `vec_void_t handles`? +::: details expserver/network/xps_socket.h ```c -struct xps_loop_s { - int epoll_fd; - struct epoll_event events[MAX_EPOLL_EVENTS]; - vec_void_t handles; // [!code focus] +#ifndef XPS_SERVER_H +#define XPS_SERVER_H + +#include "../lib/vec/vec.h" +#include "../xps.h" + +struct xps_server_s { + int port; + xps_socket_t *sock; + vec_void_t clients; + xps_server_connection_handler_t connection_cb; }; + +xps_server_t *xps_server_create(int port, xps_server_connection_handler_t connection_cb); +void xps_server_destroy(xps_server_t *server); +void xps_server_loop_read_handler(xps_server_t *server); + +#endif ``` -Think of a **handle** as a wrapper around a file descriptor associated with a particular resource (TCP client or TCP server). It provides a convenient way to manage and interact with these resources within the event loop. +::: -Each handle is an object of `struct xps_loop_handle_s`. +Each server instance will have the following: -```c -struct xps_loop_handle_s { - int fd; - void *item; - xps_loop_handle_type_t type; -}; +- `int port`: an integer port number +- `xps_socket_t *sock`: pointer to an instance of socket associated with the server +- `vec_void_t clients`: list of clients handled by the server +- `xps_server_connection_handler_t connection_cb`: a callback function called when the server receives a new client connection -typedef enum { - HANDLE_TCP_CLIENT, - HANDLE_TCP_SERVER, -} xps_loop_handle_type_t; -``` +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. -Each handle can be associated with either a TCP client or a TCP server, as mentioned before. But how do we figure out which one of them the handle represents? `xps_loop_handle_type_t type` is utilized for this purpose. `void *item` is a pointer to an instance of `xps_server_t` or `xps_client_t`, depending on what the handle is created for. - -So instead of using objects of `xps_server_t` and `xps_client_t`, we will use `xps_loop_handle_s`. We can typecast `void *item` member in the object of `xps_loop_handle_s` to `xps_server_t` or `xps_client_t` depending on `xps_loop_handle_type_t type`. - -With this information, try to fill out the create and destroy functions associated with handles. The **function signatures** are given below. - -> ::: tip NOTE -> A function signature outlines the essential details of a function including its name, parameters, return type, and a brief description of its purpose. This signature serves as a reference for how the function should be called and what it does, without detailing the implementation. You will be seeing a lot of these from now onwards. -> ::: - -- expserver/core/xps_loop.c - - ```c - /** - * @brief Creates a handle for the given file descriptor, handle type, and item. - * - * This function creates a handle for the specified file descriptor, handle type, and item. - * - * @param fd The file descriptor. - * @param type The type of the handle. - * @param item Pointer to the item associated with the handle. - * @return Pointer to the created handle, or NULL if an error occurs. - */ - xps_loop_handle_t *xps_handle_create(int fd, xps_loop_handle_type_t type, void *item) { - - // Alloc memory for handle instance - xps_loop_handle_t *handle = (xps_loop_handle_t *)malloc(sizeof(xps_loop_handle_t)); - if (handle == NULL) { - logger(LOG_ERROR, "xps_handle_create()", - "failed to alloc memory for handle. malloc() returned NULL"); - return NULL; - } +::: details expserver/network/xps_server.c - /* assign values to handle */ +```c +xps_server_t *xps_server_create(int port, xps_server_connection_handler_t connection_cb) { - logger(LOG_DEBUG, "xps_handle_create()", "created handle"); + /* check if port is a valid port number */ - return handle; + /* setup server address */ + xps_socket_t *sock = /* create socket instance using xps_socket_create() */ + if (sock == NULL) { + logger(LOG_ERROR, "xps_server_create()", + "failed to create socket instance. xps_socket_create() returned NULL"); + return NULL; } - /** - * @brief Destroys a handle. - * - * This function destroys the handle and frees the memory associated with it. - * - * @param handle Pointer to the handle to be destroyed. - */ - void xps_handle_destroy(xps_loop_handle_t *handle) { - ... - } - ``` - -> ::: warning -> Don’t forget to handle params and other errors! -> ::: - -Let’s move onto loop functions. Previously, our `loop_create()` function in Stage 5 was as simple as returning a new epoll instance. The function becomes a bit more sophisticated with the introduction of the `xps_loop_s` structure. - -The new `xps_loop_create()` function should create an object of `xps_loop_s` (`xps_loop_t *loop`), attach a new epoll instance and return the object. - -- expserver/core/xps_loop.c - ```c - /** - * @brief Creates a new event loop. - * - * This function creates a new event loop and initializes its epoll instance. - * - * @return xps_loop_t* Pointer to the created event loop, or NULL if an error occurs. - */ - xps_loop_t *xps_loop_create() { - ... - } - ``` - -> ::: tip -> Use `vec_init(&(loop->handles))` for memory allocation. -> ::: - -The `xps_loop_destroy()` function frees up memory from the loop object. It clears the memory taken by the handles by iterating through all the handles in the loop object, and destroying them with the help of `xps_handle_destroy()`. - -- expserver/core/xps_loop.c - ```c - /** - * @brief Destroys an event loop. - * - * This function destroys the specified event loop and releases all associated resources. - * - * @param loop Pointer to the event loop to destroy. - */ - void xps_loop_destroy(xps_loop_t *loop) { - ... + // setsockopt() for reusing ports + const int enable = 1; + if (setsockopt(sock->fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) { + logger(LOG_ERROR, "xps_server_create()", "failed setsockopt(). reusing ports"); + perror("Error message"); + xps_socket_destroy(sock); + return NULL; } - ``` -> ::: tip -> Use `vec_deinit(&(loop->handles))` to deallocate memory. -> ::: + /* bind socket to port */ -Now that we've established the ability to create a loop, let's proceed to implement the functions for attaching and detaching items from the loop. We'll define `xps_loop_attach()` and `xps_loop_detach()` for this purpose. + /* start listening on port */ -`xps_loop_attach()` takes in a parameter `void *item`. This object could be an instance of client or server; typecast it to `xps_client_t` or `xps_server_t` depending on the `type` parameter. This will allow you to access the FD in it. After you get the FD, you can create a handle using `xps_handle_create()` and add it to list of handles in loop. + xps_server_t *server = /* allocate memory for server instance using malloc */ + if(server == NULL) { + logger(LOG_ERROR, "xps_server_create()", "failed to server instance. malloc() returned NULL"); + xps_socket_destroy(sock); + return NULL; + } -- expserver/core/xps_loop.c + server->port = /* fill this */ + server->sock = /* fill this */ + server->connection_cb = /* fill this */ + vec_init(&(server->clients)); - ```c - /** - * @brief Attaches an item to the event loop. - * - * This function attaches the specified item to the event loop based on its type. - * - * @param loop Pointer to the event loop. - * @param type Type of the handle. - * @param item Pointer to the item to attach. - * @return int E_FAIL if an error occurs, OK otherwise. - */ - int xps_loop_attach(xps_loop_t *loop, xps_loop_handle_type_t type, void *item) { + logger(LOG_DEBUG, "xps_server_create()", "created server on port %d", port); - /* handle params */ + return server; - int fd = -1; +} +``` - if (type == HANDLE_TCP_CLIENT) { - xps_client_t *client = (xps_client_t *)item; - fd = client->sock->fd; - } +::: - else if (/* TCP server */) { - /* fill this */ - } +This destroy function might be a bit tricky on first glance as `xps_server_t` structure has members of different types. Let’s break it down: - xps_loop_handle_t *handle = /* create handle */ +- `server→sock` can be destroyed using `xps_socket_destroy()` +- `server→clients` should be destroyed individually by iterating through each one of them. Code for this section will be provided as we are yet to do the client code. We destroy each client using `xps_client_destroy()` which will be implemented in the upcoming section. - // Add socket to epoll - struct epoll_event event; - event.events = /* fill this */ - event.data.fd = /* fill this */ - event.data.ptr = (void *)handle; +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). - /* attach event to epoll using epoll_ctl() */ +::: details expserver/network/xps_server.c - vec_push(&(loop->handles), (void *)handle); +```c +void xps_server_destroy(xps_server_t *server) { - logger(LOG_DEBUG, "xps_loop_attach()", "attached item to loop"); + /* validate params */ - return OK; + /* destory socket using xps_socket_destroy() */ + // Destory clients + for (int i = 0; i < server->clients.length; i++) { + xps_client_t *client = (xps_client_t *)(server->clients.data[i]); + if (client != NULL) + xps_client_destroy(client); // we will implement this later } - ``` -`xps_loop_detach()` takes in a FD that has to be detached from the loop. There are three things to do in `xps_loop_detach()` function: + vec_deinit(&(server->clients)); -- Find the handle to be deleted from the list of handles in the loop and set it to NULL -- Destroy the handle using `xps_handle_destroy()` -- Remove FD from the epoll + int port = server->port; -Doing the first two will remove the handle from everywhere. + /* free server */ -- expserver/core/xps_loop.c + logger(LOG_DEBUG, "xps_loop_destroy()", "destroyed server on port %d", port); - ```c - /** - * @brief Detaches an item from the event loop. - * - * This function detaches an item with the specified file descriptor from the event loop. - * - * @param loop Pointer to the event loop. - * @param type Type of the handle. - * @param fd File descriptor of the item to detach. - * @return int E_FAIL if an error occurs, OK otherwise. - */ - int xps_loop_detach(xps_loop_t *loop, xps_loop_handle_type_t type, int fd) { +} +``` - /* handle params */ +::: - vec_void_t *handles = &(loop->handles); +The above code snippet shows us how we can destroy objects stored in a `vec` list. - int handle_index = -1; - for (/* iterate through all the handles */) { - xps_loop_handle_t *handle = (xps_loop_handle_t *)((*handles).data[i]); - /* fill this */ - } +`server→clients` is a `vec` list of type `vec_void_t`. It is filled with all the clients that are associated with the server. Each client instance will be of type `xps_client_t`. So, while iterating through the list, we first have to typecast the void instance to `xps_client_t`. - // Setting NULL in handles list - (*handles).data[handle_index] = NULL; +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 - xps_loop_handle_t *handle = (xps_loop_handle_t *)((*handles).data[handle_index]); +::: details expserver/network/xps_server.c - /* destroy handle using xps_handle_destroy() */ +```c +void xps_server_loop_read_handler(xps_server_t *server) { + server->connection_cb(server, OK); +} +``` - /* remove fd from epoll */ +::: - logger(LOG_DEBUG, "xps_loop_detach()", "detached item from loop"); +You might be thinking, why this function is necessary. You will be seeing it used by the event loop to notify the server of read events on its listening socket. Even though it is possible for event loop to directly invoke the `connection_cb()` function, the wrapper `xps_server_loop_read_handler()` is primarily a convention used for consistency purposes as other modules also has similar functions that require some processing other than just invoking the callback. - return OK; +`connection_cb()`’s implementation will be done in the `main.c` file. It takes the server and a integer status as its parameters. The possible values of status is defined in `xps.h`. When we progress further in the course, status codes will be used everywhere. But right now, we only utilize `OK`. - } - ``` +Diagram here -Time to start the loop and handle the events. As we’ve done this multiple, you would be aware of what `xps_loop_run()` is responsible for. Refer to the previous stages if you want a recap. +--- -In Stage 5, determining whether an FD belongs to a server or client used to require iterating through all servers and clients. However, with handles, our task has significantly simplified. +### Milestone #1 -- expserver/core/xps_loop.c +Let’s have a recap of what we have done till now. - ```c - /** - * @brief Runs the event loop. - * - * This function runs the event loop and handles the events on it continuously - * - * @param loop Pointer to the event loop to run. - */ - void xps_loop_run(xps_loop_t *loop) { +- We now have a socket module that will take care of creating, destroying and making the sockets non blocking. +- We also have a `xps_server_create()` function that will take a `port` number and a `connection_cb()` function, and create an instance of a server that will be listening on the given port. +- We also have the `xps_server_destroy()` function which can close the socket and free the memory allocated when the server is no longer in use. - /* handle params */ +--- - while (loop) { +### `xps_client.h & xps_client.c` - /* epoll wait */ +`xps_client` module will deal with all things to do with clients, i.e. creation, deletion, writing etc. - for(/* loop through epoll events */) { +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. - struct epoll_event curr_event = loop->events[i]; +::: details expserver/network/xps_client.h - xps_loop_handle_t *curr_handle = (xps_loop_handle_t *)curr_event.data.ptr; +```c +#ifndef XPS_CLIENT_H +#define XPS_CLIENT_H - /* check if handle still exists */ +#include "../xps.h" - if(/* handle is of client type */) { - /* fill this */ - } - else if(/* handle if of server type */) { - /* fill this */ - } +struct xps_client_s { + xps_socket_t *sock; + xps_server_t *server; + xps_client_read_handler_t read_cb; +}; - } +xps_client_t *xps_client_create(xps_server_t *server, xps_socket_t *sock, + xps_client_read_handler_t read_cb); +void xps_client_destroy(xps_client_t *client); +void xps_client_write(xps_client_t *client, xps_buffer_t *buff); +xps_buffer_t *xps_client_read(xps_client_t *client); +void xps_client_loop_read_handler(xps_client_t *client); - } - ``` +#endif +``` -Explain why the handle might not exist +::: -> ::: tip -> Use `vec_filter_null(&(loop->handles))` to remove all the handles that were converted to NULL during `xps_loop_detach()`. -> ::: +Each client instance will have the following: ---- +- `xps_socket_t *sock`: pointer to an instance of socket associated with the client +- `xps_server_t *server`: server that the client belongs to +- `xps_client_read_handler_t read_cb`: a callback function called when there is something available to read from the client -### Milestone #1 +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 + +```c +xps_client_t *xps_client_create(xps_server_t *server, xps_socket_t *sock, xps_client_read_handler_t read_cb) { + + /* validate params */ + + xps_client_t *client = /* allocate memory for client instance using malloc*/ -Recap: + /* populate client object */ + + return client; + +} +``` -- *** +::: -Core being the most important module, will keep changing as we build more modules to eXpServer. +::: warning +Moving forward, the documentation or code snippets may not specify error-handling locations. It will be our duty to anticipate potential error points and manage them accordingly. Remember to handle errors proactively and utilise the logger utility extensively. +::: -The `main()` function in the`main.c` file will create an instance of core with `xps_core_create()` and start it using `xps_core_start()`. From there, the core takes over. The `xps_core_start()` takes in the core instance and also the ports for the listening sockets for the server. +Recall how in the `xps_server_destroy()` function we used `xps_client_destroy()` to destroy individual clients by passing the client object to it. We will implement this now. -### `xps_core.h & xps_core.c` +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`. -With the ports it receives, create server instances with the `xps_server_create()` function. Don’t forget to add the servers to the core’s list of servers. Start the loop using `xps_loop_run()` at the end. +::: details expserver/network/xps_client.c -- expserver/core/xps_core.c - ```c - /** - * @brief Starts the XPS core. - * - * This function starts the XPS core by creating servers for the specified ports and running the - * event loop. - * - * @param core Pointer to the XPS core. - * @param ports Array of port numbers to listen on. - * @param n_ports Number of ports in the array. - */ - void xps_core_start(xps_core_t *core, int *ports, int n_ports) { - ... +```c +void xps_client_destroy(xps_client_t *client) { + + /* validate params */ + + for (int i = 0; i < client->server->clients.length; i++) { + xps_client_t *curr = (xps_client_t *)(client->server->clients.data[i]); + if (/* fill this */) { + client->server->clients.data[i] = NULL; + break; + } } - ``` -But wait. We didn’t create the loop anywhere yet. And the core too. Let’s do that in `xps_core_create()`. Make use of `xps_loop_create()` for creating the loop. Attach the loop to the core instance. + /* destroy socket */ + + /* free client */ + +} +``` + +::: + +::: danger QUESTION +Why do you think it is necessary to set NULL in server’s client list while destroying the client? Why can’t we just remove the item from the list instead of setting it to NULL? +::: + +Now that we have a client instance, we need a way (functions) to communicate (read and write) with it. This is where `xps_client_read()` and `xps_client_write()` come in. + +Refer the `handle_client()` function from the previous stage and modify the functions below. + +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](/guides/references/xps_buffer). + +::: details expserver/network/xps_client.c + +```c +void xps_client_write(xps_client_t *client, xps_buffer_t *buff) { + + /* send message to the client using send */ -While you are at it, finish the `xps_core_destroy()` function, which destroys all the servers and loop associated with the core. +} -- expserver/core/xps_core.c +xps_buffer_t *xps_client_read(xps_client_t *client) { - ```c - /** - * @brief Creates a new XPS core instance. - * - * This function creates a new XPS core instance and initializes its event loop. - * - * @return Pointer to the created XPS core, or NULL if an error occurs. - */ - xps_core_t *xps_core_create() { - ... + /* read message from client to buffer using recv */ + + /* return a buffer instance type xps_buffer_t if success or NULL if error */ + +} +``` + +::: + +Create a simple function, `xps_client_loop_read_handler()` to call the client read callback. + +::: details expserver/network/xps_client.c + +```c +void xps_client_loop_read_handler(xps_client_t *client) { + + /* fill this */ + +} +``` + +::: + +### `main.c` + +In `main.c`, we'll initialize eXpServer, utilizing some functions from the previous stage. In this stage, our main function will contain event loop related functions only. + +As we implemented a proxy last time, code related to upstream wont be relevant as of now. We will get to that when we work on the upstream module in Stage 9. + +We’ll start with the `main()` function because that is where the execution starts. What might be the `main()` function’s responsibilities? + +- To create a loop instance - `loop_create()` +- To start server(s) - `xps_server_create()` +- To start the event loop - `loop_run()` + +::: tip +Always try to keep the main function simple and distribute the work into dedicated functions. +::: + +::: details expserver/main.c + +```c +// Global variables +int epoll_fd; +struct epoll_event events[MAX_EPOLL_EVENTS]; + +xps_server_t *servers[10]; +int n_servers = 0; + +int main() { + + /* create loop using loop_create()*/ + + // Create servers on ports 8001, 8002, 8003, 8004 + n_servers = 4; + for (int i = 0; i < n_servers; i++) { + int port = 8001 + i; + servers[i] = xps_server_create(port, **xps_server_connection_handler**); + loop_attach(epoll_fd, servers[i]->sock->fd, EPOLLIN); + logger(LOG_INFO, "main()", "Server listening on port %d", port); } - /** - * @brief Destroys an XPS core instance. - * - * This function destroys the specified XPS core instance and releases all associated resources. - * - * @param core Pointer to the XPS core to destroy. - */ - void xps_core_destroy(xps_core_t *core) { - ... + /* start event loop */ + +} +``` + +::: + +The implementations of `loop_attach()` and `loop_detach()` remain unchanged from the previous stage. + +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 + +```c +void xps_server_connection_handler(xps_server_t *server, int status) { + + /* accept connection */ + + xps_socket_t *sock = /* create socket instance using xps_socket_create() */ + + xps_client_t *client = xps_client_create(server, sock, **xps_client_read_handler**); + + // Adding client to clients list of server + vec_push(&(server->clients), (void *)client); + + /* attach client to loop using loop_attach() */ + +} +``` + +::: + +`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 + +```c +void xps_client_read_handler(xps_client_t *client, int status) { + + xps_buffer_t *buff = /* read message from client using xps_client_read() */ + + /* error handling (detach client fd from loop and destory client) */ + + // Add null terminator to end of message string + buff->data[buff->len] = 0; + + printf("%s", buff->data); + + /* reverse message */ + + /* send reversed message to client using xps_client_write() */ + +} +``` + +::: + +Now that we have all the functions, we can start writing the `loop_run()` function which is responsible for catching all events and calling the corresponding callbacks. The callback functions (handlers) that we have written so far will start to make sense as we implement `loop_run()` . + +Similar to `loop_run()` in last stage, we get events from the server and the client. Identifying if it is a server or a client event is the tricky part now. + +In case of a server event, you would have to loop through all the server instances and find the server with the matching socket fd. When the server is found, call the `xps_server_loop_read_handler()` to take care of the event. + +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 + +```c +void loop_run(int epoll_fd) { + + while (1) { + + /* epoll wait */ + + for (/* loop though epoll events */) { + + int curr_fd = events[i].data.fd; + + // Check if event is on a server + for (/* loop through servers */) { + if (servers[i]->sock->fd == curr_fd) { + + // server with event found + + /* fill this */ + + } + } + + // Check if event is on a client + for (/* loop through servers */) { + for (/* loop through clients of this server */) { + xps_client_t *curr_client = (xps_client_t *)(servers[i]->clients.data[j]); + if (curr_client != NULL && curr_fd == curr_client->sock->fd) { + + // client with event found + + /* fill this */ + + } + } + } + } - ``` -> ::: warning -> There will be some changes to the server and client module to take core into account. We will work on them after we are done with `xps_core`. -> ::: +} +``` + +::: + +::: tip NOTE +Take a note of the type casting on client from server’s clients list to `xps_client_t` in the client loop. +::: -> ::: danger QUESTION -> We used `xps_server_connection_handler()` to handle a new client connection and `xps_client_read_handler()` when there is data available to read. Would there be any changes to these? -> ::: +### Milestone #2 -### `xps_server.c & xps_client.c` +Time to test the code! + +The following command can be used to compile all the code in stage 5: + +```bash +gcc -g -o xps main.c network/xps_socket.c network/xps_server.c network/xps_client.c lib/vec/vec.c utils/xps_buffer.c utils/xps_logger.c +``` + +As we have a lot of files to compile this time around, we suggest using a script file to make the process easier. You will find a `build.sh` file in the `expserver/` directory. Copy over the command and use the following to run the script: + +```bash +bash build.sh +``` + +Running the output file `./xps` should give you the following output: + +```bash +[INFO] main() : Server listening on port 8001 +[INFO] main() : Server listening on port 8002 +[INFO] main() : Server listening on port 8003 +[INFO] main() : Server listening on port 8004 +``` + +Don’t worry if your code does the produce the expected output in the first attempt. Debugging and figuring out the error is as important as writing the code itself. The `xps_logger` utility and GDB are your best friends here. ## Conclusion + +That’s it! We understand that this stage was longer and complex that the ones before. In the next stage, we will work on the core and loop modules; arguably the two most important modules for the functioning of eXpServer. diff --git a/docs/roadmap/phase-1/stage-7.md b/docs/roadmap/phase-1/stage-7.md index cfe3b82..84647ae 100644 --- a/docs/roadmap/phase-1/stage-7.md +++ b/docs/roadmap/phase-1/stage-7.md @@ -1 +1,497 @@ -# Stage 7: TCP Module +# Stage 7: Core & Loop Modules + +## Recap + +- We created server and client modules + +## Introduction + +In this stage, we will be modularizing the code further by creating two new modules: + +1. Core module +2. Loop module + +The core module, as its name implies, will act as the central hub of the server, where all components will be connected to, including the loop. The `main()` function in `main.c` will create an instance of core and subsequently ‘start’ it. The core should take care of everything else from that point onwards. + +You can probably predict what the loop module will contain. + +New modules comes with new structures, thus `xps_core_s` and `xps_loop_s` have been added to core and loop header files. + +```c +// From xps_core.h +struct xps_core_s { + xps_loop_t *loop; + vec_void_t servers; +}; + +// From xps_loop.h +struct xps_loop_s { + int epoll_fd; + struct epoll_event events[MAX_EPOLL_EVENTS]; + vec_void_t handles; +}; +``` + +### File structure + +![filestructure.png](/assets/stage-6/filestructure.png) + +Find below the updated `xps.h` file. New additions to the file are indicated in green: + +- expserver/xps.h + + ```c + #ifndef XPS_H + #define XPS_H + + #define DEFAULT_BACKLOG 64 + #define MAX_EPOLL_EVENTS 16 + #define DEFAULT_BUFFER_SIZE 100000 + + // Error constants + #define OK 0 + #define E_FAIL -1 + #define E_AGAIN -2 + #define E_CLOSE -3 + #define E_NOTFOUND -4 + #define E_PERMISSION -5 + #define E_EOF -6 + + // Types + typedef unsigned char u_char; + typedef unsigned int u_int; + + typedef enum { // [!code ++] + HANDLE_TCP_CLIENT, // [!code ++] + HANDLE_TCP_SERVER, // [!code ++] + } xps_loop_handle_type_t; // [!code ++] + + struct xps_buffer_s; + struct xps_socket_s; + struct xps_server_s; + struct xps_client_s; + struct xps_core_s; // [!code ++] + struct xps_loop_s; // [!code ++] + struct xps_loop_handle_s; // [!code ++] + + typedef struct xps_buffer_s xps_buffer_t; + typedef struct xps_socket_s xps_socket_t; + typedef struct xps_server_s xps_server_t; + typedef struct xps_client_s xps_client_t; + typedef struct xps_core_s xps_core_t; // [!code ++] + typedef struct xps_loop_s xps_loop_t; // [!code ++] + typedef struct xps_loop_handle_s xps_loop_handle_t; // [!code ++] + + typedef void (*xps_server_connection_handler_t)(xps_server_t *server, int status); + typedef void (*xps_client_read_handler_t)(xps_client_t *client, int status); + + #endif + ``` + +> ::: tip NOTE +> You are free to modify the code in any file you want, including the `xps.h` file. The documentation is just a here to guide and nudge you in the right direction and not restrict you in any way. Feel free to add new functions, global variables, constants, structs. Just make sure you retain the content you write when you are copying code snippets like above. +> ::: + +## Implementation + +Let’s have a clear picture before we move forward with the code. + +![core.png](/assets/stage-6/core.png) + +- The main function will create an instance of a core, and start it by providing the number of listening sockets. +- The core will be responsible for spinning up the servers and starting the loop. +- The loop module will contain the epoll, and have functions related to creating and destroying the loop instance, starting it, attaching and detaching events to loop, handling the events, etc. +- **A loop belongs to a core.** So when we create a loop instance, attach it to the core instance. + +> ::: tip NOTE +> Each server instance will have its own listening socket. We know that we can spin up multiple servers, i.e. multiple listen sockets. From now onwards, these collection of servers running on different ports will be collectively called the **server**. +> ::: + +Let’s start with the loop module and move onto the core module. + +### `xps_loop.h & xps_loop.c` + +Recall in the last stage, we had code related to the epoll in the `main.c` file. Let’s make a module for it now. + +The code below has the contents of the header file for `xps_loop`. Have a look at it and make a copy of it in your codebase. + +- expserver/core/xps_loop.h + + ```c + #ifndef XPS_LOOP_H + #define XPS_LOOP_H + + #include + + #include "../lib/vec/vec.h" + #include "../xps.h" + + struct xps_loop_handle_s { + int fd; + void *item; + xps_loop_handle_type_t type; + }; + + struct xps_loop_s { + int epoll_fd; + struct epoll_event events[MAX_EPOLL_EVENTS]; + vec_void_t handles; + }; + + xps_loop_t *xps_loop_create(); + void xps_loop_destroy(xps_loop_t *loop); + int xps_loop_attach(xps_loop_t *loop, xps_loop_handle_type_t type, void *item); + int xps_loop_detach(xps_loop_t *loop, xps_loop_handle_type_t type, int fd); + void xps_loop_run(xps_loop_t *loop); + + #endif + ``` + +Did you notice something new in `xps_loop_s` i.e. `vec_void_t handles`? + +```c +struct xps_loop_s { + int epoll_fd; + struct epoll_event events[MAX_EPOLL_EVENTS]; + vec_void_t handles; // [!code focus] +}; +``` + +Think of a **handle** as a wrapper around a file descriptor associated with a particular resource (TCP client or TCP server). It provides a convenient way to manage and interact with these resources within the event loop. + +Each handle is an object of `struct xps_loop_handle_s`. + +```c +struct xps_loop_handle_s { + int fd; + void *item; + xps_loop_handle_type_t type; +}; + +typedef enum { + HANDLE_TCP_CLIENT, + HANDLE_TCP_SERVER, +} xps_loop_handle_type_t; +``` + +Each handle can be associated with either a TCP client or a TCP server, as mentioned before. But how do we figure out which one of them the handle represents? `xps_loop_handle_type_t type` is utilized for this purpose. `void *item` is a pointer to an instance of `xps_server_t` or `xps_client_t`, depending on what the handle is created for. + +So instead of using objects of `xps_server_t` and `xps_client_t`, we will use `xps_loop_handle_s`. We can typecast `void *item` member in the object of `xps_loop_handle_s` to `xps_server_t` or `xps_client_t` depending on `xps_loop_handle_type_t type`. + +With this information, try to fill out the create and destroy functions associated with handles. The **function signatures** are given below. + +> ::: tip NOTE +> A function signature outlines the essential details of a function including its name, parameters, return type, and a brief description of its purpose. This signature serves as a reference for how the function should be called and what it does, without detailing the implementation. You will be seeing a lot of these from now onwards. +> ::: + +- expserver/core/xps_loop.c + + ```c + /** + * @brief Creates a handle for the given file descriptor, handle type, and item. + * + * This function creates a handle for the specified file descriptor, handle type, and item. + * + * @param fd The file descriptor. + * @param type The type of the handle. + * @param item Pointer to the item associated with the handle. + * @return Pointer to the created handle, or NULL if an error occurs. + */ + xps_loop_handle_t *xps_handle_create(int fd, xps_loop_handle_type_t type, void *item) { + + // Alloc memory for handle instance + xps_loop_handle_t *handle = (xps_loop_handle_t *)malloc(sizeof(xps_loop_handle_t)); + if (handle == NULL) { + logger(LOG_ERROR, "xps_handle_create()", + "failed to alloc memory for handle. malloc() returned NULL"); + return NULL; + } + + /* assign values to handle */ + + logger(LOG_DEBUG, "xps_handle_create()", "created handle"); + + return handle; + + } + + /** + * @brief Destroys a handle. + * + * This function destroys the handle and frees the memory associated with it. + * + * @param handle Pointer to the handle to be destroyed. + */ + void xps_handle_destroy(xps_loop_handle_t *handle) { + ... + } + ``` + +> ::: warning +> Don’t forget to handle params and other errors! +> ::: + +Let’s move onto loop functions. Previously, our `loop_create()` function in Stage 5 was as simple as returning a new epoll instance. The function becomes a bit more sophisticated with the introduction of the `xps_loop_s` structure. + +The new `xps_loop_create()` function should create an object of `xps_loop_s` (`xps_loop_t *loop`), attach a new epoll instance and return the object. + +- expserver/core/xps_loop.c + ```c + /** + * @brief Creates a new event loop. + * + * This function creates a new event loop and initializes its epoll instance. + * + * @return xps_loop_t* Pointer to the created event loop, or NULL if an error occurs. + */ + xps_loop_t *xps_loop_create() { + ... + } + ``` + +> ::: tip +> Use `vec_init(&(loop->handles))` for memory allocation. +> ::: + +The `xps_loop_destroy()` function frees up memory from the loop object. It clears the memory taken by the handles by iterating through all the handles in the loop object, and destroying them with the help of `xps_handle_destroy()`. + +- expserver/core/xps_loop.c + ```c + /** + * @brief Destroys an event loop. + * + * This function destroys the specified event loop and releases all associated resources. + * + * @param loop Pointer to the event loop to destroy. + */ + void xps_loop_destroy(xps_loop_t *loop) { + ... + } + ``` + +> ::: tip +> Use `vec_deinit(&(loop->handles))` to deallocate memory. +> ::: + +Now that we've established the ability to create a loop, let's proceed to implement the functions for attaching and detaching items from the loop. We'll define `xps_loop_attach()` and `xps_loop_detach()` for this purpose. + +`xps_loop_attach()` takes in a parameter `void *item`. This object could be an instance of client or server; typecast it to `xps_client_t` or `xps_server_t` depending on the `type` parameter. This will allow you to access the FD in it. After you get the FD, you can create a handle using `xps_handle_create()` and add it to list of handles in loop. + +- expserver/core/xps_loop.c + + ```c + /** + * @brief Attaches an item to the event loop. + * + * This function attaches the specified item to the event loop based on its type. + * + * @param loop Pointer to the event loop. + * @param type Type of the handle. + * @param item Pointer to the item to attach. + * @return int E_FAIL if an error occurs, OK otherwise. + */ + int xps_loop_attach(xps_loop_t *loop, xps_loop_handle_type_t type, void *item) { + + /* handle params */ + + int fd = -1; + + if (type == HANDLE_TCP_CLIENT) { + xps_client_t *client = (xps_client_t *)item; + fd = client->sock->fd; + } + + else if (/* TCP server */) { + /* fill this */ + } + + xps_loop_handle_t *handle = /* create handle */ + + // Add socket to epoll + struct epoll_event event; + event.events = /* fill this */ + event.data.fd = /* fill this */ + event.data.ptr = (void *)handle; + + /* attach event to epoll using epoll_ctl() */ + + vec_push(&(loop->handles), (void *)handle); + + logger(LOG_DEBUG, "xps_loop_attach()", "attached item to loop"); + + return OK; + + } + ``` + +`xps_loop_detach()` takes in a FD that has to be detached from the loop. There are three things to do in `xps_loop_detach()` function: + +- Find the handle to be deleted from the list of handles in the loop and set it to NULL +- Destroy the handle using `xps_handle_destroy()` +- Remove FD from the epoll + +Doing the first two will remove the handle from everywhere. + +- expserver/core/xps_loop.c + + ```c + /** + * @brief Detaches an item from the event loop. + * + * This function detaches an item with the specified file descriptor from the event loop. + * + * @param loop Pointer to the event loop. + * @param type Type of the handle. + * @param fd File descriptor of the item to detach. + * @return int E_FAIL if an error occurs, OK otherwise. + */ + int xps_loop_detach(xps_loop_t *loop, xps_loop_handle_type_t type, int fd) { + + /* handle params */ + + vec_void_t *handles = &(loop->handles); + + int handle_index = -1; + for (/* iterate through all the handles */) { + xps_loop_handle_t *handle = (xps_loop_handle_t *)((*handles).data[i]); + /* fill this */ + } + + // Setting NULL in handles list + (*handles).data[handle_index] = NULL; + + xps_loop_handle_t *handle = (xps_loop_handle_t *)((*handles).data[handle_index]); + + /* destroy handle using xps_handle_destroy() */ + + /* remove fd from epoll */ + + logger(LOG_DEBUG, "xps_loop_detach()", "detached item from loop"); + + return OK; + + } + ``` + +Time to start the loop and handle the events. As we’ve done this multiple, you would be aware of what `xps_loop_run()` is responsible for. Refer to the previous stages if you want a recap. + +In Stage 5, determining whether an FD belongs to a server or client used to require iterating through all servers and clients. However, with handles, our task has significantly simplified. + +- expserver/core/xps_loop.c + + ```c + /** + * @brief Runs the event loop. + * + * This function runs the event loop and handles the events on it continuously + * + * @param loop Pointer to the event loop to run. + */ + void xps_loop_run(xps_loop_t *loop) { + + /* handle params */ + + while (loop) { + + /* epoll wait */ + + for(/* loop through epoll events */) { + + struct epoll_event curr_event = loop->events[i]; + + xps_loop_handle_t *curr_handle = (xps_loop_handle_t *)curr_event.data.ptr; + + /* check if handle still exists */ + + if(/* handle is of client type */) { + /* fill this */ + } + else if(/* handle if of server type */) { + /* fill this */ + } + + } + + } + ``` + +Explain why the handle might not exist + +> ::: tip +> Use `vec_filter_null(&(loop->handles))` to remove all the handles that were converted to NULL during `xps_loop_detach()`. +> ::: + +--- + +### Milestone #1 + +Recap: + +- *** + +Core being the most important module, will keep changing as we build more modules to eXpServer. + +The `main()` function in the`main.c` file will create an instance of core with `xps_core_create()` and start it using `xps_core_start()`. From there, the core takes over. The `xps_core_start()` takes in the core instance and also the ports for the listening sockets for the server. + +### `xps_core.h & xps_core.c` + +With the ports it receives, create server instances with the `xps_server_create()` function. Don’t forget to add the servers to the core’s list of servers. Start the loop using `xps_loop_run()` at the end. + +- expserver/core/xps_core.c + ```c + /** + * @brief Starts the XPS core. + * + * This function starts the XPS core by creating servers for the specified ports and running the + * event loop. + * + * @param core Pointer to the XPS core. + * @param ports Array of port numbers to listen on. + * @param n_ports Number of ports in the array. + */ + void xps_core_start(xps_core_t *core, int *ports, int n_ports) { + ... + } + ``` + +But wait. We didn’t create the loop anywhere yet. And the core too. Let’s do that in `xps_core_create()`. Make use of `xps_loop_create()` for creating the loop. Attach the loop to the core instance. + +While you are at it, finish the `xps_core_destroy()` function, which destroys all the servers and loop associated with the core. + +- expserver/core/xps_core.c + + ```c + /** + * @brief Creates a new XPS core instance. + * + * This function creates a new XPS core instance and initializes its event loop. + * + * @return Pointer to the created XPS core, or NULL if an error occurs. + */ + xps_core_t *xps_core_create() { + ... + } + + /** + * @brief Destroys an XPS core instance. + * + * This function destroys the specified XPS core instance and releases all associated resources. + * + * @param core Pointer to the XPS core to destroy. + */ + void xps_core_destroy(xps_core_t *core) { + ... + } + ``` + +> ::: warning +> There will be some changes to the server and client module to take core into account. We will work on them after we are done with `xps_core`. +> ::: + +> ::: danger QUESTION +> We used `xps_server_connection_handler()` to handle a new client connection and `xps_client_read_handler()` when there is data available to read. Would there be any changes to these? +> ::: + +### `xps_server.c & xps_client.c` + +## Conclusion diff --git a/docs/roadmap/phase-1/stage-8.md b/docs/roadmap/phase-1/stage-8.md index 7fa628e..fb35111 100644 --- a/docs/roadmap/phase-1/stage-8.md +++ b/docs/roadmap/phase-1/stage-8.md @@ -1 +1 @@ -# Stage 8: Upstream Module +# Stage 8: TCP Module diff --git a/docs/roadmap/phase-1/stage-9.md b/docs/roadmap/phase-1/stage-9.md index 38e1d6a..2fc6925 100644 --- a/docs/roadmap/phase-1/stage-9.md +++ b/docs/roadmap/phase-1/stage-9.md @@ -1 +1 @@ -# Stage 9: File Module +# Stage 9: Upstream Module diff --git a/docs/roadmap/phase-2/stage-10.md b/docs/roadmap/phase-2/stage-10.md deleted file mode 100644 index a578755..0000000 --- a/docs/roadmap/phase-2/stage-10.md +++ /dev/null @@ -1 +0,0 @@ -# Stage 10: HTTP Parser diff --git a/docs/roadmap/phase-2/stage-11.md b/docs/roadmap/phase-2/stage-11.md index a6d4bb4..d586477 100644 --- a/docs/roadmap/phase-2/stage-11.md +++ b/docs/roadmap/phase-2/stage-11.md @@ -1 +1 @@ -# Stage 11: HTTP Req & Res Modules +# Stage 11: HTTP Parser diff --git a/docs/roadmap/phase-2/stage-12.md b/docs/roadmap/phase-2/stage-12.md index d68a431..ca166c5 100644 --- a/docs/roadmap/phase-2/stage-12.md +++ b/docs/roadmap/phase-2/stage-12.md @@ -1 +1 @@ -# Stage 12: Config & Session Modules +# Stage 12: HTTP Req & Res Modules diff --git a/docs/roadmap/phase-2/stage-13.md b/docs/roadmap/phase-2/stage-13.md index 2951067..a290cad 100644 --- a/docs/roadmap/phase-2/stage-13.md +++ b/docs/roadmap/phase-2/stage-13.md @@ -1 +1 @@ -# Stage 13: HTTP Specification +# Stage 13: Config & Session Modules diff --git a/docs/roadmap/phase-2/stage-14.md b/docs/roadmap/phase-2/stage-14.md new file mode 100644 index 0000000..35003e2 --- /dev/null +++ b/docs/roadmap/phase-2/stage-14.md @@ -0,0 +1 @@ +# Stage 14: HTTP Specification diff --git a/docs/roadmap/phase-3/stage-14.md b/docs/roadmap/phase-3/stage-14.md deleted file mode 100644 index 22e13e1..0000000 --- a/docs/roadmap/phase-3/stage-14.md +++ /dev/null @@ -1 +0,0 @@ -# Stage 14: IP Whitelist/Blacklist diff --git a/docs/roadmap/phase-3/stage-15.md b/docs/roadmap/phase-3/stage-15.md index 0f82d61..b8babb6 100644 --- a/docs/roadmap/phase-3/stage-15.md +++ b/docs/roadmap/phase-3/stage-15.md @@ -1 +1 @@ -# Stage 15: Directory Browsing +# Stage 15: IP Whitelist/Blacklist diff --git a/docs/roadmap/phase-3/stage-16.md b/docs/roadmap/phase-3/stage-16.md index efef65d..89579a1 100644 --- a/docs/roadmap/phase-3/stage-16.md +++ b/docs/roadmap/phase-3/stage-16.md @@ -1 +1 @@ -# Stage 16: Gzip Compression +# Stage 16: Directory Browsing diff --git a/docs/roadmap/phase-3/stage-17.md b/docs/roadmap/phase-3/stage-17.md index b91cb51..23f01c3 100644 --- a/docs/roadmap/phase-3/stage-17.md +++ b/docs/roadmap/phase-3/stage-17.md @@ -1 +1 @@ -# Stage 17: Load Balancing +# Stage 17: Gzip Compression diff --git a/docs/roadmap/phase-3/stage-18.md b/docs/roadmap/phase-3/stage-18.md index 1033c18..091815c 100644 --- a/docs/roadmap/phase-3/stage-18.md +++ b/docs/roadmap/phase-3/stage-18.md @@ -1 +1 @@ -# Stage 18: Rate Limiting & Timeout +# Stage 18: Load Balancing diff --git a/docs/roadmap/phase-3/stage-19.md b/docs/roadmap/phase-3/stage-19.md new file mode 100644 index 0000000..83ac1aa --- /dev/null +++ b/docs/roadmap/phase-3/stage-19.md @@ -0,0 +1 @@ +# Stage 19: Rate Limiting & Timeout diff --git a/docs/roadmap/phase-4/stage-19.md b/docs/roadmap/phase-4/stage-19.md deleted file mode 100644 index 38ba4c2..0000000 --- a/docs/roadmap/phase-4/stage-19.md +++ /dev/null @@ -1 +0,0 @@ -# Stage 19: Transport Layer Security (TLS) diff --git a/docs/roadmap/phase-4/stage-20.md b/docs/roadmap/phase-4/stage-20.md index 44ff8b3..8c0db39 100644 --- a/docs/roadmap/phase-4/stage-20.md +++ b/docs/roadmap/phase-4/stage-20.md @@ -1 +1 @@ -# Stage 20: Caching +# Stage 20: Transport Layer Security (TLS) diff --git a/docs/roadmap/phase-4/stage-21.md b/docs/roadmap/phase-4/stage-21.md index 5d30857..77fdc07 100644 --- a/docs/roadmap/phase-4/stage-21.md +++ b/docs/roadmap/phase-4/stage-21.md @@ -1 +1 @@ -# Stage 21: Multiprocess Architecture +# Stage 21: Caching diff --git a/docs/roadmap/phase-4/stage-22.md b/docs/roadmap/phase-4/stage-22.md new file mode 100644 index 0000000..14a83e1 --- /dev/null +++ b/docs/roadmap/phase-4/stage-22.md @@ -0,0 +1 @@ +# Stage 22: Multiprocess Architecture