|
1 | 1 | (ns chat-server.core
|
2 | 2 | (:require [clj-sockets.core :as socket]
|
3 |
| - [chat-server.repl :as repl]) |
| 3 | + [chat-server.repl :as repl] |
| 4 | + [clojure.string :as string]) |
4 | 5 | (:gen-class))
|
5 | 6 |
|
6 |
| -(def clients (ref {})) |
| 7 | +(def clients (ref [])) |
7 | 8 |
|
| 9 | +(defn write-line |
| 10 | + "Write a single message to a client, returning the client." |
| 11 | + [client message] |
| 12 | + (socket/write-line (:socket client) message) |
| 13 | + client) |
8 | 14 |
|
9 |
| -(defn serve-client |
10 |
| - [nick client] |
11 |
| - (doseq [line (socket/read-lines client)] |
12 |
| - (println "got message from user:" line) |
13 |
| - (let [other-clients (vals (dissoc @clients nick))] |
14 |
| - (case |
15 |
| - (first (clojure.string/split line #" ")) |
16 |
| - ;; Instructor Note: explain why a doall is needed here |
17 |
| - "MSG" (doall (map #(socket/write-line % (str nick ": " (subs line 4))) other-clients)) |
| 15 | +(defn error |
| 16 | + "Write an error to a socket, returning nil." |
| 17 | + [socket message] |
| 18 | + (socket/write-line socket (str "ERROR: " message))) |
18 | 19 |
|
19 |
| - ;;TODO: Process other kinds of command here |
| 20 | +(defn nick-exists? |
| 21 | + "Return truthy if a nick is already in use, otherwise nil." |
| 22 | + [nick] |
| 23 | + (let [nicks (map #(-> % deref :nick) @clients)] |
| 24 | + (some #{nick} nicks))) |
20 | 25 |
|
21 |
| - ;; else |
22 |
| - (socket/write-line client "ERROR: I don't understand"))))) |
| 26 | +(defn set-nick |
| 27 | + "Set the nick of an existing client. This function is transactional." |
| 28 | + [client nick] |
| 29 | + (dosync |
| 30 | + (if (nick-exists? nick) |
| 31 | + (error (:socket @client) "nick already exists") |
| 32 | + (send-off client assoc :nick nick)))) |
23 | 33 |
|
24 |
| -(defn new-client |
| 34 | +(defn send-message |
| 35 | + "Send a message to all clients but the originating one." |
| 36 | + [client message] |
| 37 | + (doseq [d-client @clients] |
| 38 | + (when-not (= client d-client) |
| 39 | + (send-off d-client write-line (str (:nick @client) ": " message))))) |
| 40 | + |
| 41 | +(defn terminate-client! |
| 42 | + "Close the socket and remove the client from the list." |
| 43 | + [client] |
| 44 | + (try |
| 45 | + (socket/close-socket (:socket @client)) |
| 46 | + (dosync |
| 47 | + (alter clients (partial remove #{client}))) |
| 48 | + (catch Throwable e |
| 49 | + (println (.getMessage e))))) |
| 50 | + |
| 51 | +(defn listen-client |
| 52 | + "Listen for and dispatch incoming messages from a client." |
25 | 53 | [client]
|
26 |
| - (let [command (socket/read-line client)] |
27 |
| - (println "got message:" command) |
28 |
| - (if-let [[_ nick] (re-matches #"USER (.*)" command)] |
29 |
| - ;; Instructor note: explain why we have to use a transaction here to make sure checking if user exists and adding them happens atomically |
30 |
| - (if (dosync |
31 |
| - (when-not (get @clients nick) |
32 |
| - (alter clients assoc nick client))) |
33 |
| - |
34 |
| - (serve-client nick client) |
35 |
| - |
36 |
| - (do |
37 |
| - (socket/write-line client "ERROR: Nick already taken") |
38 |
| - (socket/close-socket client)))))) |
| 54 | + (let [{:keys [socket nick channels] :or {channels []}} @client] |
| 55 | + (loop [line (socket/read-line socket)] |
| 56 | + (let [[command & words] (string/split line #" ")] |
| 57 | + (case command |
| 58 | + "USER" (set-nick client (string/join "-" words)) |
| 59 | + "MSG" (send-message client (string/join " " words)) |
| 60 | + "QUIT" (terminate-client! client) |
| 61 | + (error socket "I don't understand"))) |
| 62 | + (when-not (.isClosed socket) |
| 63 | + (recur (socket/read-line socket)))))) |
| 64 | + |
| 65 | +(defn handle-client-error |
| 66 | + "Handle a client error." |
| 67 | + [the-agent exception] |
| 68 | + (let [s (:socket @the-agent) |
| 69 | + msg (.getMessage exception)] |
| 70 | + (when-not (.isClosed s) |
| 71 | + (error s msg)) |
| 72 | + (println msg) |
| 73 | + (terminate-client! the-agent))) |
| 74 | + |
| 75 | +(defn new-client |
| 76 | + "Takes a freshly opened socket connection, creates a new client and |
| 77 | + calls the dispatcher." |
| 78 | + [s] |
| 79 | + (loop [line (socket/read-line s)] |
| 80 | + (if-let [[_ nick] (re-matches #"USER (.*)" line)] |
| 81 | + (if-let [client (dosync |
| 82 | + (when-not (nick-exists? nick) |
| 83 | + (let [client (agent {:socket s :nick nick :channels []} |
| 84 | + :error-mode :continue |
| 85 | + :error-handler handle-client-error)] |
| 86 | + (alter clients conj client) |
| 87 | + client)))] |
| 88 | + (listen-client client) |
| 89 | + (do (error s "nick is already taken, try another") |
| 90 | + (recur (socket/read-line s)))) |
| 91 | + (do (error s "first set a nick with USER") |
| 92 | + (recur (socket/read-line s)))))) |
39 | 93 |
|
40 | 94 | (defn -main
|
41 | 95 | "The hello world of chat servers"
|
|
0 commit comments