[250108] build your own redis in clojure

Table of Contents

링크 : https://blog.pjam.me/posts/toy-redis-clojure/

재미있어 보여서 따라해보았다.

1 소스코드

(ns redis-server-atom
  (:require [clojure.java.io :as io]
	    [clojure.core.async :as a]
	    [clojure.string :as string])
  (:import (java.net ServerSocket)))

(defn atoi
  "Attempt to convert a string to integer, returns nil if it can't be parsed"
  [string]
  (try
    (Integer/valueOf string)
    (catch NumberFormatException _e
      nil)))

(def valid-commands
  "Valid commands"
  #{"GET" "SET" "DEL" "INCR"})

(defn key-request
  "Helper to structure the basic parts of a command"
  [command key]
  {:command command :key key})

(defn key-value-request
  "Helper to structure the various parts of a SET command"
  [command key value]
  (assoc (key-request command key) :value value))

(defn request-for-command
  "Return a structured representation of a client command"
  [command parts]
  (cond
    (= command "GET")
    (key-request :get (get parts 1))
    (= command "SET")
    (key-value-request :set (get parts 1) (get parts 2))
    (= command "INCR")
    (key-request :incr (get parts 1))
    (= command "DEL")
    (key-request :del (get parts 1))))

(defn increment-number
  "Wrap the lower level operations required to process an increment command"
  [db key]
  (swap-vals! db (fn [current-state]
		   (if (contains? current-state key)
		     (let [number (atoi (get current-state key))]
		       (if number
			 (assoc current-state key (str (+ number 1)))
			 current-state))
		     (assoc current-state key "1")))))

(defn process-command
  "Perform various operations depending on the command sent by the client"
  [db request]
  (let [command (request :command)
	key (request :key)
	value (request :value)]
    (cond
      (= command :get)
      (if key
	(get @db key "")
	"ERR wrong number of arguments for 'get' command")
      (= command :set)
      (if (and key value)
	(do
	  (swap! db (fn [current-state]
		      (assoc current-state key value)))
	  "OK")
	"ERR wrong number of arguments for 'set' command")
      (= command :del)
      (if key
	(let [[old-value _] (swap-vals! db (fn [current-state]
					     (dissoc current-state key)))]
	  (if (contains? old-value key) "1" "0"))
	"ERR wrong number of arguments for 'del' command")
      (= command :incr)
      (if key
	(let [[_ new-value] (increment-number db key)
	      number (atoi (get new-value key))]
	  (if number
	    number
	    "ERR value is not an integer or out of range"))
	"ERR wrong number of arguments for 'incr' command")
      :else "Unknown command")))


(defn handle-client
  "Read from a connected client, and handles the various commands accepted by the server"
  [client-socket db]
  (a/go-loop [] ;; (1)
   (let [request (.readLine (io/reader client-socket))
	 writer (io/writer client-socket)]
     (if (nil? request)
       (do
	 (println "Nil request, closing")
	 (.close client-socket))
       (let [parts (string/split request #" ")
	     command (get parts 0)]
	 (cond
	   (contains? valid-commands command)
	   (let [request (request-for-command command parts) ;; (2)
		 value (process-command db request)] ;; (3)
	     (.write writer (str value "\n"))
	     (.flush writer)
	     (recur))
	   (= command "QUIT")
	   (.close client-socket)
	   :else (do
		   (println "Unknown request:" request)
		   (recur))))))))

(defn main
  "Start a server and continuously wait for new clients to connect"
  [& _args]
  (println "About to start ...")
  (let [db (atom {})]
    (with-open [server-socket (ServerSocket. 3000)]
      (loop []
	(let [client-socket (.accept server-socket)]
	  (handle-client client-socket db))
	(recur)))))


(main)

;; clj -Sdeps '{:deps {org.clojure/core.async {:mvn/version "1.7.701"}}}' -M redis_server_atom.clj
;; nc localhost 3000

Author: Younghwan Nam

Created: 2025-02-20 Thu 14:08

Emacs 27.2 (Org mode 9.4.4)

Validate