init commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
;;; 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)
|
||||
Reference in New Issue
Block a user