Echo TCP server in Clojure
Repeating the same thing as the other note where it was created the simplest UTP server, but this time in TCP. The goal is the same as before: echo back whatever it receives in uppercase.
Naive approach
(ns user (:require [clojure.string :as str]) (:import (java.net ServerSocket) (java.io BufferedReader InputStreamReader PrintWriter)))
(defn create-tcp-server [{:keys [port handler]}] (with-open [socket (ServerSocket. port) client-socket (.accept socket) in (BufferedReader. (InputStreamReader. (.getInputStream client-socket))) out (PrintWriter. (.getOutputStream client-socket) true)] (let [line (.readLine in)] (.println out (handler line)))))
(create-tcp-server {:port 1230 :handler str/upper-case})We can send a message to it and it echos back in uppercase.
nc -t 127.0.0.1 1230helloHELLOThe first problem with this implementation is that it only receives a message and it closes the connection. Before fixing that let’s understand the classes used.
Java classes
ServerSocket
It’s used for connection-oriented communication.
Key methods:
accept(): listens for a connection to be made and accept it. Blocks until a connection is madebind(SocketAddress endpoint): binds the socket to an specific IPclose(): closes the socket
In the naive approach the constructor already binds to the port. with-open also takes care of closing the connection once the code has executed.
BufferedReader
A nice class for reading text. It’s efficient and thread-safe.
Key methods:
readLine(): reads a line of text. A line is considered to be terminated by any one of a line feed (‘\n’), a carriage return (‘\r’), or a carriage return followed immediately by a linefeed.
PrintWriter
It’s used for printing text to an OutputStream. Needs to pay attention regarding the flush behavior. I used println because it adds the line feed automatically.
Responding to multiple messages of the same client
To accept multiple messages it’s necessary to keep reading the input. This can be done with another function and loop/recur:
(defn- handle-input [{:keys [in out handler]}] (loop [] (when-let [line (.readLine in)] (println "Received:" line) (.println out (handler line)) (recur))))
(defn create-tcp-server [{:keys [port handler]}] (with-open [socket (ServerSocket. port) client-socket (.accept socket) in (BufferedReader. (InputStreamReader. (.getInputStream client-socket))) out (PrintWriter. (.getOutputStream client-socket) true)] (handle-input {:in in :out out :handler handler})))Again, testing with nc:
nc -t 127.0.0.1 1230helloHELLOthereTHEREThis implementation only accepts one client. It also shutdown the server once that client has disconnected.
Multiple clients
To support multiple clients at the same time we can use threads.
(defn- client-handler [client-socket handler] (future (with-open [in (BufferedReader. (InputStreamReader. (.getInputStream client-socket))) out (PrintWriter. (.getOutputStream client-socket) true)] (handle-input {:in in :out out :handler handler}))))
(defn create-tcp-server [{:keys [port handler]}] (with-open [socket (ServerSocket. port)] (while true (client-handler (.accept socket) handler))))This accepts multiple clients but there’s not way to shutdown the server.
Ability to start and stop the server
(ns user (:require [clojure.string :as str]) (:import (java.net ServerSocket) (java.io BufferedReader InputStreamReader PrintWriter)))
(defn- handle-input [{:keys [in out handler]}] (loop [] (when-let [line (.readLine in)] (println "Received:" line) (.println out (handler line)) (recur))))
(defn- client-handler [client-socket handler] (future (try (with-open [in (BufferedReader. (InputStreamReader. (.getInputStream client-socket))) out (PrintWriter. (.getOutputStream client-socket) true)] (handle-input {:in in :out out :handler handler})) (catch Exception e (println "Client error:" (.getMessage e))) (finally (when-not (.isClosed client-socket) (.close client-socket) (println "Client disconnected by error"))))))
(defn create-tcp-server [{:keys [port handler]}] (let [state (atom {:server-socket nil :clients []}) start-server (fn [] (when (nil? (:server-socket @state)) (reset! state {:server-socket (ServerSocket. port) :clients []}) (println "Server started on port" port) (future (try (loop [] (let [client-socket (.accept (:server-socket @state))] (swap! state update :clients conj client-socket) (client-handler client-socket handler) (recur))) (catch Exception e (println "Server error:" (.getMessage e))) (finally (when-not (.isClosed (:server-socket @state)) (.close (:server-socket @state)) (println "Server stopped by error"))))))) stop-server (fn [] (when-let [server-socket (:server-socket @state)] (doseq [client-socket (:clients @state)] (.close client-socket) (println "Client disconnected by server")) (.close server-socket) (println "Server stopped by user") (reset! state {:server-socket nil :clients []})))] {:start start-server :stop stop-server}))
(let [{:keys [start stop]} (create-tcp-server {:port 1230 :handler str/upper-case})] (start) (Thread/sleep 10000) (stop))This is a little more robust and supports multiple clients with multiple messages.
The logs of the server look like this:
Server started on port 1230Received: heyReceived: another clientClient disconnected by serverClient error: Socket closedClient disconnected by serverClient error: Socket closedServer stopped by userServer error: Socket closedI’m not sure if there is a better way to handle closing the connections without try/catch.