Hermes/IBRAHIM_MKUSA.md

8.4 KiB

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.

Authorship note: All of the code described here was written by myself.

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.

** note ** : 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.

; whisper selector for the username and message
(define (whisper-info exp)
  (cadr exp))

(define (whisper-to exp)
  (caddr exp))

(define (whisper-message exp)
  (cadddr exp))
(define (list-all-folders folder-id)
  (let ((this-level (list-folders folder-id)))
    (begin
      (display (length this-level)) (display "... ")
      (append this-level
              (flatten (map list-all-folders (map get-id this-level)))))))

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.