From b3f2b2f306707e4c384a49d29ce8fd8871cbf5d4 Mon Sep 17 00:00:00 2001 From: Nicolas Favre-Felix Date: Sat, 9 Apr 2011 16:07:28 +0200 Subject: [PATCH] Support Redis wire protocol over HTML5 WebSockets. --- README.markdown | 33 +++++++++++++++++++++- formats/raw.c | 66 ++++++++++++++++++++++++++++++++++++++++++++ formats/raw.h | 3 ++ tests/websocket.html | 40 +++++++++++++++++++++++++++ websocket.c | 21 +++++++++++--- 5 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 tests/websocket.html diff --git a/README.markdown b/README.markdown index 514abf3..6554179 100644 --- a/README.markdown +++ b/README.markdown @@ -26,6 +26,7 @@ curl -d "GET/hello" http://127.0.0.1:7379/ * Raw Redis 2.0 protocol output with `.raw` suffix * BSON support for compact responses and MongoDB compatibility. * HTTP 1.1 pipelining (70,000 http requests per second on a desktop Linux machine.) +* WebSocket support. * Connects to Redis using a TCP or UNIX socket. * Restricted commands by IP range (CIDR subnet + mask) or HTTP Basic Auth, returning 403 errors. * Possible Redis authentication in the config file. @@ -48,7 +49,6 @@ curl -d "GET/hello" http://127.0.0.1:7379/ * Multi-server support, using consistent hashing. * Database selection in the URL? e.g. `/7/GET/key` to run the command on DB 7. * SSL? -* Add WebSocket support (with which protocol?). * Send your ideas using the github tracker, on twitter [@yowgi](http://twitter.com/yowgi) or by mail to n.favrefelix@gmail.com. # HTTP error codes @@ -252,3 +252,34 @@ $ md5sum redis-logo.png out.png The file was uploaded and re-downloaded properly: it has the same hash and the content-type was set properly thanks to the `.png` extension. + +# WebSockets +Webdis supports WebSocket clients implementing [dixie-76](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76). +Web Sockets are supported with the following formats, selected by the connection URL: + +* JSON (on `/` or `/.json`) +* Raw Redis wire protocol (on `/.raw`) + +**Example**: +
+function testJSON() {
+	var jsonSocket = new WebSocket("ws://127.0.0.1:7379/.json");
+	jsonSocket.onopen = function() {
+
+		console.log("JSON socket connected!");
+		jsonSocket.send(JSON.stringify(["SET", "hello", "world"]));
+		jsonSocket.send(JSON.stringify(["GET", "hello"]));
+	};
+	jsonSocket.onmessage = function(messageEvent) {
+		console.log("JSON received:", messageEvent.data);
+	};
+}
+testJSON();
+
+ +This produces the following output: +
+JSON socket connected!
+JSON received: {"SET":[true,"OK"]}
+JSON received: {"GET":"world"}
+
diff --git a/formats/raw.c b/formats/raw.c index 76565e5..8d44454 100644 --- a/formats/raw.c +++ b/formats/raw.c @@ -1,6 +1,8 @@ #include "raw.h" #include "common.h" #include "http.h" +#include "client.h" +#include "cmd.h" #include #include @@ -48,6 +50,70 @@ integer_length(long long int i) { return sz; } +/* extract Redis protocol string from WebSocket frame and fill struct cmd. */ +struct cmd * +raw_ws_extract(struct http_client *c, const char *p, size_t sz) { + + struct cmd *cmd = NULL; + void *reader = NULL; + redisReply *reply = NULL; + unsigned int i; + (void)c; + + /* create protocol reader */ + reader = redisReplyReaderCreate(); + + /* add data */ + redisReplyReaderFeed(reader, (char*)p, sz); + + /* parse data into reply object */ + if(redisReplyReaderGetReply(reader, (void**)&reply) == REDIS_ERR) { + goto end; + } + + /* add data from reply object to cmd struct */ + if(reply->type != REDIS_REPLY_ARRAY) { + goto end; + } + + /* create cmd object */ + cmd = cmd_new(reply->elements); + + for(i = 0; i < reply->elements; ++i) { + redisReply *ri = reply->element[i]; + + switch(ri->type) { + case REDIS_REPLY_STRING: + cmd->argv_len[i] = ri->len; + cmd->argv[i] = calloc(cmd->argv_len[i] + 1, 1); + memcpy(cmd->argv[i], ri->str, ri->len); + break; + + case REDIS_REPLY_INTEGER: + cmd->argv_len[i] = integer_length(ri->integer); + cmd->argv[i] = calloc(cmd->argv_len[i] + 1, 1); + sprintf(cmd->argv[i], "%lld", ri->integer); + break; + + default: + cmd_free(cmd); + cmd = NULL; + goto end; + } + } + + +end: + /* free reader */ + if(reader) redisReplyReaderFree(reader); + + /* free reply */ + if(reply) freeReplyObject(reply); + + return cmd; +} + + static char * raw_array(const redisReply *r, size_t *sz) { diff --git a/formats/raw.h b/formats/raw.h index f25607f..10321e3 100644 --- a/formats/raw.h +++ b/formats/raw.h @@ -5,9 +5,12 @@ #include struct cmd; +struct http_client; void raw_reply(redisAsyncContext *c, void *r, void *privdata); +struct cmd * +raw_ws_extract(struct http_client *c, const char *p, size_t sz); #endif diff --git a/tests/websocket.html b/tests/websocket.html new file mode 100644 index 0000000..921f35a --- /dev/null +++ b/tests/websocket.html @@ -0,0 +1,40 @@ + + + WebSocket example + + + + diff --git a/websocket.c b/websocket.c index 7f5c7c4..36d3ef3 100644 --- a/websocket.c +++ b/websocket.c @@ -1,11 +1,14 @@ #include "md5/md5.h" #include "websocket.h" #include "client.h" -#include "formats/json.h" #include "cmd.h" #include "worker.h" #include "pool.h" +/* message parsers */ +#include "formats/json.h" +#include "formats/raw.h" + #include #include #include @@ -148,23 +151,33 @@ static int ws_execute(struct http_client *c, const char *frame, size_t frame_len) { struct cmd*(*fun_extract)(struct http_client *, const char *, size_t) = NULL; + formatting_fun fun_reply = NULL; - if(strncmp(c->path, "/.json", 6) == 0) { + if((c->path_sz == 1 && strncmp(c->path, "/", 1) == 0) || + strncmp(c->path, "/.json", 6) == 0) { fun_extract = json_ws_extract; + fun_reply = json_reply; + } else if(strncmp(c->path, "/.raw", 5) == 0) { + fun_extract = raw_ws_extract; + fun_reply = raw_reply; } if(fun_extract) { + + /* Parse websocket frame into a cmd object. */ struct cmd *cmd = fun_extract(c, frame, frame_len); + if(cmd) { /* copy client info into cmd. */ cmd_setup(cmd, c); cmd->is_websocket = 1; - /* get redis connection from pool */ + /* get Redis connection from pool */ redisAsyncContext *ac = (redisAsyncContext*)pool_get_context(c->w->pool); /* send it off */ - cmd_send(ac, json_reply, cmd); + cmd_send(ac, fun_reply, cmd); + return 0; } }