132 lines
5.2 KiB
Plaintext
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)
|