Redish is a Redis-compatible server
1.5K
A .NET 8.0 library for building Redis-compatible servers using the RESP (Redis Serialization Protocol). Provides low-level protocol parsing and event-driven architecture for implementing custom Redis servers.
The motivation behind building this library is to 1) expose an interface for C# developers to build their own Redis-compatible backend services and 2) have a platform on which to rapidly prototype data path improvements, enhancements, integrations, or anything else they can dream without requiring a thorough understand of Redis and C. The intent of the library is to inspire exploration, and in no uncertain terms, NOT to try and rebuild the wheel or otherwise create an alternative to a beloved, mature platform like Redis.
RespListener and RespInterfaceThis library and the Redish.Server project specifically have been tested with the redis-cliโ , StackExchange.Redisโ , and Redis Insightโ . Basic functionality and interoperability have been tested. Please file an issue if you encounter any incompatibilities.
The fastest way to get a Redis-compatible server running:
# Clone and build
git clone https://github.com/jchristn/RedisRespServer.git
cd RedisRespServer/src
dotnet build
# Run the server
cd Redish.Server
dotnet run
Server starts on port 6379 and supports Redis commands like SET, GET, DEL, MSET, MGET, INCR, INCRBY, DECR, HMSET, and more.
cd Sample.RedisInterface
dotnet run
This starts a simple Redis server on port 6379 with basic command support.
The library has three main components:
RedisValue base class with expiration supportusing RedisResp;
var listener = new RespListener(6379);
// Handle array commands (like SET key value)
listener.ArrayReceived += (sender, e) => {
if (e.Value is object[] args && args.Length >= 2)
{
string command = args[0].ToString().ToUpper();
Console.WriteLine($"Received command: {command}");
// Implement your command handling here
}
};
// Handle client connections
listener.ClientConnected += (sender, e) =>
Console.WriteLine($"Client connected: {e.GUID}");
await listener.StartAsync();
Console.WriteLine("Server running on port 6379");
Console.ReadKey();
using RedisResp;
var listener = new RespListener(6379);
var respInterface = new RespInterface(listener);
// Traditional event-driven approach
respInterface.ArrayReceived += HandleRedisCommand;
// Or functional approach
respInterface.ArrayHandler = (e) => {
// Handle command and return response string
return "+OK\r\n";
};
await respInterface.StartAsync();
Based on the Sample.RedisInterface project, here's how to handle basic commands:
private static readonly ConcurrentDictionary<string, RedisValue> _storage = new();
private async void HandleRedisCommand(object sender, RespDataReceivedEventArgs e)
{
if (e.Value is not object[] args || args.Length == 0) return;
string command = args[0].ToString().ToUpper();
string response = command switch
{
"GET" when args.Length >= 2 => HandleGet(args[1].ToString()),
"SET" when args.Length >= 3 => HandleSet(args[1].ToString(), args[2].ToString()),
"DEL" when args.Length >= 2 => HandleDel(args[1].ToString()),
"EXISTS" when args.Length >= 2 => HandleExists(args[1].ToString()),
"KEYS" when args.Length >= 2 => HandleKeys(args[1].ToString()),
"MSET" when args.Length >= 3 => HandleMset(args),
"MGET" when args.Length >= 2 => HandleMget(args),
"INCR" when args.Length >= 2 => HandleIncr(args[1].ToString()),
"INCRBY" when args.Length >= 3 => HandleIncrBy(args[1].ToString(), args[2].ToString()),
"DECR" when args.Length >= 2 => HandleDecr(args[1].ToString()),
"HMSET" when args.Length >= 4 => HandleHmset(args),
"PING" => "+PONG\r\n",
"FLUSHDB" => HandleFlushDB(),
_ => "-ERR unknown command\r\n"
};
// Send response back to client
await SendResponseAsync(e.ClientGUID, response);
}
private string HandleGet(string key)
{
if (_storage.TryGetValue(key, out var value) && !value.IsExpired && value is StringValue stringVal)
{
return $"${stringVal.Value.Length}\r\n{stringVal.Value}\r\n";
}
return "$-1\r\n"; // null
}
private string HandleSet(string key, string value)
{
_storage[key] = new StringValue(value);
return "+OK\r\n";
}
The Sample.RedisInterface shows how to send responses by writing directly to the client's NetworkStream:
private async Task SendResponseAsync(Guid clientGuid, string response)
{
if (_clientConnections.TryGetValue(clientGuid, out var client) && client.Connected)
{
var stream = client.GetStream();
var data = Encoding.UTF8.GetBytes(response);
await stream.WriteAsync(data, 0, data.Length);
}
}
The library includes a StorageBase abstract class in Redish.Server for implementing custom storage:
using Redish.Server.Storage;
public class CustomStorageDriver : StorageBase
{
public override RedisValue this[string key]
{
get => GetFromYourStorage(key);
set => SaveToYourStorage(key, value);
}
public override bool TryAdd(string key, RedisValue value)
{
// Implement atomic add operation
}
public override bool Remove(string key)
{
// Implement key removal
}
// Implement other abstract methods...
}
The RespListener can parse and raise events for:
+OK\r\n) - SimpleStringReceived event-ERR message\r\n) - ErrorReceived event:100\r\n) - IntegerReceived event$5\r\nhello\r\n) - BulkStringReceived event*2\r\n$3\r\nSET\r\n$3\r\nkey\r\n) - ArrayReceived event$-1\r\n) - NullReceived event,1.23\r\n) - DoubleReceived event#t\r\n, #f\r\n) - BooleanReceived event(123456789\r\n) - BigNumberReceived event!5\r\nerror\r\n) - BlobErrorReceived event=15\r\ntxt:hello world\r\n) - VerbatimStringReceived event%1\r\n+key\r\n+value\r\n) - MapReceived event~2\r\n+a\r\n+b\r\n) - SetReceived event|1\r\n+attr\r\n+val\r\n) - AttributeReceived event>2\r\n+pubsub\r\n+message\r\n) - PushReceived eventThe library defines these value types in RedisValueType enum:
public enum RedisValueType
{
String, // Basic string values
Hash, // Hash maps
List, // Ordered lists
Set, // Unordered sets
SortedSet, // Scored sorted sets
Json, // JSON documents
Stream // Redis streams
}
Each RedisValue has automatic expiration support with ExpiresAt, IsExpired, and SetExpiration() methods.
The repository includes several working examples:
RespInterfaceTest with redis-cli:
redis-cli -h localhost -p 6379
127.0.0.1:6379> SET mykey "Hello"
OK
127.0.0.1:6379> GET mykey
"Hello"
127.0.0.1:6379> MSET key1 "value1" key2 "value2"
OK
127.0.0.1:6379> MGET key1 key2
1) "value1"
2) "value2"
127.0.0.1:6379> SET counter 10
OK
127.0.0.1:6379> INCR counter
(integer) 11
127.0.0.1:6379> INCRBY counter 5
(integer) 16
127.0.0.1:6379> DECR counter
(integer) 15
127.0.0.1:6379> HMSET user name "John" age "30"
OK
127.0.0.1:6379> PING
PONG
Or run the test projects:
cd Test.StackExchangeRedis
dotnet run
src/
โโโ RedisRespServer/ # Core library (RespListener, RespInterface)
โโโ Redish.Server/ # Full Redis server implementation
โโโ Sample.RedisInterface/ # Basic example server
โโโ Sample.RedisServer/ # Alternative example
โโโ Test.StackExchangeRedis/ # Compatibility tests
โโโ Test/ # Additional test utilities
This project is licensed under the MIT License - see the LICENSE.mdโ file for details.
Build your own Redis-compatible server in minutes! โก
Content type
Image
Digest
sha256:98eaa50d8โฆ
Size
339.9 MB
Last updated
7 days ago
docker pull jchristn77/redish