Node.js + Redis Pub/Sub Örneği

Örnek uygulamada, iki üye arasındaki mesajlaşma sayfasında, bir üye diğerine mesaj yazarken karşı tarafa “X mesaj yazıyor..” şeklinde anlık durum bilgisi vereceğiz.

Önce Node.js kullanan server tarafınına bakalım;

var io = require("socket.io").listen(3000)
    , redis = require("redis")
    , pub = redis.createClient()
    , sub = redis.createClient();

console.log("Listening on *:3000");
 
io.sockets.on("connection", function (socket) {

    // listen event'i ile client bir kanala abone oluyor.
    socket.on("listen", function (data) {
        // kanala abone ol.
        sub.subscribe("user:" + data.user_id);
    });

    // bir kanala mesaj gonderir.
    socket.on("message", function (data) {
        // kanala mesaj gönder.
        pub.publish("user:" + data.user_id, data.action);
    });
 
    // abone olunan kanallara mesaj geldiginde
    sub.on("message", function (channel, message) {
        // bu kanala gelen mesaji ilgili event"e gonderir.
        // redis channel => socket event
        socket.emit(channel, message);
    });
 
    // baglantiyi kapattiginda
    socket.on("close", function () {
        sub.quit();
    });
});

Client tarafı ise şöyle;

<input type="text" id="msg">
<br>
<div id="thread-status"></div>

<!-- aşağıdaki socket.io dosyası, yukarıdaki node.js server tarafından sunuculacaktır. -->
<script src="http://localhost:3000/socket.io/socket.io.js"></script>
<script>
    var socket = io.connect("http://localhost:3000");
    var thread_status = "";

    // bu örnekte 27 nolu kullanıcı ile 3 nolu kullanıcı mesajlaşıyor.
    // üyelerin yazı yazma etkinlikleri karşı tarafın kanalına bildiriliyor.
    var my_user_id  = 3;
    var her_user_id = 27;
    var her_name    = "Karşı taraf";

    // üye kendi kanalına abone oluyor.
    // böylece 3 nolu kullanıcı kanalına gelen mesajları alabilir.
    socket.emit("listen", {user_id: my_user_id});

    // kendi kanalına mesaj geldiğinde çalışacak callback methodu.
    socket.on("user:" + my_user_id, function (action) {

        var status = $("#thread-status");

        if (action == "write:start") {
            status.html(her_name + " yazıyor..");
        } else if (action == "write:stop") {
            status.html("");
        }
    });

    $(document).ready(function () {

        // mesaj yazma alanında değişiklik olduğunda
        $("#msg").keyup(function () {

            var msg    = $(this).val();
            var action = "";

            // action'ı belirle.
            if (msg) {
                action = "write:start";
            } else {
                action = "write:stop";
            }

            // her harfe basıldığında event göndermeye gerek yok.
            // o nedenle bir öncekiyle aynı ise bir şey yapma.
            if (thread_status == action) return;

            // yeni durumu socket ile diğer üyenin kanalına gönder.
            window.socket.emit("message", { user_id: her_user_id, action: action });

            // mevcut durumu güncelle.
            thread_status = action;
        });
    });
</script>

Yukarıdaki örnekte herhangi bir güvenlik önlemi bulunmuyor. Yani isteyen istediği kanala browser’daki konsol yardımıyla abone olabilir. Bu açığı basit bir token yardımıyla çözebilirsiniz:

  • Her kullanıcı için unique bir token oluştur
  • Kullanıcının mesajlaşma yetkisi olduğu diğer kullanıcı id’lerini bir Redis listesine (user:76d34d94c2e7fdd50c36585a4e9a7b77:friends) ekle
  • Abonelik ve mesaj gönderme işlemlerinde bu token’ını da data’ya ekle
    window.socket.emit('message', {
        user_id : 27, 
        action  : "message:start", 
        token   : "76d34d94c2e7fdd50c36585a4e9a7b77"
    });
    
  • Node.js tarafında abonelik ve mesaj gönderme işlemi yapmadan önce yetki kontrolü yap
    socket.on('listen', function (data)
    {
        // kullanicidan token iste.
        // yetkili olduğu kişiler listesinde var mi diye kontrol et.
        // eger listesindeyse dinlemesine izin ver.
    
        client.sismember('user:' + data.token + ':friends', data.user_id, function (err, response) {
    
            if (response) {
                // istenilen kanala abone ol
                sub.subscribe("user:" + data.user_id);
            }
        });
    });
    

Dikkat edilmesi gereken bir diğer husus da iki üyenin aynı üyeye mesaj yazma durumunda aynı kanala event göndereceklerinden karışıklık meydana gelecektir. Bu nedenle event’ler kullanıcı kanalları yerine, sadece iki üyenin yetkisi olan bir diyalog kanalına gönderilmelidir.