Files
CljElixir/examples/tcp_chat_server.clje
2026-03-09 23:09:46 -04:00

132 lines
5.2 KiB
Plaintext

;;; 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:<username> | MSG:<text> | QUIT
;;; Server → Client: SYS:<text> | MSG:<user>:<text> | JOIN:<user> | QUIT:<user>
(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:<text> 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:<username>
;; 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:<username>\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 -- <username>")
(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)