;;; TCP Chat Server — multi-terminal chat using gen_tcp ;;; ;;; Start the server: ;;; mix clje.run --no-halt examples/tcp_chat_server.clje ;;; ;;; Then connect clients in separate terminals: ;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- alice ;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- bob ;;; ;;; Protocol (line-based): ;;; Client → Server: JOIN: | MSG: | QUIT ;;; Server → Client: SYS: | MSG:: | JOIN: | QUIT: (ns TcpChatServer (:require [clje.core :refer :all] [Map] [String] [gen_tcp] [inet])) ;; ── Room manager ────────────────────────────────────────────────── ;; Holds {username → socket} map, broadcasts messages to all members. (defn room-loop [members] (receive [:join username socket handler-pid] (do (TcpChatServer/broadcast members (str "JOIN:" username "\n")) (TcpChatServer/send-line socket (str "SYS:Welcome " username "! " (count members) " user(s) online.\n")) (TcpChatServer/room-loop (assoc members username #el[socket handler-pid]))) [:msg username text] (do (TcpChatServer/broadcast members (str "MSG:" username ":" text "\n")) (TcpChatServer/room-loop members)) [:quit username] (let [new-members (dissoc members username)] (TcpChatServer/broadcast new-members (str "QUIT:" username "\n")) (TcpChatServer/room-loop new-members)) [:list reply-pid] (do (send reply-pid #el[:members (Map/keys members)]) (TcpChatServer/room-loop members)))) (defn broadcast [members line] (doseq [[_name [socket _pid]] members] (gen_tcp/send socket line))) (defn send-line [socket line] (gen_tcp/send socket line)) ;; ── Per-client handler ──────────────────────────────────────────── ;; Receives TCP data via active mode and forwards to room manager. (defn client-handler [socket room username] (receive [:tcp _sock data] (let [line (String/trim data)] (cond (String/starts-with? line "MSG:") (do (send room #el[:msg username (String/slice line 4 (String/length line))]) (TcpChatServer/client-handler socket room username)) (== line "QUIT") (do (send room #el[:quit username]) (gen_tcp/close socket) (println (str " [" username " quit]"))) :else (do (TcpChatServer/send-line socket (str "SYS:Unknown command. Send MSG: or QUIT\n")) (TcpChatServer/client-handler socket room username)))) [:tcp_closed _sock] (do (send room #el[:quit username]) (println (str " [" username " disconnected]"))) [:tcp_error _sock _reason] (do (send room #el[:quit username]) (println (str " [" username " error]"))))) ;; ── Accept loop ─────────────────────────────────────────────────── (defn accept-loop [listen-socket room] (case (gen_tcp/accept listen-socket) [:ok client-socket] (do (TcpChatServer/handle-new-client client-socket room) (TcpChatServer/accept-loop listen-socket room)) [:error reason] (println (str "Accept error: " reason)))) (defn handle-new-client [socket room] ;; First message must be JOIN: ;; Socket starts in passive mode so we can recv synchronously (case (gen_tcp/recv socket 0 5000) [:ok data] (let [line (String/trim data)] (if (String/starts-with? line "JOIN:") (let [username (String/slice line 5 (String/length line)) ;; Switch to active mode for the handler _ (inet/setopts socket (list #el[:active true])) handler (spawn (fn [] (TcpChatServer/client-handler socket room username)))] (gen_tcp/controlling-process socket handler) (send room #el[:join username socket handler]) (println (str " [" username " connected]"))) (do (TcpChatServer/send-line socket "SYS:First message must be JOIN:\n") (gen_tcp/close socket)))) [:error _reason] (gen_tcp/close socket))) ;; ── Start ───────────────────────────────────────────────────────── (defn start [port] (case (gen_tcp/listen port (list :binary #el[:active false] #el[:packet :line] #el[:reuseaddr true])) [:ok listen-socket] (do (println (str "TCP Chat Server listening on port " port)) (println "Connect with: mix clje.run --no-halt examples/tcp_chat_client.clje -- ") (let [room (spawn (fn [] (TcpChatServer/room-loop {})))] (TcpChatServer/accept-loop listen-socket room))) [:error reason] (println (str "Failed to listen on port " port ": " reason)))) ;; Start the server on port 4040 (TcpChatServer/start 4040)