Multithreaded Chat Server in C and Java!

Creating clients - Part 2

Author: Joshua Crotts

Date: 2021/05/21

Welcome back! In this tutorial, we'll make the structs and data associated with a client. However, I want to introduce a topic first. See, when we create the server, we need a way of keeping track of clients. We ought to store them in some type of data structure so we can traverse it and find clients for later lookup. How can we do that, you ask?

Normally, if you were efficient, you may want to do some sort of TreeSet or something like that to ensure that clients are ordered for quick searches down the road, but for this project where there won't be more than 5 clients connected, it's perfectly reasonable to go with a standard linked list.

A linked list, for those unfamiliar, is a data structure similar to an array, but different in that there is no such thing as contiguous memory - everything is linked together by pointers. So, instead of having an "index", each element is called a "node". All nodes will point to another node, namely, their successor. So, the head, or the front of the list points to the next element in line. That element points to the next "next" element, and so on and so forth.

This specific kind of linked list is called a singly-linked list - you can kind more about the details online. We are going to create what's called a doubly linked list. The difference is that instead of only having one pointer (namely, to the next node in succession), each node will have two: one for the next node, and one for the previous. This will make removing and inserting clients much easier down the road.

I won't go too into details about the implementation of the data structure itself, since that's effectively a prerequisite. However, I will provide details on the code related to the networking and whatnot. Let's get started!


Firstly, let's create a header file called client.h and a source called client.c.

          
            #ifndef CLIENT_H
            #define CLIENT_H
            
            #include <stdio.h>

            // TODO client.h

            #endif // CLIENT_H
          
        
          
            #include "client.h"

            // TODO client.c
          
        

Neat. Now, let's talk about what the header file needs. We need three things: a linked list, a type for the linked list nodes, and a type for the client itself.

The linked list structure will have:

  • A pointer to the head node of the list, NULL if list is empty
  • A pointer to the tail node of the list, NULL if list is empty
  • A size variable denoting how many clients are in the list

The linked list node structure will have:

  • A pointer to the node immediately after this node, NULL if none.
  • A pointer to the tail immediately before this node, NULL if none.
  • A pointer to the client struct that this node represents (the data itself!)

And finally, the client itself will have multiple things, and we may need to extend this definition later. For now, though:

  • A communication file descriptor
  • A name
  • A pointer to the write FILE.
  • A pointer to the read FILE.

We'll expand on those last two in the client struct in a bit, but the first two should be trivial if you understand linked lists (if not, please read about them before proceeding - they are super important!). So, let's code them up!

          
            /**
             * Typedef for the list itself. Keeps track of two pointers:
             * the head and the rear of the list. 
             */
            typedef struct client_list_s {
              struct client_node_s *head;   /* Head (first element) of the list. */
              struct client_node_s *tail;   /* Tail (last element) of the list. */
              size_t size;                  /* Number of currently-connected clients. */
            } client_list_t;

            /**
             * Node in the linked list. Abstracts the client_s struct and keeps 
             * two pointers: the previous node and the next node.
             */
            struct client_node_s {
              struct client_s *client;
              struct client_node_s *next;
              struct client_node_s *prev;
            };
          
        

Let's discuss the client type for a minute (haven't coded it yet). Recall that when a client connects to the server in the connection while loop, we get two things: the name, and the communication file descriptor. These are the two (obvious) fields that we named earlier, but we also named two others: the read and write FILE pointers. If you've ever messed with file IO in C, FILE * should make sense to you. If not, however, it's still relatively simple.

There are three "primary" types of file streams: standard input, standard output, and standard error. These, in C, are listed as stdin, stdout, stderr respectively. We can mimic the behavior of these file streams using FILE pointers with that communication file descriptor. We need to be able to read and write to the socket, right? So, how can we do that? It's simple, really (I know I keep saying that, but the concept itself is - the underlying code may be a bit more confusing to some)! We can use that unique communication file descriptor that we get from accept, and create FILE pointer objects from them, and dedicate one to reading, and one to writing!

I hear some of you asking: "Why not just create one FILE pointer and use that instead?" Great question! The issue with this is that, when attempting to read from the client using only one file descriptor (or FILE * in this context), for instance, there can be no other activity on that file descriptor, meaning we can't write to the client at all until the read is complete. That's why we dedicate two duplicate file descriptors for this. Effectively, we create two pipes: one solely for reading, and one solely for writing for that specific client and only that specific client. Sounds confusing at first, I know! Let's start off by writing the client struct.

            
              struct client_s {
                char *username;
                int comm_fd;
                
                FILE *read_fp;
                FILE *write_fp;
            };
            
          

So far so good, eh? Again, the first two fields should be clear - they're retrieved from the accept call. The two bottom ones may be a bit fuzzy, but you can think of them as "links". You can use FILE * for reading any type of connection (for the most part - this may need fact-checking...)! You can read not only from sockets like we're doing, but also from files directly (which is generally what it's used for). Of course, we could just use the read and write or send and recv functions, but those are lower level, and using FILE * allows us to use high-level and string-friendly functions such as fgets and getline.

Alright, let's get started with client.c:

            
                #include <string.h>
                #include <stdio.h>
                #include <stdlib.h>

                #include "client.h"
                
                void
                client_create( int comm_fd ) {
                  // Dynamically allocate struct.
                  struct client_s *client = malloc( sizeof( struct client_s ) );
                  if ( client == NULL ) {
                    perror( "Could not allocate memory for client struct.\n" );
                    exit( EXIT_FAILURE );
                  }
                  memset( client, 0, sizeof( struct client_s ) );
                  // TODO...
                }
            
          

When we accept the client request, we're going to create and dynamically allocate a client struct. From here, we'll need to make a couple of modifications to the header file so server.c can call this function.

Add the extern void client_create( int comm_fd ); prototype to client.h, and include client.h in server.c. Those are all the modifications for now.

Next, let's create the pipes for reading and writing to the client. Recall that the file descriptor is a pipe for this, but because we want to read and write at the same time to and from the client, we need to make use of the dup() function. This function returns a duplicated copy of the file descriptor. Since we need two new links, we can call dup twice and dedicate one file descriptor for reading, and one for writing. Note that, to do this, you need to include two new headers: unistd.h, and fcntl.h. So, here's what I mean:

            
              // Create the read and write file descriptors.
              client->comm_fd = comm_fd;
              int read_fd = dup( client->comm_fd );
              int write_fd = dup( client->comm_fd );
            
              // Now attach them to FILE *s.
              client->read_fp = fdopen( read_fd, "r" );
              client->write_fp = fdopen( write_fd, "w" );
            
          

Notice what we're doing here: after we create the two unique file descriptors, we attach them to FILE * with the fdopen function (conveniently named "file-descriptor open"!). You may have seen a variant of this called fopen. The difference here is that we're operating with a file descriptor integer rather than a path location or filename. Again, we do this so we have access to high-level functions for string processing/parsing later on - it makes our lives much easier!

For now, that's all we'll do. Let's write the client cleanup function, of course!

            
              void 
              client_destroy( struct client_s *client ) {
                // Free the name if it exists.
                if ( client->username != NULL ) {
                  free( client->username );
                }

                if ( fclose( client->read_fp ) < 0 ) {
                  perror( "Could not close client's read file ptr.\n" );
                  exit ( EXIT_FAILURE );
                }

                if ( fclose( client->write_fp ) < 0 ) {
                  perror( "Could not close client's write file ptr.\n" );
                  exit( EXIT_FAILURE );
                }

                free( client );
              }
            
            

You may be wondering: what is username? Remember it's that char * in the struct! We haven't done anything with it yet because 1. we're not passing the IP address from the server to the client_create function as there's no real need to, and 2. because it's not a static char array, we have to check to see if it has been malloc'd, and if so, free it. We'll come back to this later.

The rest of the code should make sense. We're closing the file pointers and if an error occurs, we just bail out. Finally, we free the memory associated with the client. Make sure to add this function to the header.

Awesome. Since I want to make these parts a bit shorter, we'll stop here for now, and next time we'll actually build the doubly linked list to store clients once they connect. Check the latest part 2 release tag for the latest changes, including the oh-so-slightly updated Makefile.

GitHub Code for Part 2.