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.
(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 1230helloHELLO
The 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.
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 socketIn the naive approach the constructor already binds to the port. with-open
also takes care of closing the connection once the code has executed.
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.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.
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 1230helloHELLOthereTHERE
This implementation only accepts one client. It also shutdown the server once that client has disconnected.
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.
(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 closed
I’m not sure if there is a better way to handle closing the connections without try/catch
.