Статьи

Приложение чата с использованием Socket.IO

В этой статье мы собираемся создать простое приложение для чата, используя Socket.IO и Backbone.js . Socket.IO предназначен для разработчиков, которые хотят начать разработку интерактивных веб-приложений в режиме реального времени, таких как системы чата или многопользовательские игры. Backbone.js добавит структуру в наш клиентский код и упростит управление и разделение проблем в нашем приложении.

Читатели должны быть знакомы с Node.js и Express . Знакомство с Backbone является плюсом, а также Underscore.js , который используется для базовых шаблонов.

Вступление

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

Архитектура приложений

Сторона клиента

Начнем с рассмотрения кода на стороне клиента. Все чаты обрабатываются в HomeView Давайте начнем с определения HomeModel/public/js/models/main.js

 var HomeModel = Backbone.Model.extend({
  defaults: {
    // Backbone collection for users
    onlineUsers: new UserCollection(),

    // Backbone collection for user chats, initialized with a predefined chat model
    userChats: new ChatCollection([
      new ChatModel({sender: '', message: 'Chat Server v.1'})
    ])
  },

  // method for adding a new user to onlineUsers collection
  addUser: function(username) {
    this.get('onlineUsers').add(new UserModel({name: username}));
  },

  // method for removing a user from onlineUsers collection
  removeUser: function(username) {
    var onlineUsers = this.get('onlineUsers');
    var u = onlineUsers.find(function(item) {
          return item.get('name') == username;
        });

    if (u) {
      onlineUsers.remove(u);
    }
  },

  // method for adding new chat to userChats collection
  addChat: function(chat) {
    this.get('userChats').add(new ChatModel({sender: chat.sender, message: chat.message}));
  },
});

Мы используем коллекции Backbone для прослушивания изменений в коллекции. Обновления в коллекциях отражаются автоматически нашим представлением.

Далее мы определяем наш домашний шаблон внутри /public/index.html

 <script type="text/template" id="home-template">
  <div class="row">
    <div class="col-md-10">
      <div class="panel panel-default">
        <div class="panel-heading">Lobby</div>
        <div class="panel-body">
          <div class="nano">
            <div class="content">
              <div class="list-group" id="chatList"></div>
            </div>
          </div>
          <form>
            <input class="form-control" type="text" id="chatInput"></input>
          </form>
        </div>
      </div>
    </div>
    <div class="col-md-2">
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title">Online Users <span class="badge pull-right" id="userCount"></span></h3>
        </div>
        <div class="panel-body">
          <div class="nano">
            <div class="content">
              <div class="list-group" id="userList"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</script>

HomeView/public/js/views/main.js Файл относительно длинный, поэтому читатель может его изучить.

Клиент чата

Далее давайте определим наш чат-клиент Socket.IO. Он связывается с сервером, отправляя сообщения на сервер и прослушивая уведомления. Эти уведомления запускают события на шине событий для связи с контроллером. Следующий код находится в /public/js/socketclient.js

 var ChatClient = function(options) {
  // redefine this to avoid conflicts
  var self = this;

  // app event bus
  self.vent = options.vent;

  // server hostname replace with your server's hostname eg: http://localhost
  self.hostname = 'http://chatfree.herokuapp.com';

  // connects to the server
  self.connect = function() {
    // connect to the host
    self.socket = io.connect(self.hostname);

    // set responseListeners on the socket
    self.setResponseListeners(self.socket);
  }

  // send login message
  self.login = function(name) {
    self.socket.emit('login', name);
  }

  // send chat message
  self.chat = function(chat) {
    self.socket.emit('chat', chat);
  }

  self.setResponseListeners = function(socket) {
    // handle messages from the server
    socket.on('welcome', function(data) {
      // request server info
      socket.emit('onlineUsers');

      self.vent.trigger('loginDone', data);
    });

    socket.on('loginNameExists', function(data) {
      self.vent.trigger('loginNameExists', data);
    });

    socket.on('loginNameBad', function(data) {
      self.vent.trigger('loginNameBad', data);
    });

    socket.on('onlineUsers', function(data) {
      console.log(data);
      self.vent.trigger('usersInfo', data);
    });

    socket.on('userJoined', function(data) {
      self.vent.trigger('userJoined', data);
    });

    socket.on('userLeft', function(data) {
      self.vent.trigger('userLeft', data);
    });

    socket.on('chat', function(data) {
      self.vent.trigger('chatReceived', data);
    });
  }
}

Socket.IO действительно позволяет легко отправлять / получать сообщения между клиентом и сервером. Здесь мы используем два метода:

  • socket.emit(message, [callback])
  • socket.on(message, callback) callback

Ниже приведена схема, показывающая, как выглядит наш протокол чата:

Протокол чата

Главный контроллер

Для заключительной части на стороне клиента, у нас есть наш контроллер, управляющий между представлениями, моделями и клиентом сокетов. Поместите это в /public/js/main.js

 var MainController = function() {
  var self = this;

  // Event Bus for socket client
  self.appEventBus = _.extend({}, Backbone.Events);
  // Event Bus for Backbone Views
  self.viewEventBus = _.extend({}, Backbone.Events);

  // initialize function
  self.init = function() {
    // create a chat client and connect
    self.chatClient = new ChatClient({vent: self.appEventBus});
    self.chatClient.connect();

    // create our views, place login view inside container first.
    self.loginModel = new LoginModel();
    self.containerModel = new ContainerModel({
      viewState: new LoginView({
        vent: self.viewEventBus,
        model: self.loginModel
      })
    });
    self.containerView = new ContainerView({model: self.containerModel});
    self.containerView.render();
  };

  // View Event Bus Message Handlers
  self.viewEventBus.on('login', function(name) {
    // socketio login
    self.chatClient.login(name);
  });

  self.viewEventBus.on('chat', function(chat) {
    // socketio chat
    self.chatClient.chat(chat);
  });

  // Socket Client Event Bus Message Handlers

  // triggered when login success
  self.appEventBus.on('loginDone', function() {
    self.homeModel = new HomeModel();
    self.homeView  = new HomeView({vent: self.viewEventBus, model: self.homeModel});

    // set viewstate to homeview
    self.containerModel.set('viewState', self.homeView);
  });

  // triggered when login error due to bad name
  self.appEventBus.on('loginNameBad', function(name) {
    self.loginModel.set('error', 'Invalid Name');
  });

  // triggered when login error due to already existing name
  self.appEventBus.on('loginNameExists', function(name) {
    self.loginModel.set('error', 'Name already exists');
  });

  // triggered when client requests users info
  // responds with an array of online users.
  self.appEventBus.on('usersInfo', function(data) {
    var onlineUsers = self.homeModel.get('onlineUsers');
    var users = _.map(data, function(item) {
      return new UserModel({name: item});
    });

    onlineUsers.reset(users);
  });

  // triggered when a client joins the server
  self.appEventBus.on('userJoined', function(username) {
    self.homeModel.addUser(username);
    self.homeModel.addChat({sender: '', message: username + ' joined room.'});
  });

  // triggered when a client leaves the server
  self.appEventBus.on('userLeft', function(username) {
    self.homeModel.removeUser(username);
    self.homeModel.addChat({sender: '', message: username + ' left room.'});
  });

  // triggered when chat receieved
  self.appEventBus.on('chatReceived', function(chat) {
    self.homeModel.addChat(chat);
  });
}

Чтобы загрузить все, мы просто создаем MainControllerinit/public/js/main.js

 $(document).ready(function() {
  var mainController = new MainController();

  mainController.init();
});

Вот и все на стороне клиента. Если вы столкнулись с какими-либо ошибками, в Chrome есть отличные средства отладки. Используйте его вкладку сети, чтобы увидеть, действительно ли сообщения обмениваются.

Сторона сервера

Далее мы обратимся к стороне сервера, которая реализована в Node.js, Express и Socket.IO. Поместите этот код, который реализует компонент сервера Express, в /scripts/web.js

 // requirements
var express = require('express');
var http = require('http');
var socketio = require('socket.io');
var path = require('path');

// routes
var routes = require('../routes/index.js');

var app = express();

// routes middleware
app.use(app.router);
// serve public folder
app.use(express.static(path.join(__dirname, '../public')));

// serve index.html for every path 
app.use(routes.index);

// this is how you use socket io with express
var server = http.createServer(app);
var io = socketio.listen(server);

var port = process.env.PORT || 8080;

server.listen(port, function() {
  console.log(' - listening on ' + port+ ' ' + __dirname);
});

// require our chatserver
var ChatServer = require('./chatserver');

// initialize a new chat server.
new ChatServer({io: io}).init();

Чат-сервер

Последняя часть нашего приложения — это чат-сервер. Это отвечает за ведение списка онлайн-пользователей и трансляцию сообщений чата. Первым событием, которое наш сервер получит при новом клиентском соединении, является меткое connection обработчики событий connectionsocket socket

  • socket.on(message, callback)callback message
  • socket.on('disconnect', callback)callback
  • socket.emit(message, args)message
  • socket.broadcast.send(message, args)message

Теперь мы увидели, как обрабатывать сокеты, давайте определим пользовательскую модель внутри /scripts/chatserver.js

 // User Model
var User = function(args) {
  var self = this;

  // Socket field
  self.socket = args.socket;
  // username field
  self.user = args.user;
}

Наконец, наш чат-сервер находится в /scripts/chatserver.js

 var Server = function(options) {
  var self = this;

  self.io = options.io;

  // users array
  self.users = [];

  // initialize function
  self.init = function() {
    // Fired upon a connection
    self.io.on('connection', function(socket) {
      self.handleConnection(socket);
    });
  }

  // socket handler for an incoming socket
  self.handleConnection = function(socket) {
    // wait for a login message
    socket.on('login', function(username) {
      var nameBad = !username || username.length < 3 || username.length > 10;

      // check for badname
      if (nameBad) {
        socket.emit('loginNameBad', username);
        return;
      }

      var nameExists = _.some(self.users, function(item) {
        return item.user == username;
      });

      // check for already existing name
      if (nameExists) {
        socket.emit('loginNameExists', username);
      } else {
        // create a new user model
        var newUser = new User({ user: username, socket: socket });
        // push to users array
        self.users.push(newUser);
        // set response listeners for the new user
        self.setResponseListeners(newUser);
        // send welcome message to user
        socket.emit('welcome');
        // send user joined message to all users
        self.io.sockets.emit('userJoined', newUser.user);
      }
    });
  }

  // method to set response listeners
  self.setResponseListeners = function(user) {
    // triggered when a socket disconnects
    user.socket.on('disconnect', function() {
      // remove the user and send user left message to all sockets
      self.users.splice(self.users.indexOf(user), 1);
      self.io.sockets.emit('userLeft', user.user);
    });
    // triggered when socket requests online users
    user.socket.on('onlineUsers', function() {
      var users = _.map(self.users, function(item) {
        return item.user;
      });

      user.socket.emit('onlineUsers', users);
    });

    // triggered when socket send a chat message
    user.socket.on('chat', function(chat) {
      if (chat) {
        self.io.sockets.emit('chat', { sender: user.user, message: chat });
      }
    });
  }
}

Вывод

Мы видели, как создать простое приложение для чата, используя Backbone и Socket.IO. Существует множество функций Socket.IO, которые мы не рассмотрели в этой статье, таких как комнаты и пространства имен. Socket.IO позволяет легко обмениваться сообщениями между клиентами и сервером. Мы использовали Node.js для серверной части, хотя в Socket.IO есть библиотеки для многих других платформ. Для получения дополнительной информации и ресурсов, проверьте вики Socket.IO .

Вы можете найти исходный код этой статьи на GitHub . Живая демоверсия также доступна на Heroku .