The main loop

With our many helper functions out of the way, we can now finish web_server.c. Remember to first #include chap07.h and also add in all of the types and functions we've defined so far—struct client_info, get_content_type(), create_socket(), get_client(), drop_client(), get_client_address(), wait_on_clients(), send_400(), send_404(), and serve_resource().

We can then begin the main() function. It starts by initializing Winsock on Windows:

/*web_server.c except*/

int main() {

#if defined(_WIN32)
WSADATA d;
if (WSAStartup(MAKEWORD(2, 2), &d)) {
fprintf(stderr, "Failed to initialize. ");
return 1;
}
#endif

We then use our earlier function, create_socket(), to create the listening socket. Our server listens on port 8080, but feel free to change it. On Unix-based systems, listening on low port numbers is reserved for privileged accounts. For security reasons, our web server should be running with unprivileged accounts only. This is why we use 8080 as our port number instead of the HTTP standard port, 80.

The code to create the server socket is as follows:

/*web_server.c except*/

SOCKET server = create_socket(0, "8080");

If you want to accept connections from only the local system, and not outside systems, use the following code instead:

/*web_server.c except*/

SOCKET server = create_socket("127.0.0.1", "8080");

We then begin an endless loop that waits on clients. We call wait_on_clients() to wait until a new client connects or an old client sends new data:

/*web_server.c except*/

while(1) {

fd_set reads;
reads = wait_on_clients(server);

The server then detects whether a new client has connected. This case is indicated by server being set in fd_set reads. We use the FD_ISSET() macro to detect this condition:

/*web_server.c except*/

if (FD_ISSET(server, &reads)) {
struct client_info *client = get_client(-1);

client->socket = accept(server,
(struct sockaddr*) &(client->address),
&(client->address_length));

if (!ISVALIDSOCKET(client->socket)) {
fprintf(stderr, "accept() failed. (%d) ",
GETSOCKETERRNO());
return 1;
}


printf("New connection from %s. ",
get_client_address(client));
}

Once a new client connection has been detected, get_client() is called with the argument -1-1 is not a valid socket specifier, so get_client() creates a new struct client_info. This struct client_info is assigned to the client variable.

The accept() socket function is used to accept the new connection and place the connected clients address information into the respective client fields. The new socket returned by accept() is stored in client->socket.

The client's address is printed using a call to get_client_address(). This is helpful for debugging.

Our server must then handle the case where an already connected client is sending data. This is a bit more complicated. We first walk through the linked list of clients and use FD_ISSET() on each client to determine which clients have data available. Recall that the linked list root is stored in the clients global variable.

We begin our linked list walk with the following:

/*web_server.c except*/

struct client_info *client = clients;
while(client) {
struct client_info *next = client->next;

if (FD_ISSET(client->socket, &reads)) {

We then check that we have memory available to store more received data for client. If the client's buffer is already completely full, then we send a 400 error. The following code checks for this condition:

/*web_server.c except*/

if (MAX_REQUEST_SIZE == client->received) {
send_400(client);
continue;
}

Knowing that we have at least some memory left to store received data, we can use recv() to store the client's data. The following code uses recv() to write new data into the client's buffer while being careful to not overflow that buffer:

/*web_server.c except*/

int r = recv(client->socket,
client->request + client->received,
MAX_REQUEST_SIZE - client->received, 0);

A client that disconnects unexpectedly causes recv() to return a non-positive number. In this case, we need to use drop_client() to clean up our memory allocated for that client:

/*web_server.c except*/

if (r < 1) {
printf("Unexpected disconnect from %s. ",
get_client_address(client));
drop_client(client);

If the received data was written successfully, our server adds a null terminating character to the end of that client's data buffer. This allows us to use strstr() to search the buffer, as the null terminator tells strstr() when to stop.

Recall that the HTTP header and body is delineated by a blank line. Therefore, if strstr() finds a blank line ( ), we know that the HTTP header has been received and we can begin to parse it. The following code detects whether the HTTP header has been received:

/*web_server.c except*/

} else {
client->received += r;
client->request[client->received] = 0;

char *q = strstr(client->request, " ");
if (q) {

Our server only handles GET requests. We also enforce that any valid path should start with a slash character; strncmp() is used to detect these two conditions in the following code:

/*web_server.c except*/

if (strncmp("GET /", client->request, 5)) {
send_400(client);
} else {
char *path = client->request + 4;
char *end_path = strstr(path, " ");
if (!end_path) {
send_400(client);
} else {
*end_path = 0;
serve_resource(client, path);
}
}
} //if (q)

In the preceding code, a proper GET request causes the execution of the else branch. Here, we set the path variable to the beginning of the request path, which is starting at the fifth character of the HTTP request (because C arrays start at zero, the fifth character is located at client->request + 4).

The end of the requested path is indicated by finding the next space character. If found, we just call our serve_resource() function to fulfil the client's request.

Our server is basically functional at this point. We only need to finish our loops and close out the main() function. The following code accomplishes this:

/*web_server.c except*/

}
}

client = next;
}

} //while(1)


printf(" Closing socket... ");
CLOSESOCKET(server);


#if defined(_WIN32)
WSACleanup();
#endif

printf("Finished. ");
return 0;
}

Note that our server doesn't actually have a way to break from its infinite loop. It simply listens to connections forever. As an exercise, you may want to add in functionality that allows the server to shut down cleanly. This was omitted only to keep the code simpler. It may also be useful to drop all connected clients with this line of code—while(clients) drop_client(clients);

That concludes the code for web_server.c. I recommend you download web_server.c from this book's code repository and try it out.

You can compile and run web_server.c on Linux and macOS with the following commands:

gcc web_server.c -o web_server
./web_server

On Windows, the command to compile and run using MinGW is as follows:

gcc web_server.c -o web_server.exe -lws2_32
web_server.exe

The following screenshot shows the server being compiled and run on macOS:

If you connect to the server using a standard web browser, you should see something such as the following screenshot:

You can also drop different files into the public folder and play around with creating more complicated websites.

An alternative source file, web_server2.c, is also provided in this chapter's code repository. It behaves exactly like the code we developed, but it avoids having global state (at the expense of a little added verbosity). This may make web_server2.c more suitable for integration into more significant projects and continued development.

Although the web server we developed certainly works, it does have a number of shortcomings. Please don't deploy this server (or any other network code) in the wild without very carefully considering these shortcomings, some of which we address next.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.156.50