Teach You to Write a Simple Redis Client Framework - .NET Core

2020年11月10日 50点热度 1人点赞 0条评论
内容目录

Recently, CEO Ye wrote a FreeRedis, and I happened to be learning Redis, so I decided to try and write a simple RedisClient as well. Currently, FreeRedis is in its early stages and requires more participation and testing from users. If anyone is interested, please join in and contribute.

FreeRedis project address: https://github.com/2881099/FreeRedis

Following the NCC open-source spirit of openness, sharing, innovation, and contribution! Everyone can participate in community development and contribute their efforts!

From the looks of it, FreeRedis is expected to become powerful and perform astonishingly, faster than bullets and mightier than engines, just like Freesql!

The source code for this tutorial can be found on GitHub: https://github.com/whuanle/RedisClientLearn

Since the code is simple, not many features are considered; it does not support password login, clustering, or concurrency.

First, install Redis on your own computer. The Windows version download address is: https://github.com/MicrosoftArchive/redis/releases

Then download the Windows version of the Redis management tool.

The Windows version of Redis Desktop Manager 64-bit 2019.1 (Chinese version) download address: https://www.7down.com/soft/233274.html

Official latest version download address: https://redisdesktop.com/download

0. About Redis RESP

RESP stands for REdis Serialization Protocol, which is the serialization protocol used by Redis to define the data transmission rules when the client connects to Redis via socket.

Official protocol description: https://redis.io/topics/protocol

The request-response method of the RESP protocol when communicating with Redis is as follows:

  • The client sends commands as RESP bulk string arrays (i.e., C# uses byte[] to store string commands) to the Redis server.
  • The server replies based on the command using RESP types.

The types in RESP do not refer to Redis's basic data types, but to the format of the data response:

In RESP, the type of certain data is determined by the first byte:

  • For simple strings, the first byte of the reply is “+”
  • For errors, the first byte of the reply is “-”
  • For integers, the first byte of the reply is “:”
  • For bulk strings, the first byte of the reply is “$”
  • For arrays, the first byte of the reply is “*”

For these, beginners may not be very clear, so let’s do a practical operation.

We open Redis Desktop Manager, then click on the console, and input:

set a 12
set b 12
set c 12
MGET abc

Press the Enter key after each line. MGET is the command in Redis to retrieve multiple keys' values at once.

The output result is as follows:

Local:0>SET a 12
"OK"
Local:0>SET b 12
"OK"
Local:0>SET c 12
"OK"
Local:0>MGET a b c
 1)  "12"
 2)  "12"
 3)  "12"

However, this management tool removes the protocol identifiers from RESP. Let's write some demo code to restore the essence of RESP.

using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            IPAddress IP = IPAddress.Parse("127.0.0.1");
            IPEndPoint IPEndPoint = new IPEndPoint(IP, 6379);
            Socket client = new Socket(IP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            await client.ConnectAsync(IPEndPoint);

            if (!client.Connected)
            {
                Console.WriteLine("Failed to connect to Redis server!");
                Console.Read();
            }

            Console.WriteLine("Congratulations, successfully connected to Redis server");

            // Background message reception
            new Thread(() =>
            {
                while (true)
                {
                    byte[] data = new byte[100];
                    int size = client.Receive(data);
                    Console.WriteLine();
                    Console.WriteLine(Encoding.UTF8.GetString(data));
                    Console.WriteLine();
                }
            }).Start();

            while (true)
            {
                Console.Write("$> ");
                string command = Console.ReadLine();
                // The command sent must end with \r\n
                int size = client.Send(Encoding.UTF8.GetBytes(command + "\r\n"));
                Thread.Sleep(100);
            }
        }
    }
}

Input and output result:

$> SET a 123456789
+OK
$> SET b 123456789
+OK
$> SET c 123456789
+OK
$> MGET a b c

*3
$9
123456789
$9
123456789
$9
123456789

As can be seen, the message content returned by Redis begins with characters like $, *, +, etc., and is separated by \r\n.

The way we write the Redis Client is to receive socket content and then parse the actual data from it.

Each time a set command is successfully sent, it returns +OK; *3 indicates there are three arrays; $9 indicates the length of the received data is 9.

That's roughly how it works. Now let's write a simple Redis Client framework and then go to sleep.

Remember to use netstandard2.1 because some conversions between byte[], string, and ReadOnlySpan<T> are easier with netstandard2.1.

1. Define Data Types

Based on the previous demo, let's define a type to store those special symbols:

    /// &lt;summary&gt;
    /// RESP Response Types
    /// &lt;/summary&gt;
    public static class RedisValueType
    {
        public const byte Errors = (byte)&#039;-&#039;;
        public const byte SimpleStrings = (byte)&#039;+&#039;;
        public const byte Integers = (byte)&#039;:&#039;;
        public const byte BulkStrings = (byte)&#039;$&#039;;
        public const byte Arrays = (byte)&#039;*&#039;;


        public const byte R = (byte)&#039;\r&#039;;
        public const byte N = (byte)&#039;\n&#039;;
    }

2. Define Asynchronous Message State Machine

Create a MessageStrace class that serves as an asynchronous state machine for message responses and also has the functionality to parse data streams.

    /// &lt;summary&gt;
    /// Custom Message Queue State Machine
    /// &lt;/summary&gt;
    public abstract class MessageStrace
    {
        protected MessageStrace()
        {
            TaskCompletionSource = new TaskCompletionSource&lt;string&gt;();
            Task = TaskCompletionSource.Task;
        }

        protected readonly TaskCompletionSource&lt;string&gt; TaskCompletionSource;

        /// &lt;summary&gt;
        /// Indicates whether the task is complete and receives Redis response string data stream
        /// &lt;/summary&gt;
        public Task&lt;string&gt; Task { get; private set; }

        /// &lt;summary&gt;
        /// Receive data stream
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;stream&quot;&gt;&lt;/param&gt;
        /// &lt;param name=&quot;length&quot;&gt;actual length&lt;/param&gt;
        public abstract void Receive(MemoryStream stream, int length);

        /// &lt;summary&gt;
        /// Response is completed
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;data&quot;&gt;&lt;/param&gt;
        protected void SetValue(string data)
        {
            TaskCompletionSource.SetResult(data);
        }

        /// &lt;summary&gt;
        /// Parse the number following the $ or * symbol, must pass the index of the character immediately after the symbol
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;data&quot;&gt;&lt;/param&gt;
        /// &lt;param name=&quot;index&quot;&gt;Position to parse to&lt;/param&gt;
        /// &lt;returns&gt;&lt;/returns&gt;
        protected int BulkStrings(ReadOnlySpan&lt;byte&gt; data, ref int index)
        {
            int start = index;
            int end = start;

            while (true)
            {
                if (index + 1 &gt;= data.Length)
                    throw new ArgumentOutOfRangeException(&quot;Overflow&quot;);

                // \r\n
                if (data[index].CompareTo(RedisValueType.R) == 0 &amp;&amp; data[index + 1].CompareTo(RedisValueType.N) == 0)
                {
                    index += 2;     // Point to the next character after \n
                    break;
                }
                end++;
                index++;
            }

            // Slice the number after $2 and *3 symbols
            return Convert.ToInt32(Encoding.UTF8.GetString(data.Slice(start, end - start).ToArray()));
        }
    }

3. Define Command Sending Template

Since Redis commands are numerous, to better encapsulate them, we define a message sending template that specifies five types using five types for sending from the Client.

Define a unified template class:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace CZGL.RedisClient
{
    /// &lt;summary&gt;
    /// Command Sending Template
    /// &lt;/summary&gt;
    public abstract class CommandClient&lt;T&gt; where T : CommandClient&lt;T&gt;
    {
        protected RedisClient _client;
        protected CommandClient()
        {
        }
        protected CommandClient(RedisClient client)
        {
            _client = client;
        }

        /// &lt;summary&gt;
        /// Reuse
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;client&quot;&gt;&lt;/param&gt;
        /// &lt;returns&gt;&lt;/returns&gt;
        internal virtual CommandClient&lt;T&gt; Init(RedisClient client)
        {
            _client = client;
            return this;
        }

        /// &lt;summary&gt;
        /// Check if the request is successful
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;value&quot;&gt;Response message&lt;/param&gt;
        /// &lt;returns&gt;&lt;/returns&gt;
        protected bool IsOk(string value)
        {
            if (value[0].CompareTo(&#039;+&#039;) != 0 || value[1].CompareTo(&#039;O&#039;) != 0 || value[2].CompareTo(&#039;K&#039;) != 0)
                return false;
            return true;
        }

        /// &lt;summary&gt;
        /// Send command
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;command&quot;&gt;The command to send&lt;/param&gt;
        /// &lt;param name=&quot;strace&quot;&gt;Data type client&lt;/param&gt;
        /// &lt;returns&gt;&lt;/returns&gt;
        protected Task SendCommand&lt;TStrace&gt;(string command, out TStrace strace) where TStrace : MessageStrace, new()
        {
            strace = new TStrace();
            return _client.SendAsync(strace, command);
        }
    }
}

4. Define Redis Client

The RedisClient class is used to send Redis commands, place tasks in a queue, receive the data from Redis, and write the data stream to memory, dequeue, and set the return value of the asynchronous task.

The sending process can be concurrent, but the reception of message content uses a single thread. To ensure message order, a queue is used to record the sequence of Send - Receive.

C# sockets can be quite finicky; achieving concurrent and high-performance sockets is not that easy.

The following code has three places commented out, and later we will continue to write other parts of the code that will use them.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;


namespace CZGL.RedisClient
{
    /// <summary>
    /// Redis Client
    /// </summary>
    public class RedisClient
    {
        private readonly IPAddress IP;
        private readonly IPEndPoint IPEndPoint;
        private readonly Socket client;

        //private readonly Lazy<StringClient> stringClient;
        //private readonly Lazy<HashClient> hashClient;
        //private readonly Lazy<ListClient> listClient;
        //private readonly Lazy<SetClient> setClient;
        //private readonly Lazy<SortedClient> sortedClient;

        // Data stream request queue
        private readonly ConcurrentQueue<MessageStrace> StringTaskQueue = new ConcurrentQueue<MessageStrace>();

        public RedisClient(string ip, int port)
        {
            IP = IPAddress.Parse(ip);
            IPEndPoint = new IPEndPoint(IP, port);

            //stringClient = new Lazy<StringClient>(() => new StringClient(this));
            //hashClient = new Lazy<HashClient>(() => new HashClient(this));
            //listClient = new Lazy<ListClient>(() => new ListClient(this));
            //setClient = new Lazy<SetClient>(() => new SetClient(this));
            //sortedClient = new Lazy<SortedClient>(() => new SortedClient(this));

            client = new Socket(IP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        }

        /// <summary>
        /// Start connecting to Redis
        /// </summary>
        public async Task<bool> ConnectAsync()
        {
            await client.ConnectAsync(IPEndPoint);
            new Thread(() => { ReceiveQueue(); })
            {
                IsBackground = true
            }.Start();
            return client.Connected;
        }

        /// <summary>
        /// Send a command and add it to the queue
        /// </summary>
        /// <param name="task"></param>
        /// <param name="command"></param>
        /// <returns></returns>
        internal Task<int> SendAsync(MessageStrace task, string command)
        {
            var buffer = Encoding.UTF8.GetBytes(command + "\r\n");
            var result = client.SendAsync(new ArraySegment<byte>(buffer, 0, buffer.Length), SocketFlags.None);
            StringTaskQueue.Enqueue(task);
            return result;
        }


        /*         
        Microsoft tests response times with different sized buffer inputs.

        1024 - real 0m0.102s; user  0m0.018s; sys   0m0.009s
        2048 - real 0m0.112s; user  0m0.017s; sys   0m0.009s
        8192 - real 0m0.163s; user  0m0.017s; sys   0m0.007s
         256 - real 0m0.101s; user  0m0.019s; sys   0m0.008s
          16 - real 0m0.144s; user  0m0.016s; sys   0m0.010s

        The default size of the .NET Socket buffer is 8192 bytes.
        Socket.ReceiveBufferSize: An Int32 that contains the size, in bytes, of the receive buffer. The default is 8192.
        
        Many responses are simply "+OK\r\n", and MemoryStream defaults to a size of 256 (though it can be customized).
        A buffer that's too large wastes memory; exceeding 256 means MemoryStream will allocate new buffers of 256 size, impacting performance.
        Setting BufferSize to 256 is a reasonable practice.
         */

        private const int BufferSize = 256;

        /// <summary>
        /// Single-threaded sequentially receive data streams and complete tasks from the queue
        /// </summary>
        private void ReceiveQueue()
        {
            while (true)
            {
                MemoryStream stream = new MemoryStream(BufferSize);  // Memory buffer

                byte[] data = new byte[BufferSize];        // Chunks, receiving N bytes each time

                int size = client.Receive(data);           // Wait to receive a message
                int length = size;                         // Total length of the data stream

                while (true)
                {
                    stream.Write(data, 0, size);            // Write received chunk data into memory buffer

                    // Data stream received completely
                    if (size < BufferSize)      // There is a bug where if the size of the data stream or the last chunk is exactly the size of BufferSize, it cannot escape the Receive
                    {
                        break;
                    }

                    length += client.Receive(data);       // Not yet received completely, continue receiving
                }

                stream.Seek(0, SeekOrigin.Begin);         // Reset cursor position

                // Retrieve task from the queue
                StringTaskQueue.TryDequeue(out var tmpResult);

                // Process tasks in the queue
                tmpResult.Receive(stream, length);
            }
        }

        /// <summary>
        /// Reuse
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="client"></param>
        /// <returns></returns>
        public T GetClient<T>(T client) where T : CommandClient<T>
        {
            client.Init(this);
            return client;
        }

        ///// <summary>
        ///// Get string request client
        ///// </summary>
        ///// <returns></returns>
        //public StringClient GetStringClient()
        //{
        //    return stringClient.Value;
        //}

        //public HashClient GetHashClient()
        //{
        //    return hashClient.Value;
        //}

        //public ListClient GetListClient()
        //{
        //    return listClient.Value;
        //}

        //public SetClient GetSetClient()
        //{
        //    return setClient.Value;
        //}

        //public SortedClient GetSortedClient()
        //{
        //    return sortedClient.Value;
        //}
    }
}

5. Implementing a Simple RESP Parser

The following code is used to implement parsing of Redis RESP messages. Due to time constraints, I've implemented parsing for the symbols +, -, $, and *. You can refer to this for completeness for others.

Create a MessageStraceAnalysis.cs file with the following code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace CZGL.RedisClient
{
    /// <summary>
    /// RESP data stream parser
    /// </summary>
    public class MessageStraceAnalysis<T> : MessageStrace
    {
        public MessageStraceAnalysis()
        {

        }

        /// <summary>
        /// Parse protocol
        /// </summary>
        /// <param name="data"></param>
        public override void Receive(MemoryStream stream, int length)
        {
            byte firstChar = (byte)stream.ReadByte(); // The first character; since the cursor has already moved to 1, subsequent .GetBuffer() will truncate from 1 onwards, discarding the first character.

            if (firstChar.CompareTo(RedisValueType.SimpleStrings) == 0)    // Simple strings
            {
                SetValue(Encoding.UTF8.GetString(stream.GetBuffer()));
                return;
            }

            else if (firstChar.CompareTo(RedisValueType.Errors) == 0)
            {
                TaskCompletionSource.SetException(new InvalidOperationException(Encoding.UTF8.GetString(stream.GetBuffer())));
                return;
            }

            // Not starting with + or -

            stream.Position = 0;
            int index = 0;
            ReadOnlySpan<byte> data = new ReadOnlySpan<byte>(stream.GetBuffer());

            string tmp = Analysis(data, ref index);
            SetValue(tmp);
        }

        // Enter recursive processing flow
        private string Analysis(ReadOnlySpan<byte> data, ref int index)
        {
            // *
            if (data[index].CompareTo(RedisValueType.Arrays) == 0)
            {
                string value = default;
                index++;
                int size = BulkStrings(data, ref index);

                if (size == 0)
                    return string.Empty;
                else if (size == -1)
                    return null;

                for (int i = 0; i < size; i++)
                {
                    var tmp = Analysis(data, ref index);
                    value += tmp + ((i < (size - 1)) ? "\r\n" : string.Empty);
                }
                return value;
            }

            // $..
            else if (data[index].CompareTo(RedisValueType.BulkStrings) == 0)
            {
                index++;
                int size = BulkStrings(data, ref index);

                if (size == 0)
                    return string.Empty;
                else if (size == -1)
                    return null;
                var value = Encoding.UTF8.GetString(data.Slice(index, size).ToArray());
                index += size + 2; // Move the pointer to after \n
                return value;
            }

            throw new ArgumentException("Parsing error");
        }
    }
}

6. Implementing Command Sending Clients

Since Redis has a multitude of commands, directly encapsulating all commands into RedisClient would undoubtedly lead to an overly complex API and make the code difficult to maintain. Therefore, we can split the design based on Redis types such as string, hash, set, etc., to create client interfaces.

以下是您提供内容的英文翻译:


Let's design a StringClient:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CZGL.RedisClient
{
    /// <summary>
    /// String type
    /// </summary>
    public class StringClient : CommandClient<StringClient>
    {
        internal StringClient()
        {

        }

        internal StringClient(RedisClient client) : base(client)
        {
        }

        /// <summary>
        /// Set key value
        /// </summary>
        /// <param name="key">key</param>
        /// <param name="value">value</param>
        /// <returns></returns>
        public async Task<bool> Set(string key, string value)
        {
            await SendCommand<MessageStraceAnalysis<string>>($"{StringCommand.SET} {key} {value}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return IsOk(result);
        }

        /// <summary>
        /// Get the value of a key
        /// </summary>
        /// <param name="key">key</param>
        /// <returns></returns>
        public async Task<string> Get(string key)
        {
            await SendCommand($"{StringCommand.GET} {key}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return result;
        }

        /// <summary>
        /// Retrieve specified length of data from the value of a specified key
        /// </summary>
        /// <param name="key">key</param>
        /// <param name="start">start index</param>
        /// <param name="end">end index</param>
        /// <returns></returns>
        public async Task<string> GetRance(string key, uint start, int end)
        {
            await SendCommand($"{StringCommand.GETRANGE} {key} {start} {end}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return result;
        }

        /// <summary>
        /// Set a value and return the old value
        /// </summary>
        /// <param name="key"></param>
        /// <param name="newValue"></param>
        /// <returns></returns>
        public async Task<string> GetSet(string key, string newValue)
        {
            await SendCommand($"{StringCommand.GETSET} {key} {newValue}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return result;
        }

        /// <summary>
        /// Get the value of a specific bit in binary data
        /// </summary>
        /// <param name="key"></param>
        /// <param name="index"></param>
        /// <returns>0 or 1</returns>
        public async Task<int> GetBit(string key, uint index)
        {
            await SendCommand($"{StringCommand.GETBIT} {key} {index}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return Convert.ToInt32(result);
        }

        /// <summary>
        /// Set a specific bit to 1 or 0
        /// </summary>
        /// <param name="key"></param>
        /// <param name="index"></param>
        /// <param name="value">0 or 1</param>
        /// <returns></returns>
        public async Task<bool> SetBit(string key, uint index, uint value)
        {
            await SendCommand($"{StringCommand.SETBIT} {key} {index} {value}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return IsOk(result);
        }


        /// <summary>
        /// Get the values of multiple keys
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public async Task<string[]> MGet(params string[] key)
        {
            await SendCommand($"{StringCommand.MGET} {string.Join(" ", key)}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return result.Split("\r\n");
        }



        private static class StringCommand
        {
            public const string SET = "SET";
            public const string GET = "GET";
            public const string GETRANGE = "GETRANGE";
            public const string GETSET = "GETSET";
            public const string GETBIT = "GETBIT";
            public const string SETBIT = "SETBIT";
            public const string MGET = "MGET";
            // ... ... More string commands
        }
    }
}

StringClient implements 7 Redis String type commands, and other commands are analogous.

Now, we'll open RedisClient.cs and uncomment the following portions of code:

private readonly Lazy<StringClient> stringClient; // 24 line

stringClient = new Lazy<StringClient>(() => new StringClient(this)); // 38 line

// 146 line
/// <summary>
/// Get string request client
/// </summary>
/// <returns></returns>
public StringClient GetStringClient()
{
    return stringClient.Value;
}

7. How to Use

Example of using RedisClient:

static async Task Main(string[] args)
{
    RedisClient client = new RedisClient("127.0.0.1", 6379);
    var a = await client.ConnectAsync();
    if (!a)
    {
        Console.WriteLine("Failed to connect to server");
        Console.ReadKey();
        return;
    }

    Console.WriteLine("Successfully connected to server");

    var stringClient = client.GetStringClient();
    var result = await stringClient.Set("a", "123456789");

    Console.Read();
}

The encapsulated message commands support asynchronous operations.

8. More Clients

If just the String type isn't enough, we will continue to encapsulate more clients.

哈希:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace CZGL.RedisClient
{
    public class HashClient : CommandClient<HashClient>
    {
        internal HashClient(RedisClient client) : base(client)
        {
        }

        /// <summary>
        /// Set hash
        /// </summary>
        /// <param name="key">Key</param>
        /// <param name="values">Field-value list</param>
        /// <returns></returns>
        public async Task<bool> HmSet(string key, Dictionary<string, string> values)
        {
            await SendCommand($"{StringCommand.HMSET} {key} {string.Join(" ", values.Select(x => $"{x.Key} {x.Value}").ToArray())}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return IsOk(result);
        }

        public async Task<bool> HmSet<T>(string key, T values)
        {
            Dictionary<string, string> dic = new Dictionary<string, string>();
            foreach (var item in typeof(T).GetProperties())
            {
                dic.Add(item.Name, (string)item.GetValue(values));
            }
            await SendCommand($"{StringCommand.HMSET} {key} {string.Join(" ", dic.Select(x => $"{x.Key} {x.Value}").ToArray())}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return IsOk(result);
        }

        public async Task<object> HmGet(string key, string field)
        {
            await SendCommand($"{StringCommand.HMGET} {key} {field}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return IsOk(result);
        }

        private static class StringCommand
        {
            public const string HMSET = "HMSET ";
            public const string HMGET = "HMGET";
            // ... ... More string commands
        }
    }
}

列表:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace CZGL.RedisClient
{
    public class ListClient : CommandClient<ListClient>
    {
        internal ListClient(RedisClient client) : base(client)
        {

        }


        /// <summary>
        /// Set key-value
        /// </summary>
        /// <param name="key">key</param>
        /// <param name="value">value</param>
        /// <returns></returns>
        public async Task<bool> LPush(string key, string value)
        {
            await SendCommand($"{StringCommand.LPUSH} {key} {value}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return IsOk(result);
        }


        public async Task<string> LRange(string key, int start, int end)
        {
            await SendCommand($"{StringCommand.LRANGE} {key} {start} {end}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return result;
        }

        private static class StringCommand
        {
            public const string LPUSH = "LPUSH";
            public const string LRANGE = "LRANGE";
            // ... ... More string commands
        }
    }
}

集合:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace CZGL.RedisClient
{
    public class SetClient : CommandClient<SetClient>
    {
        internal SetClient() { }
        internal SetClient(RedisClient client) : base(client)
        {

        }

        public async Task<bool> SAdd(string key, string value)
        {
            await SendCommand($"{StringCommand.SADD} {key} {value}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return IsOk(result);
        }

        public async Task<string> SMembers(string key)
        {
            await SendCommand($"{StringCommand.SMEMBERS} {key}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return result;
        }


        private static class StringCommand
        {
            public const string SADD = "SADD";
            public const string SMEMBERS = "SMEMBERS";
            // ... ... More string commands
        }
    }
}

有序集合:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace CZGL.RedisClient
{
    public class SortedClient : CommandClient<SortedClient>
    {
        internal SortedClient(RedisClient client) : base(client)
        {

        }

        public async Task<bool> ZAdd(string key, string value)
        {
            await SendCommand($"{StringCommand.ZADD} {key} {value}", out MessageStraceAnalysis<string> strace);
            var result = await strace.Task;
            return IsOk(result);
        }

        private static class StringCommand
        {
            public const string ZADD = "ZADD";
            public const string SMEMBERS = "SMEMBERS";
            // ... ... More string commands
        }
    }
}

这样,我们就有一个具有简单功能的 RedisClient 框架了。

9,更多测试

为了验证功能是否可用,我们写一些示例:

        static RedisClient client = new RedisClient("127.0.0.1", 6379);
        static async Task Main(string[] args)
        {
            var a = await client.ConnectAsync();
            if (!a)
            {
                Console.WriteLine("连接服务器失败");
                Console.ReadKey();
                return;
            }

            Console.WriteLine("连接服务器成功");

            await StringSETGET();
            await StringGETRANGE();
            await StringGETSET();
            await StringMGet();
            Console.ReadKey();
        }

        static async Task StringSETGET()
        {
            var stringClient = client.GetStringClient();
            var b = await stringClient.Set("seta", "6666");
            var c = await stringClient.Get("seta");
            if (c == "6666")
            {
                Console.WriteLine("true");
            }
        }

        static async Task StringGETRANGE()
        {
            var stringClient = client.GetStringClient();
            var b = await stringClient.Set("getrance", "123456789");
            var c = await stringClient.GetRance("getrance", 0, -1);
            if (c == "123456789")
            {
                Console.WriteLine("true");
            }
            var d = await stringClient.GetRance("getrance", 0, 3);
            if (d == "1234")
            {
                Console.WriteLine("true");
            }
        }

        static async Task StringGETSET()
        {
            var stringClient = client.GetStringClient();
            var b = await stringClient.Set("getrance", "123456789");
            var c = await stringClient.GetSet("getrance", "987654321");
            if (c == "123456789")
            {
                Console.WriteLine("true");
            }
        }

        static async Task StringMGet()
        {
            var stringClient = client.GetStringClient();
            var a = await stringClient.Set("stra", "123456789");
            var b = await stringClient.Set("strb", "123456789");
            var c = await stringClient.Set("strc", "123456789");
            var d = await stringClient.MGet("stra", "strb", "strc");
            if (d.Where(x => x == "123456789").Count() == 3)
            {
                Console.WriteLine("true");
            }
        }

10,性能测试

因为只是写得比较简单,而且是单线程,并且内存比较浪费,我觉得性能会比较差。但真相如何呢?我们来测试一下:

        static RedisClient client = new RedisClient("127.0.0.1", 6379);
        static async Task Main(string[] args)
        {
            var a = await client.ConnectAsync();
            if (!a)
            {
                Console.WriteLine("连接服务器失败");
                Console.ReadKey();
                return;
            }

            Console.WriteLine("连接服务器成功");

            var stringClient = client.GetStringClient();
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < 3000; i++)
            {
                var guid = Guid.NewGuid().ToString();
                _ = await stringClient.Set(guid, guid);
                _ = await stringClient.Get(guid);
            }

            watch.Stop();
            Console.WriteLine($"总共耗时:{watch.ElapsedMilliseconds/10} ms");
            Console.ReadKey();
        }

耗时:

总共耗时:1003 ms

大概就是 1s,3000 个 SET 和 3000 个 GET 共 6000 个请求。看来单线程性能也是很强的。

不知不觉快 11 点了,不写了,赶紧睡觉去了。

笔者其它 Redis 文章:

搭建分布式 Redis Cluster 集群与 Redis 入门

Redis 入门与 ASP.NET Core 缓存

11,关于 NCC

.NET Core Community (.NET 中心社区,简称 NCC)是一个基于并围绕着 .NET 技术栈展开组织和活动的非官方、非盈利性的民间开源社区。我们希望通过我们 NCC 社区的努力,与各个开源社区一道为 .NET 生态注入更多活力

.

Join NCC, where there are many framework authors who will teach you how to write frameworks, participate in open source projects, and make your contributions. Don't forget to join NCC!~

痴者工良

高级程序员劝退师

文章评论