Статьи

Использование веб-сокетов с серверной логикой

Редактирование 2 марта: обратите внимание, что я обнаружил, что у меня было неправильное понимание веб-сокетов. Вместо beforeSendMessage этот код должен использовать beforePublish. Смотрите эту запись в блоге для деталей.

Прошло несколько дней со времени моей последней демонстрации веб-сокета ColdFusion 10, главным образом потому, что мой сервер стал ядерным и вывел из строя несколько небольших городов. В то время как я работаю с техникой , чтобы выяснить вопрос (эй, это является бета — все — таки), я решил , что просто воспользоваться Hostek в свободное предложение от ColdFusion 10 хостинга. Для подтверждения моего аккаунта потребовалось около 30 минут, и я был в рабочем состоянии. Так что без дальнейших церемоний, давайте продолжим. (Но учтите, если вы не помните мои предыдущие демоверсии чата, перейдите по ссылкам ниже.)

В моей последней записи в блоге я изменил чат так, чтобы он возвращал количество пользователей, вошедших в систему. Это круто, но было бы еще лучше предоставить список пользователей. Также было бы здорово, если бы мы могли использовать этот список для проверки вашего имени пользователя. Мы не хотим, чтобы в чате были два человека с одинаковыми именами. К счастью, есть довольно простой способ справиться с обоими из них.

Давайте сначала обсудим, как мы собираемся создать список пользователей. В наших предыдущих версиях вы выбрали имя и начали общаться. Другие пользователи знали, кто вы, потому что вы отправили пакет сообщений, который содержал ваше имя и ваше сообщение. Но на самом деле у сервера не было представления о том, кто есть кто. Да, сервер знал, кто был подключен. Вы даже можете получить список всех этих клиентов:

allMyPeeps = wsGetSubscribers("chat");

 

Но эта информация содержит только метаданные о клиентах. Вот пример:

 

 Как мы передаем пользовательские данные на сервер? Просто. Когда вы подписываетесь на веб-сокет, у вас есть возможность передать дополнительную структуру данных. Эти данные могут быть чем угодно. Однако первое изменение, которое мы должны сделать, это изменить наш cfwebsocket, чтобы он не автоматически подписывался на канал чата. Теперь мы хотим подписаться только после того, как выбрали имя. Итак, вот тег:

<cfwebsocket name="chatWS" onMessage="msgHandler">

 

По сути, мы только что сказали, что хотим, чтобы наш дескриптор JavaScript был chatWS, а msgHandler — обработчик сообщений. Пользователь не будет подписан. Теперь мы обновляем наш обработчик для диалога, которое запрашивает имя пользователя:

$("#usernamebutton").click(function() {
    var u = $.trim($("#username").val());
    if (u == "") {
        return;
    }
    //copies it to global scope
    username=u;
    chatWS.subscribe("chat", {userinfo: {
        username: u
    }});
            
});

 

Все идет нормально? Теперь этот вызов на стороне сервера может получить все пользователи:

Woot. Хорошо, достаточно просто. Но как нам справиться с «блокировкой» подписки, если вы выбрали то же имя, что и кто-то другой? С веб-сокетами и ColdFusion 10 вы можете использовать CFC для обработки различных событий в течение срока службы веб-сокетов. Один из них — allowSubscribe. Это позволяет запретить подписку. Итак, сначала я говорю ColdFusion подключить мой канал чата к CFC (это из Application.cfc):

this.wschannels = [
2    {name="chat",cfclistener:"chatws"}
3];

 Далее мы определим CFC. У меня здесь есть несколько дополнительных функций, но сейчас остановимся на allowSubscribe:

component extends="CFIDE.websocket.ChannelListener" {

 public boolean function allowSubscribe(struct subscriberInfo) {
      if(!structKeyExists(arguments.subscriberInfo, "userinfo")) return false;
      var attemptuser = arguments.subscriberInfo.userinfo.username;
     
      //lock me baby
      lock type="exclusive" scope="application" timeout=30 {

            var users = getUserList();
            if(arrayFind(users,attemptuser) != 0) return false;
            arrayAppend(users, attemptuser);
            
            var msg = {"type":"list","userlist":users};
            wspublish("chat",msg);

            return true;
     }
 }

    public any function beforeSendMessage(any message, Struct subscriberInfo) {
          if(structKeyExists(message, "type") && message.type == "chat") message.chat=rereplace(message.chat, "<.*?>","","all");
        return message;
    }

    public function afterUnsubscribe(Struct subscriberInfo) {
        var users = getUserList();            
        var msg = {"type":"list","userlist":users};
        wspublish("chat",msg);
    }
    
    public function getUserList() {
        var users = [];
        arrayEach(wsGetSubscribers('chat'), function(item) {
            arrayAppend(users, item.subscriberinfo.userinfo.username);
        });
        return users;
    }

}

 Логика проста. Посмотрите на наш запрос, чтобы увидеть, что имя пользователя. Затем — в блокировке, чтобы убедиться, что он однопоточный — получите список пользователей (с помощью метода утилиты getUserlist, который я написал) и посмотрите, вошли ли мы в систему. Там тоже происходит что-то еще интересное:

var users = getUserList();
if(arrayFind(users,attemptuser) != 0) return false;
arrayAppend(users, attemptuser);
            
var msg = {"type":"list","userlist":users};
wspublish("chat",msg);

 

Этот код возьмет список пользователей, и, если все будет кошерно, добавьте нового парня и опубликуйте список. По сути, наш сервер теперь передает, что список пользователей изменился. Тогда мне не нужно писать код для обработки запроса о недавнем списке пользователей на сервере. Сервер достаточно умен, чтобы вместо этого сказать клиентам.

Обратите внимание, что у нас также есть после отписаться. Это обычно обрабатывает обновление списка, когда люди уходят. Однако в текущей сборке есть ошибка, из-за которой этот метод не вызывается при закрытии браузера (или вкладки). Чтобы было понятно, клиент удаляется из списка, но обработчик событий не запускается. (Это сообщается и будет исправлено в ближайшее время.)

Хорошо, теперь давайте вернемся к нашему внешнему коду. Наш обработчик сообщений стал немного сложным. (И я действительно должен переписать его, чтобы использовать оператор switch.)

function msgHandler(message) {

    if (message.type == "data") {
        var data = JSON.parse(message.data);
        if (data.type == "chat") {
            $("#chatlog").append(data.username + " says: " + data.chat + "\n");
            $("#chatlog").scrollTop($('#chatlog')[0].scrollHeight);
        }
        else if (data.type == "subscribe") {
                $("#chatlog").append(data.chat + "\r");
                $("#chatlog").scrollTop($('#chatlog')[0].scrollHeight);
        } else if (data.type == "list") {
            var list = data.userlist.join(", ");
            $("#userCount").html(list);        
        }            
    }

    //handle failed sub
    if (message.type == "subscribe" && message.code == -1) {
        $("#modalerror").text("Username already taken!");
    }
        
    //handle subscription
    if(message.type == "response" && message.reqType == "subscribe") {
    msg = {
        type: "subscribe",
        username: username,
        chat: username+" joins the chat."
    };
    chatWS.publish("chat",msg);
    $("#usernamemodal").modal("hide");
        //run a manual invoke to get the user list
        chatWS.invoke("chat4.chatws","getUserList");
    }
        
    //handle user list
    if(message.type == "response" && message.reqType == "invoke") {
        if (message.code != 0) {
            console.log("ERROR");
            console.dir(message);
        }
        var data = JSON.parse(message.data);
        var list = data.join(", ");
        $("#userCount").html(list);        
    }
}

 

Как я уже сказал — немного сложно, правда? Наш обработчик сообщений теперь используется для нескольких целей. Он получает базовые чаты. Получает ответы на подписки. Он также получает списки пользователей. Мы должны быть в состоянии справиться со всем этим. Я определенно мог бы написать это чище, более документировано и т. Д. Я выделю одну строку, в частности:

//run a manual invoke to get the user list
chatWS.invoke("chat4.chatws","getUserList");

 

Цель этого состоит в том, чтобы при первом входе в систему рассылка пользователей отправлялась подписанным пользователям, но отправлялась до того, как мы возвращаем true в обработчике CFC, поэтому вы фактически не включены. Метод invoke объекта chatWS позволяет мне запустить метод CFC. В этом случае я просто использую тот метод, который я настроил

Есть смысл?

Итак — пока что. У меня скоро появится еще одна модификация (пользователь обнаружил еще один хак с моим кодом, и я опубликую блог о том, как это исправить). Вы можете проверить демо ниже. Я также включил почтовый индекс кода.