Hermes/IBRAHIM_MKUSA.md
2017-05-01 00:17:55 -04:00

227 lines
8.0 KiB
Markdown

# Hermes - A chat server and client written in Racket
## Ibrahim Mkusa
### April 30, 2017
# Overview
Hermes is a chat server and client written in Racket. One can run the Hermes
server on any machine that is internet accessible. The Hermes clients then
connect to the server from anywhere on the internet. It's inspired by chat
systems and clients like irc.
The goal in building Hermes was to expose myself to several concepts integral to
systems like networking, synchronization, and multitasking.
# Libraries Used
Most libraries and utilities used are part of base Drracket installation and
therefore do not need to be imported.
The date and time modules were used for various time related queries.
The tcp module was used for communication via Transmission Control Protocol.
Concurrency and synchronization modules that provide threads, and semaphores
were also used.
Below are libraries that were not part of base system:
```
(require racket/gui/base)
(require math/base)
```
* The ```racket/gui/base``` library used to build graphical user interface.
* The ```math/base``` library was used for testing purposes. It was used to
generated random numbers.
# Key Code Excerpts
Here is a discussion of the most essential procedures, including a description of how they embody ideas from
UMass Lowell's COMP.3010 Organization of Programming languages course.
Five examples are shown and they are individually numbered.
## 1. Tracking client connections using an object and closures.
The following code defines and creates a global object, ```make-connections```
that abstracts client connections. It also creates a semaphore to control access
to ```make-connections``` object.
```
(define (make-connections connections)
(define (null-cons?)
(null? connections))
(define (add username in out)
(set! connections (append connections (list (list username in out))))
connections)
(define (cons-list)
connections)
(define (remove-ports in out)
(set! connections
(filter
(lambda (ports)
(if (and (eq? in (get-input-port ports))
(eq? out (get-output-port ports)))
#f
#t))
connections)))
(define (dispatch m)
(cond [(eq? m 'null-cons) null-cons?]
[(eq? m 'cons-list) cons-list]
[(eq? m 'remove-ports) remove-ports]
[(eq? m 'add) add]))
dispatch)
(define c-connections (make-connections '()))
(define connections-s (make-semaphore 1)) ;; control access to connections
```
When the tcp-listener accepts a connection from a client, the associated input
output ports along with username are added as an entry in ```make-connections``` via ```add``` function.
External functions can operate on the connections by securing the semaphore,
and then calling ```cons-list``` to expose the underlying list of connections.
```remove-ports``` method is also available to remove input output ports from
managed connections.
## 2. Tracking received messages via objects and closures.
The code below manages broadcast messages from one client to the rest. It wraps
a list of strings inside an object that has functions similar to ```make-connections``` for
exposing and manipulating the list from external functions. The code creates
```make-messages``` global object and a semaphore to control access to it from
various threads of execution.
```
(define (make-messages messages)
(define (add message)
(set! messages (append messages (list message)))
messages)
(define (mes-list)
messages)
(define (remove-top)
(set! messages (rest messages))
messages)
(define (dispatch m)
(cond [(eq? m 'add) add]
[(eq? m 'mes-list) mes-list]
[(eq? m 'remove-top) remove-top]))
dispatch)
(define c-messages (make-messages '()))
(define messages-s (make-semaphore 1)) ;; control access to messages
```
## 3. Using map to broadcast messages from client to clients
The ```broadcast``` function is called repeatedly in a loop to extract a message
from ```make-messages``` object, and send it to every other client. It uses the
```make-connections``` objects to extract output port of a client. The ```map```
routine is called on every client in the connections object to send it
a message.
```
(define broadcast
(lambda ()
(semaphore-wait messages-s)
(cond [(not (null? ((c-messages 'mes-list))))
(map
(lambda (ports)
(if (not (port-closed? (get-output-port ports)))
(begin
(displayln (first ((c-messages 'mes-list))) (get-output-port ports))
(flush-output (get-output-port ports)))
(displayln-safe "Failed to broadcast. Port not open." error-out-s error-out)))
((c-connections 'cons-list)))
(displayln-safe (first ((c-messages 'mes-list))) convs-out-s convs-out)
;; remove top message from "queue" after broadcasting
((c-messages 'remove-top))
; debugging displayln below
; (displayln "Message broadcasted")
]) ; end of cond
(semaphore-post messages-s)))
```
After the message is send, the message is removed from the "queue" via the
```remove-top```.
The code snippet below creates a thread that iteratively calls ```broadcast```
every interval, where interval(in secs) is defined by ```sleep-t```.
```sleep``` is very important for making Hermes behave gracefully
in a system. Without it, it would be called at the rate derived from cpu clock
rate. This raises cpu temperatures substantially, and make cause a pre-mature
system shutdown.
```
(thread (lambda ()
(displayln-safe "Broadcast thread started!")
(let loopb []
(sleep sleep-t) ;; wait 0.2 ~ 0.5 secs before beginning to broadcast
(broadcast)
(loopb))))
```
## 4. Filtering a List of connections to find recipient of a whisper
I implemented a whisper functionality, where a user can whisper to any user in
the chat room. The whisper message is only sent to specified user. To implement
this i used ```filter``` over the connections, where the predicate tested whether the
current list item matched that of a specific user.
```
(define whisper (regexp-match #px"(.*)/whisper\\s+(\\w+)\\s+(.*)" evt-t0))
[whisper
(semaphore-wait connections-s)
; get output port for user
; this might be null
(define that-user-ports
(filter
(lambda (ports)
(if (string=? (whisper-to whisper) (get-username ports))
#t
#f))
((c-connections 'cons-list))))
; try to send that user the whisper
(if (and (null? that-user-ports)
#t) ; #t is placeholder for further checks
(begin
(displayln "User is unavailable. /color blue" out)
(flush-output out))
(begin
(displayln (string-append "(whisper) "
(whisper-info whisper) (whisper-message whisper))
(get-output-port (car that-user-ports)))
(flush-output (get-output-port (car that-user-ports)))))
(semaphore-post connections-s)]
```
The snippet above is part of cond statement that tests contents of input from
clients to determine what the client is trying wants/trying to do. The top-line
is using regexes to determine whether the received message is a whisper or not.
## 5. Selectors for dealing with content of a whisper from clients
Below are are three selectors that help abstract the contents of a whisper
message.
```
(define (whisper-info exp)
(cadr exp))
(define (whisper-to exp)
(caddr exp))
(define (whisper-message exp)
(cadddr exp))
```
```whisper-info``` retrieves the date-time and username info.
```whisper-to``` retrieves the username of the intented recipient of a whisper.
```whisper-message``` retrieves the actual whisper.