I am writing in C++, but here you won't see much of a difference from a plain C implementation. This is to keep the example focused on the hiredis features. I plan to refactor this code to have some C++ fun in a next post.
What I want to do, is connecting to a Redis server (assuming it runs locally on the default port), store on it a key/value pair, and immediately fetch it back. Something like this:
redisContext* ctx = TTR::connect("localhost", 6379); // 1 if(ctx) { std::string key("key"); // 2 std::string value("value"); TTR::set(ctx, key, value); // 3 std::string cache = TTR::get(ctx, key); // 4 if(value == cache) { std::cout << "Value stored and fetched correctly" << std::endl; } else { std::cout << "Something weird happened" << std::endl; } TTR::disconnect(ctx); // 5 }1. redisContext is the hiredis structure that keeps the context for a connection to Redis. Namespace is one of the few C++ features I'm using here, all my wrapper functions are in a namespace named TTR, so to avoid any name clash. The TTR::connect() function is one of them and, as you should expect, it establish a connection to the specified Redis server, or returns NULL in case of failure.
2. The key/value pair that I want to store on Redis.
3. Push a key/value to the server.
4. Fetch the value from the Radis server for the same key I have already used for the set(). I wouldn't ever expect the fetched value being different from the original one.
5. Do not forget to disconnect!
Writing code like that in the real world is asking for trouble. Converting my bunch of free functions in a class is easy, straightforward and immediately pays off, saving the nuisance of passing around the Redis context and be forced of taking care of its disposal. But I'll do it another time.
Let's now see how I have implemented my functions - remember that all of them are in the TTR namespace.
The most interesting one is connect():
redisContext* connect(const std::string& server, int port) { std::string sport = server + ":" + boost::lexical_cast<std::string>(port); // 1 if(!port || server.empty()) // 2 { std::cout << "Can't connect to Redis [" + sport + "]" << std::endl; return NULL; } redisContext* context = redisConnect(server.c_str(), port); // 3 if(!context) // paranoid { std::cout << "No memory for Redis on " + sport << std::endl; return NULL; } if(context->err) // 4 { std::cout << "Can't connect to Redis on " + sport + " - " + context->errstr << std::endl; redisFree(context); // 5 return NULL; } std::cout << "Connected to Redis [" + sport + "]" << std::endl; return context; }1. I concatenate the server name to the port number for debugging purpose, notice the usage of the handy boost lexical cast to convert an integer to a C++ string.
2. Test for valid user input. It could, and probably should, be more strict. But you get the idea.
3. Call the hiredis connection function. It would fail, returning NULL, for an out of memory problem. This is quite improbable. Still, better safe than sorry.
4. When there is a trouble connecting to the Redis server, we have it reported in the fields err and errstr on the Redis context object returned. If this is the case, I log a message, release the context, and return a fat NULL to the caller.
5. Remember, any context returned by redisConnect() has to be cleaned up calling redisFree().
The disconnect() is so boring that one would happily hide it in a class destructor:
void disconnect(redisContext* context) { if(context) // 1 { std::cout << "Disconnecting from Redis" << std::endl; redisFree(context); } }1. Passing a NULL to redisFree could result in a disaster (AKA a segmentation fault), so, I'd better check for it.
The real stuff, setting and getting a value on Redis, is done by these functions:
void set(redisContext* context, const std::string& key, const std::string& value) // 1 { if(!context) // 2 return; void* reply = // 3 redisCommand(context, "SET %b %b", key.c_str(), key.length(), value.c_str(), value.length()); if(reply) { freeReplyObject(reply); return; } // unexpected std::cout << "No reply from Redis" << std::endl; } std::string get(redisContext* context, const std::string& key) { if(!context) return ""; redisReply* reply = static_cast<redisReply*>(redisCommand(context, "GET %b", key.c_str(), key.length())); if(!reply) return ""; std::string result = reply->str ? reply->str : ""; // 4 freeReplyObject(reply); return result; }1. We don't care about the Redis reply, any failure in setting a value for a specified key for the passed Redis context is (almost) silently ignored.
2. As we have already seen, Redis doesn't check if the context we pass to it is good or not. To avoid an unpleasent segmentation fault, it's better to check it ourself.
3. Here I don't care of what is the actual reply of the Redis server, I only check if it actually emits an answer and, if so, I clean it up.
As you can see, redisCommand() is designed to be similar to the standard C fprintf() function. First argument is the Redis context on which the call is performed, Than we have a string, containing the name of the actual operation (here is SET) and any required parameter, identified by a percent flag. If you know that you are about to send plain strings with no special character in it, you can use the "%s" placeholder. By I want to play safe, so I use the "%b", that allows binary strings to be sent. In this case I have to double the number of subsequent arguments, passing both the relative C-string and its size.
4. When I GET from Redis, I am much more interested in the reply object, so I cast the redisCommand() return value (a void pointer) to a pointer to its actual type, redisReply. I need its str field, that contains the value that Redis stores for the key passed as GET parameter, so firstly I check if the reply is not NULL, then I copy its str content in a C++ string, so that I can safely clean the reply object up before returning.
No comments:
Post a Comment