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.

Terminal window
nc -t 127.0.0.1 1230
hello
HELLO

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.

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 made
  • bind(SocketAddress endpoint): binds the socket to an specific IP
  • close(): 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:

Terminal window
nc -t 127.0.0.1 1230
hello
HELLO
there
THERE

This 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 1230
Received: hey
Received: another client
Client disconnected by server
Client error: Socket closed
Client disconnected by server
Client error: Socket closed
Server stopped by user
Server error: Socket closed

I’m not sure if there is a better way to handle closing the connections without try/catch.

Back to notes