init commit
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
;;; ChatRoom — process-based chat room using spawn/send/receive
|
||||
;;;
|
||||
;;; This is a single-VM demo showing BEAM concurrency primitives.
|
||||
;;; Run with: mix clje.run examples/chat_room.clje
|
||||
;;;
|
||||
;;; For a multi-terminal chat experience, see:
|
||||
;;; examples/tcp_chat_server.clje and examples/tcp_chat_client.clje
|
||||
|
||||
(ns ChatRoom
|
||||
(:require [clje.core :refer :all]
|
||||
[Enum] [IO] [Map] [Process] [String]))
|
||||
|
||||
;; ── Room loop ─────────────────────────────────────────────────────
|
||||
;; Manages members map of {username → pid} and broadcasts messages.
|
||||
|
||||
(defn run-loop [state]
|
||||
(receive
|
||||
[:join username pid]
|
||||
(let [members (assoc (:members state) username pid)]
|
||||
(send pid #el[:welcome username (count members)])
|
||||
;; Notify existing members
|
||||
(doseq [[_name member-pid] (:members state)]
|
||||
(send member-pid #el[:system (str username " joined the room")]))
|
||||
(recur (assoc state :members members)))
|
||||
|
||||
[:message from body] :guard [(is-binary body)]
|
||||
(do
|
||||
(doseq [[_name pid] (:members state)]
|
||||
(send pid #el[:chat from body]))
|
||||
(recur state))
|
||||
|
||||
[:leave username]
|
||||
(let [new-members (dissoc (:members state) username)]
|
||||
(doseq [[_name pid] new-members]
|
||||
(send pid #el[:system (str username " left the room")]))
|
||||
(recur (assoc state :members new-members)))
|
||||
|
||||
[:who reply-pid]
|
||||
(do
|
||||
(send reply-pid #el[:members (Map/keys (:members state))])
|
||||
(recur state))
|
||||
|
||||
:shutdown
|
||||
(do
|
||||
(doseq [[_name pid] (:members state)]
|
||||
(send pid :room-closed))
|
||||
:ok)
|
||||
|
||||
:after 5000
|
||||
(if (== (count (:members state)) 0)
|
||||
:empty-timeout
|
||||
(recur state))))
|
||||
|
||||
;; ── User listener ─────────────────────────────────────────────────
|
||||
;; Collects messages received by a user and prints them.
|
||||
|
||||
(defn listen [username messages]
|
||||
(receive
|
||||
[:welcome _name member-count]
|
||||
(do
|
||||
(println (str " [" username " sees: Welcome! " member-count " user(s) here]"))
|
||||
(ChatRoom/listen username messages))
|
||||
|
||||
[:chat from body]
|
||||
(do
|
||||
(println (str " [" username " sees: " from "> " body "]"))
|
||||
(ChatRoom/listen username (cons #el[from body] messages)))
|
||||
|
||||
[:system text]
|
||||
(do
|
||||
(println (str " [" username " sees: * " text "]"))
|
||||
(ChatRoom/listen username messages))
|
||||
|
||||
:room-closed
|
||||
(println (str " [" username " sees: room closed]"))
|
||||
|
||||
:dump
|
||||
messages
|
||||
|
||||
:after 2000
|
||||
(do
|
||||
(println (str " [" username " done listening]"))
|
||||
messages)))
|
||||
|
||||
;; ── Run the demo ────────────────────────────────────────────────────
|
||||
|
||||
(println "=== ChatRoom Demo ===\n")
|
||||
|
||||
;; Start the room
|
||||
(let [room (spawn (fn [] (ChatRoom/run-loop {:owner "system" :members {}})))
|
||||
alice-listener (spawn (fn [] (ChatRoom/listen "alice" (list))))
|
||||
bob-listener (spawn (fn [] (ChatRoom/listen "bob" (list))))
|
||||
carol-listener (spawn (fn [] (ChatRoom/listen "carol" (list))))]
|
||||
|
||||
;; Alice joins
|
||||
(println "Alice joins...")
|
||||
(send room #el[:join "alice" alice-listener])
|
||||
(Process/sleep 100)
|
||||
|
||||
;; Bob joins
|
||||
(println "\nBob joins...")
|
||||
(send room #el[:join "bob" bob-listener])
|
||||
(Process/sleep 100)
|
||||
|
||||
;; Alice sends a message
|
||||
(println "\nAlice sends a message...")
|
||||
(send room #el[:message "alice" "Hello everyone!"])
|
||||
(Process/sleep 100)
|
||||
|
||||
;; Carol joins
|
||||
(println "\nCarol joins...")
|
||||
(send room #el[:join "carol" carol-listener])
|
||||
(Process/sleep 100)
|
||||
|
||||
;; Bob sends a message
|
||||
(println "\nBob sends a message...")
|
||||
(send room #el[:message "bob" "Hey Alice! Welcome Carol!"])
|
||||
(Process/sleep 100)
|
||||
|
||||
;; Carol sends a message
|
||||
(println "\nCarol sends a message...")
|
||||
(send room #el[:message "carol" "Thanks Bob!"])
|
||||
(Process/sleep 100)
|
||||
|
||||
;; Check who's online
|
||||
(println "\nWho's online?")
|
||||
(send room #el[:who *self*])
|
||||
(receive
|
||||
[:members names]
|
||||
(println (str " Online: " (Enum/join names ", "))))
|
||||
|
||||
;; Bob leaves
|
||||
(println "\nBob leaves...")
|
||||
(send room #el[:leave "bob"])
|
||||
(Process/sleep 100)
|
||||
|
||||
;; Shutdown the room
|
||||
(println "\nShutting down room...")
|
||||
(send room :shutdown)
|
||||
(Process/sleep 200)
|
||||
|
||||
(println "\n=== Demo complete ==="))
|
||||
@@ -0,0 +1,130 @@
|
||||
;;; TCP Chat Client — connects to the TCP chat server
|
||||
;;;
|
||||
;;; Usage:
|
||||
;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- <username>
|
||||
;;;
|
||||
;;; The server must be running first:
|
||||
;;; mix clje.run --no-halt examples/tcp_chat_server.clje
|
||||
;;;
|
||||
;;; Commands:
|
||||
;;; Type a message and press Enter to send
|
||||
;;; /who — list online users
|
||||
;;; /quit — disconnect and exit
|
||||
|
||||
(ns TcpChatClient
|
||||
(:require [clje.core :refer :all]
|
||||
[IO] [String] [System] [erlang] [gen_tcp]))
|
||||
|
||||
;; ── Receiver process ──────────────────────────────────────────────
|
||||
;; Listens for TCP messages from the server and prints them.
|
||||
|
||||
(defn receiver [socket parent]
|
||||
(receive
|
||||
[:tcp _sock data]
|
||||
(let [line (String/trim data)]
|
||||
(cond
|
||||
(String/starts-with? line "MSG:")
|
||||
(let [rest (String/slice line 4 (String/length line))
|
||||
parts (String/split rest ":" (list #el[:parts 2]))
|
||||
from (hd parts)
|
||||
text (hd (tl parts))]
|
||||
(IO/puts (str from "> " text))
|
||||
(TcpChatClient/receiver socket parent))
|
||||
|
||||
(String/starts-with? line "SYS:")
|
||||
(do
|
||||
(IO/puts (str "* " (String/slice line 4 (String/length line))))
|
||||
(TcpChatClient/receiver socket parent))
|
||||
|
||||
(String/starts-with? line "JOIN:")
|
||||
(do
|
||||
(IO/puts (str "* " (String/slice line 5 (String/length line)) " joined"))
|
||||
(TcpChatClient/receiver socket parent))
|
||||
|
||||
(String/starts-with? line "QUIT:")
|
||||
(do
|
||||
(IO/puts (str "* " (String/slice line 5 (String/length line)) " left"))
|
||||
(TcpChatClient/receiver socket parent))
|
||||
|
||||
:else
|
||||
(do
|
||||
(IO/puts line)
|
||||
(TcpChatClient/receiver socket parent))))
|
||||
|
||||
[:tcp_closed _sock]
|
||||
(do
|
||||
(IO/puts "* Connection closed by server.")
|
||||
(System/halt 0))
|
||||
|
||||
[:tcp_error _sock _reason]
|
||||
(do
|
||||
(IO/puts "* Connection error.")
|
||||
(System/halt 1))))
|
||||
|
||||
;; ── Input loop ────────────────────────────────────────────────────
|
||||
;; Reads from stdin and sends to the server.
|
||||
|
||||
(defn send-line [socket line]
|
||||
(gen_tcp/send socket line))
|
||||
|
||||
(defn input-loop [socket]
|
||||
(let [line (IO/gets "")]
|
||||
(cond
|
||||
(== line :eof)
|
||||
(do
|
||||
(TcpChatClient/send-line socket "QUIT\n")
|
||||
(gen_tcp/close socket)
|
||||
(System/halt 0))
|
||||
|
||||
:else
|
||||
(let [trimmed (String/trim line)]
|
||||
(cond
|
||||
(== trimmed "/quit")
|
||||
(do
|
||||
(TcpChatClient/send-line socket "QUIT\n")
|
||||
(gen_tcp/close socket)
|
||||
(IO/puts "Goodbye!")
|
||||
(System/halt 0))
|
||||
|
||||
(== trimmed "")
|
||||
(TcpChatClient/input-loop socket)
|
||||
|
||||
:else
|
||||
(do
|
||||
(TcpChatClient/send-line socket (str "MSG:" trimmed "\n"))
|
||||
(TcpChatClient/input-loop socket)))))))
|
||||
|
||||
;; ── Connect ───────────────────────────────────────────────────────
|
||||
|
||||
(defn start [username]
|
||||
(case (gen_tcp/connect (erlang/binary-to-list "127.0.0.1") 4040
|
||||
(list :binary #el[:active true] #el[:packet :line]))
|
||||
[:ok socket]
|
||||
(do
|
||||
;; Send JOIN
|
||||
(TcpChatClient/send-line socket (str "JOIN:" username "\n"))
|
||||
|
||||
;; Spawn receiver and hand it the socket
|
||||
(let [me *self*
|
||||
recv-pid (spawn (fn [] (TcpChatClient/receiver socket me)))]
|
||||
(gen_tcp/controlling-process socket recv-pid)
|
||||
|
||||
;; Run input loop in the main process
|
||||
(IO/puts (str "Connected as " username ". Type a message or /quit to exit."))
|
||||
(TcpChatClient/input-loop socket)))
|
||||
|
||||
[:error reason]
|
||||
(do
|
||||
(IO/puts (str "Could not connect: " reason))
|
||||
(IO/puts "Is the server running? Start it with:")
|
||||
(IO/puts " mix clje.run --no-halt examples/tcp_chat_server.clje")
|
||||
(System/halt 1))))
|
||||
|
||||
;; ── Entry point ─────────────────────────────────────────────────────
|
||||
|
||||
(let [args (System/argv)]
|
||||
(if (== (count args) 0)
|
||||
(do
|
||||
(IO/puts "Usage: mix clje.run --no-halt examples/tcp_chat_client.clje -- <username>")
|
||||
(System/halt 1))
|
||||
(TcpChatClient/start (hd args))))
|
||||
@@ -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