Multithreaded Chat Server in C and Java!

Building the server - Part 1

Author: Joshua Crotts

Date: 2021/05/20

To begin, we are going to create a GitHub repository for our project. You can do this on your own, but I called mine Multithreaded-Chat-Server-Tutorial. Once you do this, clone it to your computer wherever you would like, but preferably a location that you can access through the terminal easily (since we’ll be running a lot of commands like make and whatnot).

Now, inside this repository folder, it will either be blank, or contain a file or two, depending on how you set it up. We need to create a few things:

  1. First, create a directory called src. This is where all of the C source .c files will be stored.
  2. Then, create a directory called include. This will be where we store all header files .h.
  3. Create a file called .gitignore (with no extension!). Because we are using GitHub, there are some files that we don’t want to upload to the remote repository like the executables and object files that are generated from compiling the program. We’ll come back to this file a bit later.
  4. Lastly, create a file called Makefile (no extension!). A makefile is essential to any large C project – it makes compilation and linking a breeze!

Note: Some people may disagree with this file structure – keeping the source and include files separate. You can structure the project however you want, but this is how I like to do it.

So, your directory should look like this:


Note that the gitignore file is hidden by default (that's what the period in front of the name does!).

Open this root folder in a developer environment (that can be Vim, Visual Studio Code (VSCode), or whatever else – I’ll be using VSCode). Now it’s time to start coding!

The first thing we’re going to actually write is the server itself. A server receives connections from clients and dispatches messages across them. For our purposes, the server is going to receive commands and messages from the client (user), and it will handle them accordingly. There’s a lot that goes into this, so we’ll break it down one step at a time. For now, let’s just handle getting a connection established.

Create a file called server.c in the src directory and server.h in the include directory. We’ll start off with the header file. We need a few definitions to get started: namely the connection IP to use, some function prototypes, and constants. Let’s just jump right into it:
          
        #ifndef SERVER_H
        #define SERVER_H
       
        typedef struct server_s {
          int socket_fd;   /* Socket file descriptor. */
          int flags;       /* Flags for the server.   */
        } server_t;
       
        enum SERVER_FLAGS {
            SERVER_ACTIVE = 1
        };
       
        extern server_t server;
       
        extern void server_init( void );
       
        #endif // SERVER_H
            
            

The first thing we do is create a typedef for a server structure. This structure will hold a few things, such as the clients that connect, the threads that handle jobs, etc. These definitions will be made later, once we get there. We also include a flag variable to keep track of the status of the server. The enum we define is simply for bookkeeping - we can store all flags that we want to use for the server here instead of doing a bunch of const int's or preprocessor #define's. Also note that we have a socket "fd". fd stands for file descriptor. A file descriptor is a handle, so to speak, meaning that this integer keeps track of the socket (connection) identifier once it is established. We need this to listen for clients later on.

We also create a variable of type server_t to reference in the server.c file. Global references/variables are generally frowned upon, but this is, effectively, the only way to do this in C. Plus, it'll be the only global we use, so it's a good compromise. You may be asking: what does extern do? Well, since we don't want to explicitly initialize a server_t variable in the header (bad practice), we tell the header "hey, there's a reference to this variable elsewhere! It's the same thing with the server_init() function. We'll call it a bit later from a different file, and that file won't care about how that function is defined, only if it is defined. :D

server_init will initialize the socket listener and other miscellaneous things. Let's go define that!

            
          #include <arpa/inet.h>
          #include <errno.h>
          #include <netdb.h>
          #include <netinet/in.h>
          #include <signal.h>
          #include <stdio.h>
          #include <stdlib.h>
          #include <string.h>
          #include <sys/socket.h>
          #include <sys/types.h>
          #include <unistd.h>
          
          #include "tcpdefs.h"
          #include "server.h"
          
          static int server_create_listener( const char *server );
          
          server_t server;

          void
          server_init( void ) {
            server.socket_fd = server_create_listener( PORT );
            printf( "Socket fd: %d\n", server.socket_fd );
          }
          
          static int
          server_create_listener( const char *server ) {
              int socket_fd;
              if ( ( socket_fd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 ) {
                perror( "Could not initialize socket.\n" );
                return -1;
              }
              return socket_fd;
          }
  

Now, I'm sure you're thinking: "Wow, that's a lot of additions!" Well, we're not even half of the way there, so don't get too excited just yet! The first thing we've done is included a bunch of C headers. Don't worry if you don't know where a lot of this stuff comes from - just include them for now.

Next, we include our server.h header which is nice, but what about tcpdefs.h? What's that, you ask? Well, we need a way of explicitly telling our server what port to start the server on. We're going to use port 8080, which is an alternate port for HTTP. The function that we need to pass this to, strangely enough, takes in the value as a string. So, since we may need to define more things for this connection (which, by the way, is TCP-based!), we're going to store these constants and other data in a header.

        
          #ifndef TCP_DEFS_H
          #define TCP_DEFS_H

          const char *PORT = "8080";

          #endif // TCP_DEFS_H
          
          

Yep, that's all there is to that! So, just make sure to include it in your server.c file, and we'll get back to that.


Let's talk about that server_create_listener function for a moment. Recall that a server listens for connections, right? Well, we have to initialize how the server actually listens before we start the listening itself. We need to initialize what's called a socket, which is just a connection between two endpoints. This socket will use TCP - a connection-based protocol, meaning that each message to and from the server has guaranteed delivery, compared to UDP which fires packets and ignores dropped ones. Since this is a chat server, we don't want packets of data to be randomly lost! We also want to set this server up to use IPv4 addressing.

To do both of these, we use the socket function which takes three parameters: a domain, a type, and a protocol. We want the domain to be AF_INET, representing IPv4 addressing. The type is SOCK_STREAM for TCP connectivity (or connection-oriented). The last parameter is a protocol, and you may be asking: didn't we just set a protocol for the first parameter? Yes, we did. Because that's the case, this can be set to 0. For more information, check out man socket(2).

Note: the man pages are extremely helpful for learning about Linux commands and C functions. To investigate, just type in the terminal (or google) man then the accompanying function.

The socket function is from the <sys/socket.h> header, so at least it's easy to remember! But, notice that we check its return value inside the if statement - this is a common and idiomatic way of checking C return values that need to be assigned. socket() returns an integer - that integer is the endpoint for the connection that we want to establish. However, if an error occurs, -1 is returned. Thus, checking to see if the value is less than 0 is a good way to error check. This will be a common practice throughout development (especially when allocating dynamic memory!).

Let's keep going (note that I only returned/printed values in these two starter functions for demonstrative purposes).


One issue that may (and will!) arise during debugging is the need to quickly retest the server. If the server is shutdown, there may still be sockets connected to its port upon restarting and listening. To prevent this, we utilize the setsockoptfunction with its SO_REUSEPORT flag.

            
              int optval = 1;
              setsockopt( socket_fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof( optval ) );
              
            

Now, we need to initialize a connection. However, we can't just accept a connection to any available IP. That's why we use what are called "hints". These hints will allow the program to find a list of possible connections, and we can choose the one we need off said list. We have to set the flags/attributes to let the system know that we want an IPv4 address for TCP connections. The AI_PASSIVE flag is new, however. It means that we plan to use this result in a call to bind.

              
                struct addrinfo hints;
                memset( &hints, 0, sizeof( hints ) );
                hints.ai_flags = AI_PASSIVE;
                hints.ai_family = AF_INET;
                hints.ai_socktype = SOCK_STREAM;
                hints.ai_protocol = 0;
                    
                struct addrinfo *result;
                int              return_val;
                if ( ( return_val = getaddrinfo( NULL, server, &hints, &result ) ) != 0 ) { 
                  fprintf( stderr, "Error in getaddrinfo: %s\n", gai_strerror( return_val ) );
                  close( socket_fd );
                  return -1; 
                }
              
            

The first thing is to create an addrinfo struct and set its necessary fields to send off to the getaddrinfo function. getaddrinfo takes in a few parameters, but the important one is the pointer to the struct addrinfo *result. To some, this may seem a little weird, but all this is doing is declaring a linked list. Since we're passing in a double pointer, the getaddrinfo function will populate (and dynamically allocate, so remember to free it later with freeaddrinfo!) this list. From there, we can poll the list to get our connection. Note that we should iterate down this list to find the connection, but for now, we can just remove the first one off the list (note that we haven't actually done this yet).


Next, we will bind the resulting connection returned from the result linked list (the head of it, rather) to our socket.

              
                int bind_return_val = bind( socket_fd, result->ai_addr, result->ai_addrlen );
                if ( bind_return_val < 0 ) {
                  perror( "DEBUG: Could not bind name and address to socket.\n" );
                  close( socket_fd );
                  return -1;
                }
              
            

We pass in the socket file descriptor, address, and address length from the result linked list head element. Note that in this code and the previous block, we do error checking. This, again, is extremely important to do! Note that if the bind fails, we close the socket file descriptor. This is, while not imperative since the program is closing anyways (and hence all memory and file descriptors are freed), still good practice.

We're almost done with this function! The last thing to do is set up the listener with the, you guessed it, listen function.

                
                  int listen_return_val = listen( socket_fd, 256 );
                  if ( listen_return_val < 0 ) {
                    perror( "DEBUG: Could not initialize socket listener.\n" );
                    close( socket_fd );
                    return -1;
                  }
                
              

listen takes two parameters: the socket file descriptor that we have been using repeatedly, and the number of concurrent connections. We'll use 256, but you could set this higher in theory.

Great! The final thing to do is free the memory from the linked list that getaddrinfo created, and to return the socket, since we'll have to repeatedly check for connections in a different function.

                
                  freeaddrinfo( result );
                  return socket_fd;
                
              

Now that we have that squared away, let's create the code that continuously listens for incoming connections/clients:
                
                  static void
                server_listen( void ) {
                  struct sockaddr_storage client_addr;
                  socklen_t               client_addr_len = sizeof( client_addr );
                  int                     comm_fd;

                  // Each time we receive a new connection, we'll iterate through this loop again.
                  while ( ( comm_fd = accept( server.socket_fd, ( struct sockaddr * ) &client_addr,
                                              &client_addr_len ) ) >= 0 ) {
                      // TODO
                  }
                }
                
              

Fortunately, this code is relatively straightforward. At it's core, while there's no connection to receive, the program blocks until one is received. Blocking is a common concept in multithreaded programming, and we'll revisit in greater detail once we introduce pthreads, but for now, just know that it means that the program will wait until a connection is established with a client. But let's take a deeper look at the code.

We set up a sockaddr_storage struct to receive client data from the accept function call. we pass the socket file descriptor as always (called from server_create_listener(...)), and a reference to the sockaddr_storage struct. This field will be populated inside the call to accept, so once the server receives a connection, we can access information from said client through that struct! Note that accept returns a communication file descriptor to that client. This file descriptor is imperative to communicating from the server to the client, so we need to make sure we save it (which is why we store it inside the while loop condition after all!).


Great! Now we can handle the stuff that happens when a client connects. Right now it's commented as TODO, so let's fill it out. All we really want to do (at this point at least) is print out the communication file descriptor and a name of the client. The client's name is their IP address. Since we're setting this server up to run on the localhost, the IP address will always be the loopback (127.0.0.1), but later on, we'll see that it can be any valid, public IP address (once we open the server to the "world" so to speak). To do this, we need to add a line of code that looks a little weird at first glance:

                
                  char *username = inet_ntoa( ( ( struct sockaddr_in * ) &client_addr )->sin_addr );
                
              

Don't get intimidated by this line! Let's break it down. We make a call to the inet_ntoa function which, when given a network address, will convert it to a string. Now, let's examine the weird casting portion(s).

We first cast the reference to client_addr (which is just a pointer!) to a struct sockaddr_in *. All this means is we're taking our sockaddr_info struct and converting it to a struct type that has information that we need. Since these two are guaranteed to be the same size, it's safe to do such a cast. Finally, we just do a standard attribute access through the -> operator to get the sin_addr field. sin_addr is of type struct in_addr, and while this doesn't mean a whole lot for us, it's exactly what inet_ntoa wants!

Now, we can just print that information out! So, let's add a line that does that inside the connection loop as I'll call it (where the TODO comment is located):

            
              printf("Received connection from %s, %d\n", username, comm_fd);
            
          

We just need to add one more thing to our server.c file: remember the server_init() function (that we, admittedly, didn't touch too much on)? We'll actually call it in a moment (for real this time!), but we need to add a call to server_listen() inside it!


Now, we can call our server_init() function in a different file. Let's call it main.c for easy reference.

            
              #include "server.h"
 
              int
              main( int argc, char *argv[] ) { 
                server_init();
                return 0;
              }
            
          

And that's all there is to it.

Now, all we have to do is compile it. For our purposes, we're going to use a Makefile. A Makefile is a collection of commands that allow for easy compilation and linking of multiple C source files, headers, and libraries. It also allows us to add option flags with ease. While I won't be going into the details of the Makefile, I encourage you to visit Colby University's tutorial on Makefiles. If you want, though, you can just copy and paste this without worrying about the implementation, as it's not completely necessary to understanding this project.

            
              CC = gcc
              EXES = main
              OBJS = main.o server.o
              CFLAGS = -g -O2 -Wno-unused-variable -Wno-unused-parameter -Wno-unused-function -Wall -Werror -Iinclude
              LDFLAGS = -lm
              
              all: $(EXES)
              
              main: $(OBJS)
                $(CC) $(CFLAGS) -o main $(OBJS) $(LDFLAGS)
              
              main.o: src/main.c
                $(CC) -c $(CFLAGS) $< -o $@
              
              server.o: src/server.c
                $(CC) -c $(CFLAGS) $< -o $@
              
              clean:
                rm -rf $(OBJS) $(EXES) *.dSYM
            
          

This is a very simple Makefile, and we're going to spice it up a bit later (well, maybe not spice it up, but rather add more files and targets), but this is all we need to compile and run. Type make, then ./main. If it worked, you should see... nothing! That's right, because it enters that connection while loop, right? So it makes sense that nothing is printing at the moment.

I hear you asking: "Well, how does a client connect?" Great question! We'll use the simple Unix command nc standing for "netcat" to connect. Type nc localhost 8080 into a separate terminal, and watch what happens:



Yep, it shows my connection! Let's try it with another one!

Did you expect that? You should have! It's obviously a different file descriptor since it's not the same connection, but it is the same client. The connection source changed, but I didn't change where the connection on the network started.

Awesome! Next time we'll work on setting up a structure for catching and "registering" these clients. Stay tuned!

GitHub Code for Part 1.