+2

Tạo một ứng dụng chat đơn giản sử dụng Nodejs và Websocket

Node.js là một sản phẩm tuyệt vời. Nó cho phép bạn tự do lên ý tưởng và tạo ra một Server mạnh mẽ bất kể sử dụng cho ứng dụng Web hay các ứng dụng Android.

Và để thử nghiệm sự tuyệt vời của Node.js thì ở bài viết này, tôi sẽ hướng dẫn các bạn tạo ra một ứng dụng đơn giản: ứng dụng gửi tin nhắn hay còn gọi là Chat application trên nền tảng Web.

Nói đến một ứng dụng Chat trên nền tảng Web thì ta sẽ nghĩ ngay đến một thư viện đi kèm với Nodejs: Socket.io. Nhưng hôm nay chúng ta sẽ không sử dụng Socket.io mà dùng một thư viện khác có tên: Websocket.

Websocket có thể coi là một thư viện gần tương tự như Socket.io, bản thân nó chứa đựng các phương thức cho phép dữ liệu được đồng bộ theo thời gian thực giữa Server - Client, cho phép tạo ra Server dành cho các ứng dụng cần đến sự đồng bộ ngay lập tức như là các ứng dụng game hay chính Google Docs cũng đang sử dụng. Điểm khác biệt lớn nhất giữa Websocket và Socket.io có lẽ đơn giản là Socket.io thì cần import một bộ thư viện, còn Websocket thì bằng một cách nào đó, bạn chỉ cần khai báo đối tượng trực tiếp trên các trình duyệt lõi Chromium hoặc Firefox mà không cần import thêm bất kỳ thư viện nào khác.

Còn lý do tại sao tôi lại dùng Websocket mà không phải Socket.io thì đơn giản vì tôi không thích phải import một bộ thư viện JS nào khác vào bộ code của mình, có thể một ngày đẹp trời nào đó chúng xảy ra xung đột, đó sẽ là một thảm họa. Một phần là vì Socket.io đã tạo ra cho bạn toàn bộ các phương thức tuyệt vời mà bạn chỉ cần gọi ra và sử dụng, còn với Websocket thì việc này "tá điền" hơn một chút nhưng cái lợi là bạn sẽ biết bạn đang viết gì và có thể dễ dàng kiểm soát bộ code của mình. Bạn có thể đọc thêm về các phương thức của Websocket tại đây

OK, lan man vậy đủ rồi, bây giờ ta sẽ đến phần chính.

Ý tưởng:

Chúng ta sẽ tạo ra một trang Web cho phép hai hay nhiều người dùng có thể chat với nhau và lưu lại lịch sử các tin nhắn này. Việc này được thực hiện thông qua sự giao tiếp giữa Server và Client. Khi một Client thực hiện việc gửi một đoạn tin nhắn, Server sẽ lắng nghe sự kiện này, sau đó Server xử lý đoạn tin nhắn đó và tạo ra một sự kiện gửi lại đoạn tin nhắn cho toàn bộ các Client đang kết nối, Các client này tự động lắng nghe sự kiện mà Server gửi lại và thực thi một phương thức cho phép tạo ra code HTML và hiển thị lên màn hình. Tóm tắt 3 bước hoạt động:

  • Server lưu lại thông tin khi người dùng kết nối vào trang Web
  • Server nhận tin nhắn khi người dùng bấm gửi
  • Server tự động broadcasts tin nhắn đó cho toàn bộ người dùng đang kết nối

Tạo trước một trang Web HTML tĩnh:

Bạn có thể tham khảo đoạn code dưới đây, hoặc nếu không thích thì bạn hãy tự tạo ra cho mình theo phong cách riêng:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>WebSockets - Simple chat</title>
    <style>
    * { font-family:tahoma; font-size:12px; padding:0px;margin:0px;}
    p { line-height:18px; }
    div { width:500px; margin-left:auto; margin-right:auto;}
    #content { padding:5px; background:#ddd; border-radius:5px;
        overflow-y: scroll; border:1px solid #CCC;
        margin-top:10px; height: 160px; }
    #input { border-radius:2px; border:1px solid #ccc;
        margin-top:10px; padding:5px; width:400px;
    }
    #status { width:88px;display:block;float:left;margin-top:15px; }
  </style>
  </head>
  <body>
    <div id="content"></div>
    <div>
      <span id="status">Connecting...</span>
      <input type="text" id="input" disabled="disabled" />
    </div>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
    </script>
    <script src="./frontend.js"></script>
  </body>
</html>

Node.js Server

Trước hết bạn cần khởi tạo một project Node.js mới, sau đó tải bộ plugin WebSocket-Node về server vừa tạo:

npm install websocket

Websocket server template

Hãy thêm đoạn code dưới đây vào file app.js (hoặc index.js) của Node.js:

var WebSocketServer = require('websocket').server;
var http = require('http');

var server = http.createServer(function(request, response) {
  // process HTTP request. Since we're writing just WebSockets
  // server we don't have to implement anything.
});
server.listen(1337, function() { });

// create the server
wsServer = new WebSocketServer({
  httpServer: server
});

// WebSocket server
wsServer.on('request', function(request) {
  var connection = request.accept(null, request.origin);

  // This is the most important callback for us, we'll handle
  // all messages from users here.
  connection.on('message', function(message) {
    if (message.type === 'utf8') {
      // process WebSocket message
    }
  });

  connection.on('close', function(connection) {
    // close user connection
  });
});

Tất nhiên đây chỉ là cái sườn để bạn có thể thấy khái quát chúng ta sẽ làm gì tiếp theo. Hãy sửa lại một chút theo như bên dưới nào:

"use strict";
// Optional. You will see this name in eg. 'ps' or 'top' command
process.title = 'node-chat';
// Port where we'll run the websocket server
var webSocketsServerPort = 1337;
// websocket and http servers
var webSocketServer = require('websocket').server;
var http = require('http');
/**
 * Global variables
 */
// latest 100 messages
var history = [ ];
// list of currently connected clients (users)
var clients = [ ];
/**
 * Helper function for escaping input strings
 */
function htmlEntities(str) {
  return String(str)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;')
      .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// Array with some colors
var colors = [ 'red', 'green', 'blue', 'magenta', 'purple', 'plum', 'orange' ];
// ... in random order
colors.sort(function(a,b) { return Math.random() > 0.5; } );
/**
 * HTTP server
 */
var server = http.createServer(function(request, response) {
  // Not important for us. We're writing WebSocket server,
  // not HTTP server
});
server.listen(webSocketsServerPort, function() {
  console.log((new Date()) + " Server is listening on port "
      + webSocketsServerPort);
});
/**
 * WebSocket server
 */
var wsServer = new webSocketServer({
  // WebSocket server is tied to a HTTP server. WebSocket
  // request is just an enhanced HTTP request. For more info 
  // http://tools.ietf.org/html/rfc6455#page-6
  httpServer: server
});
// This callback function is called every time someone
// tries to connect to the WebSocket server
wsServer.on('request', function(request) {
  console.log((new Date()) + ' Connection from origin '
      + request.origin + '.');
  // accept connection - you should check 'request.origin' to
  // make sure that client is connecting from your website
  // (http://en.wikipedia.org/wiki/Same_origin_policy)
  var connection = request.accept(null, request.origin); 
  // we need to know client index to remove them on 'close' event
  var index = clients.push(connection) - 1;
  var userName = false;
  var userColor = false;
  console.log((new Date()) + ' Connection accepted.');
  // send back chat history
  if (history.length > 0) {
    connection.sendUTF(
        JSON.stringify({ type: 'history', data: history} ));
  }
  // user sent some message
  connection.on('message', function(message) {
    if (message.type === 'utf8') { // accept only text
    // first message sent by user is their name
     if (userName === false) {
        // remember user name
        userName = htmlEntities(message.utf8Data);
        // get random color and send it back to the user
        userColor = colors.shift();
        connection.sendUTF(
            JSON.stringify({ type:'color', data: userColor }));
        console.log((new Date()) + ' User is known as: ' + userName
                    + ' with ' + userColor + ' color.');
      } else { // log and broadcast the message
        console.log((new Date()) + ' Received Message from '
                    + userName + ': ' + message.utf8Data);
        
        // we want to keep history of all sent messages
        var obj = {
          time: (new Date()).getTime(),
          text: htmlEntities(message.utf8Data),
          author: userName,
          color: userColor
        };
        history.push(obj);
        history = history.slice(-100);
        // broadcast message to all connected clients
        var json = JSON.stringify({ type:'message', data: obj });
        for (var i=0; i < clients.length; i++) {
          clients[i].sendUTF(json);
        }
      }
    }
  });
  // user disconnected
  connection.on('close', function(connection) {
    if (userName !== false && userColor !== false) {
      console.log((new Date()) + " Peer "
          + connection.remoteAddress + " disconnected.");
      // remove user from the list of connected clients
      clients.splice(index, 1);
      // push back user's color to be reused by another user
      colors.push(userColor);
    }
  });
});

Đó là toàn bộ code của phía Server, tôi đã thêm một số comment bằng tiếng Anh, bạn hãy chịu khó đọc và dịch, sẽ tốt hơn cho vốn tiếng Anh của bạn đó.

Frontend application

Hãy tạo mới một file Javascript và import vào trong file HTML đã tạo ở trên. Sau đó thêm đoạn code dưới đây vào:

$(function () {
  // if user is running mozilla then use it's built-in WebSocket
  window.WebSocket = window.WebSocket || window.MozWebSocket;

  var connection = new WebSocket('ws://127.0.0.1:1337');

  connection.onopen = function () {
    // connection is opened and ready to use
  };

  connection.onerror = function (error) {
    // an error occurred when sending/receiving data
  };

  connection.onmessage = function (message) {
    // try to decode json (I assume that each message
    // from server is json)
    try {
      var json = JSON.parse(message.data);
    } catch (e) {
      console.log('This doesn\'t look like a valid JSON: ',
          message.data);
      return;
    }
    // handle incoming message
  };
});

Đó là cái sườn để bạn có thể hình dung các phương thức sẽ hoạt động như thế nào. Giờ hãy cùng sửa lại với bộ code chuẩn:

$(function () {
  "use strict";
  // for better performance - to avoid searching in DOM
  var content = $('#content');
  var input = $('#input');
  var status = $('#status');
  // my color assigned by the server
  var myColor = false;
  // my name sent to the server
  var myName = false;
  // if user is running mozilla then use it's built-in WebSocket
  window.WebSocket = window.WebSocket || window.MozWebSocket;
  // if browser doesn't support WebSocket, just show
  // some notification and exit
  if (!window.WebSocket) {
    content.html($('<p>',
      { text:'Sorry, but your browser doesn\'t support WebSocket.'}
    ));
    input.hide();
    $('span').hide();
    return;
  }
  // open connection
  var connection = new WebSocket('ws://127.0.0.1:1337');
  connection.onopen = function () {
    // first we want users to enter their names
    input.removeAttr('disabled');
    status.text('Choose name:');
  };
  connection.onerror = function (error) {
    // just in there were some problems with connection...
    content.html($('<p>', {
      text: 'Sorry, but there\'s some problem with your '
         + 'connection or the server is down.'
    }));
  };
  // most important part - incoming messages
  connection.onmessage = function (message) {
    // try to parse JSON message. Because we know that the server
    // always returns JSON this should work without any problem but
    // we should make sure that the massage is not chunked or
    // otherwise damaged.
    try {
      var json = JSON.parse(message.data);
    } catch (e) {
      console.log('Invalid JSON: ', message.data);
      return;
    }
    // NOTE: if you're not sure about the JSON structure
    // check the server source code above
    // first response from the server with user's color
    if (json.type === 'color') { 
      myColor = json.data;
      status.text(myName + ': ').css('color', myColor);
      input.removeAttr('disabled').focus();
      // from now user can start sending messages
    } else if (json.type === 'history') { // entire message history
      // insert every single message to the chat window
      for (var i=0; i < json.data.length; i++) {
      addMessage(json.data[i].author, json.data[i].text,
          json.data[i].color, new Date(json.data[i].time));
      }
    } else if (json.type === 'message') { // it's a single message
      // let the user write another message
      input.removeAttr('disabled'); 
      addMessage(json.data.author, json.data.text,
                 json.data.color, new Date(json.data.time));
    } else {
      console.log('Hmm..., I\'ve never seen JSON like this:', json);
    }
  };
  /**
   * Send message when user presses Enter key
   */
  input.keydown(function(e) {
    if (e.keyCode === 13) {
      var msg = $(this).val();
      if (!msg) {
        return;
      }
      // send the message as an ordinary text
      connection.send(msg);
      $(this).val('');
      // disable the input field to make the user wait until server
      // sends back response
      input.attr('disabled', 'disabled');
      // we know that the first message sent from a user their name
      if (myName === false) {
        myName = msg;
      }
    }
  });
  /**
   * This method is optional. If the server wasn't able to
   * respond to the in 3 seconds then show some error message 
   * to notify the user that something is wrong.
   */
  setInterval(function() {
    if (connection.readyState !== 1) {
      status.text('Error');
      input.attr('disabled', 'disabled').val(
          'Unable to communicate with the WebSocket server.');
    }
  }, 3000);
  /**
   * Add message to the chat window
   */
  function addMessage(author, message, color, dt) {
    content.prepend('<p><span style="color:' + color + '">'
        + author + '</span> @ ' + (dt.getHours() < 10 ? '0'
        + dt.getHours() : dt.getHours()) + ':'
        + (dt.getMinutes() < 10
          ? '0' + dt.getMinutes() : dt.getMinutes())
        + ': ' + message + '</p>');
  }
});

Cơ bản vậy là xong rồi đó. Giờ là đến màn chạy thử:

node chat-server.js

Nếu chạy đúng, dòng dưới đây sẽ hiển thị trên cửa sổ console:

Wed Jul 20 2018 09:15:44 GMT+7  Server is listening on port 1337

Giờ bạn có thể mở file HTML khi nãy và thực hiện chat thử, mỗi một cửa sổ trình duyệt mở vào file HTML đó sẽ được coi như một người dùng khác. Hy vọng bài viết này sẽ giúp ích cho bạn trong tương lai!

Nguồn tham khảo:

https://medium.com/@martin.sikora/node-js-websocket-simple-chat-tutorial-2def3a841b61


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.