Skip to content

Commit 130047f

Browse files
committed
all
0 parents  commit 130047f

11 files changed

+437
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore bin folder
2+
bin/

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Redis-Compatible Cache Server (Reverse Engineered)
2+
3+
This project is a lightweight, reverse-engineered implementation of a Redis-compatible server. It supports basic caching operations such as `SET` and `GET`, and is designed to be compact and efficient. The server is compatible with the official Redis client, making it easy to integrate into existing systems that rely on Redis for caching.
4+
5+
## Features
6+
7+
- **Redis Protocol Compatibility**: The server implements a subset of the Redis protocol (RESP - REdis Serialization Protocol), allowing it to work seamlessly with the official Redis client.
8+
- **Basic Caching Operations**:
9+
- `SET key value`: Stores a key-value pair in the cache.
10+
- `GET key`: Retrieves the value associated with a key.
11+
- **Lightweight and Efficient**: The server is designed to be minimalistic, focusing only on the essential caching functionality.
12+
- **Concurrency Support**: The server uses a thread-safe key-value store, ensuring safe concurrent access to the cache.
13+
14+
## How It Works
15+
16+
The server listens for incoming connections on a specified port (default: `:2345`). When a client connects, it processes commands sent in the Redis protocol format. The server supports the following commands:
17+
18+
1. **SET**: Stores a key-value pair in the cache.
19+
- Example: `SET mykey myvalue`
20+
2. **GET**: Retrieves the value associated with a key.
21+
- Example: `GET mykey`
22+
3. **HELLO**: A custom command that responds with server information.
23+
- Example: `HELLO world`
24+
4. **CLIENT**: A placeholder command for future client-related functionality.
25+
26+
The server uses a simple in-memory key-value store (`KeyVal`) to manage the cache. It ensures thread safety using a read-write mutex (`sync.RWMutex`).
27+
28+
## Compatibility with Redis Client
29+
30+
This server is designed to be compatible with the official Redis client. You can use any Redis client library or the `redis-cli` tool to interact with this server. For example:
31+
32+
```bash
33+
# Using redis-cli
34+
$ redis-cli -p 2345
35+
127.0.0.1:2345> SET mykey myvalue
36+
OK
37+
127.0.0.1:2345> GET mykey
38+
"myvalue"

Taskfile.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
version: '3'
2+
3+
tasks:
4+
build:
5+
cmds:
6+
- go build -o bin/
7+
8+
run:
9+
deps: [build]
10+
cmds:
11+
- ./bin/lightweightCacheServer.exe
12+
13+
test:
14+
cmds:
15+
- go test -v ./...

go.mod

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module github.com/shatwik7/lightweightCacheServer
2+
3+
go 1.23.5
4+
5+
require github.com/gofiber/fiber/v2 v2.52.6
6+
7+
require (
8+
github.com/cespare/xxhash/v2 v2.2.0 // indirect
9+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
10+
github.com/redis/go-redis/v9 v9.7.0 // indirect
11+
github.com/tidwall/resp v0.1.1 // indirect
12+
github.com/valyala/bytebufferpool v1.0.0 // indirect
13+
)

go.sum

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
2+
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
3+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
4+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
5+
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
6+
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
7+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
9+
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
10+
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
11+
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
12+
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
13+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
14+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=

keyVal.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
import "sync"
4+
5+
type KeyVal struct {
6+
mu sync.RWMutex
7+
data map[string][]byte
8+
}
9+
10+
func NewKeyVal() *KeyVal {
11+
return &KeyVal{
12+
data: map[string][]byte{},
13+
}
14+
}
15+
16+
func (KeyVal *KeyVal) Set(key, val []byte) error {
17+
KeyVal.mu.Lock()
18+
defer KeyVal.mu.Unlock()
19+
KeyVal.data[string(key)] = []byte(val)
20+
return nil
21+
}
22+
23+
func (KeyVal *KeyVal) Get(key []byte) ([]byte, bool) {
24+
KeyVal.mu.RLock()
25+
defer KeyVal.mu.RUnlock()
26+
val, ok := KeyVal.data[string(key)]
27+
return val, ok
28+
}

keyVal_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestKeyVal(t *testing.T) {
8+
KeyVal := NewKeyVal()
9+
key := "mykey"
10+
val := "myval"
11+
err := KeyVal.Set([]byte(key), []byte(val))
12+
if err != nil {
13+
t.Fatal(err)
14+
}
15+
val2, ok := KeyVal.Get([]byte(key))
16+
if !ok {
17+
t.Fatal(err)
18+
}
19+
if string(val2) != val {
20+
t.Fatal(err)
21+
}
22+
}

main.go

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"net"
7+
8+
"github.com/gofiber/fiber/v2/log"
9+
"github.com/tidwall/resp"
10+
)
11+
12+
const defaultListenAddress = ":2345"
13+
14+
type Message struct {
15+
cmd Command
16+
peer *Peer
17+
}
18+
19+
type Config struct {
20+
ListenAddress string
21+
}
22+
23+
type Server struct {
24+
Config
25+
peers map[*Peer]bool
26+
ln net.Listener
27+
addPeerCh chan *Peer
28+
quitCh chan struct{}
29+
msgCh chan Message
30+
delPeerCh chan *Peer
31+
KeyVal *KeyVal
32+
}
33+
34+
func NewServer(cfg Config) *Server {
35+
if len(cfg.ListenAddress) == 0 {
36+
cfg.ListenAddress = defaultListenAddress
37+
}
38+
return &Server{
39+
Config: cfg,
40+
peers: make(map[*Peer]bool),
41+
addPeerCh: make(chan *Peer),
42+
quitCh: make(chan struct{}),
43+
msgCh: make(chan Message),
44+
delPeerCh: make(chan *Peer),
45+
KeyVal: NewKeyVal(),
46+
}
47+
}
48+
49+
func (s *Server) Start() error {
50+
ln, err := net.Listen("tcp", s.ListenAddress)
51+
if err != nil {
52+
return err
53+
}
54+
s.ln = ln
55+
go s.loop()
56+
log.Infof("Running on PORT : ", s.Config.ListenAddress)
57+
return s.acceptLoop()
58+
}
59+
func (s *Server) loop() {
60+
for {
61+
select {
62+
case msg := <-s.msgCh:
63+
if err := s.handleMessage(msg); err != nil {
64+
slog.Error("raw message eror", "err", err)
65+
}
66+
case <-s.quitCh:
67+
return
68+
case peer := <-s.addPeerCh:
69+
slog.Info("peer connected", "remoteAddr", peer.conn.RemoteAddr())
70+
s.peers[peer] = true
71+
case peer := <-s.delPeerCh:
72+
slog.Info("peer disconnected", "remoteAddr", peer.conn.RemoteAddr())
73+
delete(s.peers, peer)
74+
}
75+
}
76+
}
77+
78+
func (s *Server) acceptLoop() error {
79+
for {
80+
conn, err := s.ln.Accept()
81+
if err != nil {
82+
log.Error("Accept Error : Can not accept :", err)
83+
continue
84+
}
85+
go s.handleConn(conn)
86+
}
87+
}
88+
89+
func (s *Server) handleMessage(msg Message) error {
90+
switch v := msg.cmd.(type) {
91+
case ClientCommand:
92+
if err := resp.
93+
NewWriter(msg.peer.conn).
94+
WriteString("OK"); err != nil {
95+
return err
96+
}
97+
case SetCommand:
98+
if err := s.KeyVal.Set(v.key, v.val); err != nil {
99+
return err
100+
}
101+
if err := resp.
102+
NewWriter(msg.peer.conn).
103+
WriteString("OK"); err != nil {
104+
return err
105+
}
106+
case GetCommand:
107+
val, ok := s.KeyVal.Get(v.key)
108+
if !ok {
109+
return fmt.Errorf("key not found")
110+
}
111+
if err := resp.
112+
NewWriter(msg.peer.conn).
113+
WriteString(string(val)); err != nil {
114+
return err
115+
}
116+
case HelloCommand:
117+
spec := map[string]string{
118+
"server": "redis",
119+
}
120+
_, err := msg.peer.Send(respWriteMap(spec))
121+
if err != nil {
122+
return fmt.Errorf("peer send error: %s", err)
123+
}
124+
}
125+
return nil
126+
}
127+
128+
func (s *Server) handleConn(conn net.Conn) {
129+
peer := NewPeer(conn, s.msgCh, s.delPeerCh)
130+
s.addPeerCh <- peer
131+
slog.Info("new peer connected", "remote Addr :", conn.RemoteAddr().String())
132+
if err := peer.readLoop(); err != nil {
133+
log.Error("Peer read error", err)
134+
}
135+
}
136+
137+
func main() {
138+
server := NewServer(Config{})
139+
log.Fatal(server.Start().Error())
140+
fmt.Println("hi!")
141+
}

main_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"testing"
8+
"time"
9+
10+
"github.com/redis/go-redis/v9"
11+
)
12+
13+
func TestOfficialRedisClient(t *testing.T) {
14+
listenAddr := ":5001"
15+
server := NewServer(Config{
16+
ListenAddress: listenAddr,
17+
})
18+
go func() {
19+
log.Fatal(server.Start())
20+
}()
21+
time.Sleep(time.Millisecond * 400)
22+
23+
rdb := redis.NewClient(&redis.Options{
24+
Addr: fmt.Sprintf("localhost%s", ":5001"),
25+
Password: "",
26+
DB: 0,
27+
})
28+
29+
testCases := map[string]string{
30+
"hello": "world",
31+
"creater": "god",
32+
"god": "me",
33+
"me": "shatwik",
34+
}
35+
for key, val := range testCases {
36+
if err := rdb.Set(context.Background(), key, val, 0).Err(); err != nil {
37+
t.Fatal(err)
38+
}
39+
newVal, err := rdb.Get(context.Background(), key).Result()
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
if newVal != val {
44+
t.Fatalf("expected %s but got %s", val, newVal)
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)