Skip to content
Rakshita Varadarajan edited this page Dec 8, 2020 · 5 revisions

IEEE Distributed DNS
Blog 1: Learning Phase

Implementation of a working terminal-based client-server chat application

Prerequisite Learning

The prerequisite learning required to implement this chat app required the preliminary learning and basic understanding of concepts in Operating Systems (OS) & Networks, along with learning and familiarizing ourselves with Golang.

In OS, we learned the basic concept of processes, threads, process synchronization (mutexes), and deadlocks.

In Networks, we acquired knowledge related to the TCP and IP protocols and understood the functions of the Transport and Network layer.

As for Go, after finishing the ‘Tour of Go’ which helped us become comfortable with the language, we learned about more of the features of the language which helped us implement the application efficiently.

Implementation in Go

Since this is a client-server chat application, we decided to implement communication between the client and server using raw sockets. For this, we used the ‘net’ package in Golang which had all of the required methods to implement this efficiently using TCP sockets.

In our program, the single central server handles communication with multiple clients concurrently. Golang was the language chosen as with its features of goroutines and channels, it provides ample support for concurrency and thus suited our requirements the most.

A goroutine is a function that is capable of running concurrently with other functions. A goroutine is a lightweight thread of execution, meaning that it is lighter than a thread and thus it takes up fewer resources as compared to a thread. So, we can create thousands of these goroutines. By using the keyword ‘go’ followed normally by the function call, we create a goroutine.

Channels allow goroutines to communicate with one another and to synchronize their executions. They can be of different data types to pass results of that datatype from one goroutine to another.

We’ve also used the ‘context’ package in Golang, which alerts the goroutine when it is time to stop the execution of the same and to return. The contexts used are primarily of the cancellable context type, which implies that either till the goroutine returns or till the context is canceled, the execution of the same keeps going on.

As we are accessing the same data (critical section of the code) over multiple goroutines, we have also used the mutex (mutual exclusion) concept of concurrency as a locking mechanism to ensure only one goroutine can access the data at a given time. ‘Sync’ package contains the Mutex, which in turn contains the Lock and Unlock methods. (More on this has been explained in the server section).

Execution Flow

It consists of the main program which amalgamates the client and server. Initially the main has to be run and put in server mode. Then we can run the main program in client mode (by starting the program on multiple terminals on the local system/other systems on the network), and proceed to communicate with other clients connected to the server.

The main first initialises a ‘context’ when it is run. This is done so that when the user exits the program by pressing ctrl-C abruptly (or when other causes of termination, such as the server shutting down occur), the different goroutines being run can terminate gracefully doing what's needed to exit.

The main program imports the server and client functions. In Server mode, it requests the user to enter the address/port to attach to and its password. On providing the same it sets things up and runs the server function.

In Client mode, it requests the user to enter their name and server password and the server address/port to connect to. On entering this, all this is set up and runs the client function.

Server Functions

When we run the program by passing ‘-s’ as the command line argument we enter the server part of the program. An instance of the new server is created with the basic details such as server password and port number. Then the server run function is called.

The server structure consists of :-

  • Password string - the password of the server
  • Address string - the port to connect to
  • ClientConnections map[string]net.Conn - maps client username to connection
  • sync.RWMutex - Readers Writers mutual exclusion lock

Run - It is called from main.go . It binds a socket to the port and then starts listening on it. Spawns a goroutine ListenForConnections to listen for new client connections and write it to a channel on which the Run function is listening on, to handle this new connection. Whenever a new client joins it will spawn a goroutine ManageClient.

ManageClient - It authenticates the user by checking username and password. It spawns another goroutine ListenForMessages to handle incoming messages from the client. We use a map to maintain a list of clients and it maps the username to the connection.

ListenForMessages - This goroutine listens for messages sent by a given client on the corresponding socket connection between that client and the server. Since TCP only guarantees reliability we will be appending some special characters to the message and then wait till they have been received to ensure that we got the entire message. Our server provides the following functions.

Private Messaging (pm) - Sends a message from 1 sender to 1 receiver only.

Broadcast Message (broad) - Sends a message from the sender to all other clients connected to the server. Using the map it will send the message to everyone except the person whose username matches with the sender’s name.(i.e the sender itself)

Quit - Deletes the client connection from the map and then closes the connection to leave the server.

For all the above functions to maintain synchronization we use a RWmutex which is a multiple readers / single writer mutual exclusion lock that helps to solve the readers writers problem. Whenever we insert a new client in the map or search for a client in the map we need to use this mutex. At a time we can allow multiple clients to read from the map but no client should write during this time. This can be done by using RLock() and RUnlock(). If we want to use the other way which is when a client is writing into the map nobody else should read or write then it can be done by using Lock() and Unlock().

Client Functions

When we run the program by passing ‘-c’ as the command line argument we enter the client part of the program. An instance of the new client is created with the basic details such as username, password and port number. Then the client run function is called.

The client structure consists of :-

  • Username string - username of the client
  • Password string - the password of the server
  • Address string - the ip:port to connect to

Run - It is called from main.go . Based on the previous details entered by the user it tries to establish a tcp connection to the server. It also authenticates the user by verifying that the password matches with the server password and that the username is a unique one. It spawns 2 goroutines HandleClient and HandleServer.

HandleClient - Called by Run(). It spawns a goroutine getClientMessage that listens for user messages entered in STDIN and then passes it to a channel that goes back to HandleClient function. So when a message is received in the channel it immediately sends it to the server.

HandleServer - Called by Run(). It spawns a goroutine getServerMessage that reads messages sent by the server. TCP only guarantees reliability (i.e., whatever is sent is eventually received in the correct order). But it does not ensure that the entire message sent by the sender is received in one go by the receiver - i.e., it may deliver the message part-by-part. To ensure that the entire message has been received, the receiver waits till certain special characters that we append to the message at the sender side to mark the end of the message have been received, and then sends the message in a channel back to the HandleServer function which then prints the message.

Docker

Docker is a software platform for building applications based on containers — small and lightweight execution environments that make shared use of the operating system kernel but otherwise run in isolation from one another. The need for this is so that applications on the same host or cluster are isolated from one another so they don’t unduly interfere with each other’s operation or maintenance. This is difficult due to the fact that different applications might need different versions of a package and libraries. One way to solve this is virtual machines but they have their own OS and are bulky. Containers on the other hand isolate applications execution environments from one another, but share the underlying OS kernel. They are much lighter and use far less resources. Containers provide a highly efficient and highly granular mechanism for combining software components into the kinds of application and service stacks needed in a modern enterprise, and for keeping those software components updated and maintained.

Components of Docker:

  • Dockerfile - It is a text file written in an understandable syntax that has instructions to build a docker image.

  • Docker Image - A docker image is a portable file containing specifications or which software components the container will run and how. This is built using the ‘build’ instruction having the dockerfile written.

  • Docker run - An image is run using this command. Containers are designed to be transient and temporary, but they can be stopped and restarted, which launches the container into the same state as when it was stopped. Further, multiple container instances of the same image can be run simultaneously (as long as each container has a unique name).