+4

Tìm hiểu Websocket và xây dựng ứng dụng bidirectional chat

1. Websocket là gì?

WebSoket là công nghệ hỗ trợ giao tiếp hai chiều giữa client và server bằng cách sử dụng một TCP socket để tạo một kết nối hiệu quả và ít tốn kém. Mặc dù được thiết kế để chuyên sử dụng cho các ứng dụng web, lập trình viên vẫn có thể đưa chúng vào bất kì loại ứng dụng nào.

  • Ưu điểm

WebSockets cung cấp khả năng giao tiếp hai chiều mạnh mẽ, có độ trễ thấp và dễ xử lý lỗi. Không cần phải có nhiều kết nối như phương pháp Comet long-polling và cũng không có những nhược điểm như Comet streaming. API cũng rất dễ sử dụng trực tiếp mà không cần bất kỳ các tầng bổ sung nào, so với Comet, thường đòi hỏi một thư viện tốt để xử lý kết nối lại, thời gian chờ timeout, các Ajax request (yêu cầu Ajax), các tin báo nhận và các dạng truyền tải tùy chọn khác nhau (Ajax long-polling và jsonp polling).

  • Nhược điểm

Những nhược điểm của WebSockets gồm có:

Không có phạm vi yêu cầu nào. Do WebSocket là một TCP socket chứ không phải là HTTP request, nên không dễ sử dụng các dịch vụ có phạm vi-yêu cầu, như SessionInViewFilter của Hibernate. Hibernate là một framework kinh điển cung cấp một bộ lọc xung quanh một HTTP request. Khi bắt đầu một request, nó sẽ thiết lập một contest (chứa các transaction và liên kết JDBC) được ràng buộc với luồng request. Khi request đó kết thúc, bộ lọc hủy bỏ contest này.

2. Node.js là gì?

Node.js là một hệ thống phần được thiết kế để viết các ứng dụng internet có khả năng mở rộng, đặc biệt là máy chủ web. Chương trình được viết bằng JavaScript, sử dụng kỹ thật điều khiển theo sự kiện, nhập/xuất không đồng bộ để tối thiểu tổng chi phí và tối đại khả năng mở rộng. Node.js bao gồm có V8 JavaScript engine của Google,libUV, và vài thư viện khác.

Lợi thế của Node.js để lập trình web-socket:

  • Thứ nhất: javascript là ngôn ngữ lập trình hướng sự kiện, mà trong lập trình thời gian thực, cách tiếp cận bằng lập trình sự kiện là cách tiếp cận khôn ngoan nhất.
  • Thứ hai: Node.js chạy non-blocking việc hệ thống không phải tạm ngừng để xử lý xong một request sẽ giúp cho server trả lời client gần như ngay tức thì.
  • Thứ ba: lập trình socket yêu cầu bạn phải xây dựng được mô hình lắng nghe – trả lời từ cả 2 bên. Nói khác đi, vai trò của client và server phải tương đương nhau, mà client thì chạy bằng javascript, nên nếu server cũng chạy bằng javascript nữa, thì việc lập trình sẽ dễ dàng và thân thiện hơn.

Giao thức bắt tay của WebSocket: socket_handshake.gif

3. Cài đặt công cụ

Bạn cài đặt NodeJs theo thứ tự các dòng lệnh sau:

sudo apt-get update
sudo apt-get install python-software-properties python g++ make
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs

Tiếp đó, bạn cài đặt thêm node-uuid (sẽ giới thiệu kỹ hơn về UUID ở phần sauu) theo lệnh:

npm install node-uuid

4. WebSocket Events

WebSocket hỗ trợ bốn sự kiện, chúng đều đã có sẵn trong JavaScript API và được xác định theo W3C: • open • message • error • close

Với JavaScript, you listen for these events to fire either with the handler on<event name> , or the addEventListener() method. Your code will provide a callback that will execute every time that event gets fired.

4.1 Event: Open

Khi WebSocket server phản hồi với yêu cầu kết nối, và quy trình bắt tay hoàn thành, sự kết nối giữa client và server được thiết lập. Khi đó, server đã sẵn sàng để gửi và nhận messages từ ứng dụng client.

// WebSocket connection established
ws.onopen = function(e) {
    console.log("Connection established");
    ws.send(JSON.stringify(stock_request));
};

Ví dụ trên thể hiện việc client hiển thị trạng thái kết nối thành công ra màn hình, kết nối được thiết lập, sẵn sàng cho việc trao đổi thông tin hai chiều. Sau đó client đã gửi một thông điệp đầu tiên lên server.

4.2 Event: Message

Sau khi kết nối thành công với websocket server, client sẽ có thể gửi và nhận thông điệp từ server (sử dụng các Websocket methods sẽ được đề cập phía sau bài viết này). Các WebSocket API sẽ chuẩn bị các thông điệp đầy đủ để được xử lý trong onmessage.

4.3 Event: Error

Khi có lỗi xảy ra với bất kỳ lý do gì. Giả sử rằng sự kết nối WebSocket bị đóng. Khi đó event close xảy ra ngay sau khi một trong các lỗi xuất hiện. Sau đây là một ví dụ về cách xử lý trong trường hợp xảy ra lỗi :

ws.onerror = function(e) {
    console.log("WebSocket failure, error", e);
    handleErrors(e);
};

4.4 Event: Close

Sự kiên close được gọi khi kết nối WebSocket đóng, và các sự kiện onerror sẽ được thực thi. Sau khi sự kết nối được đóng, sự giao tiếp giữa server và client cũng sẽ không đưuọc tiếp tục. Ví dụ sau sẽ trả về mảng giá trị 0 khi kết nối đã được đóng.

ws.onclose = function(e) {
    console.log(e.reason + " " + e.code);
    for(var symbol in stocks) {
        if(stocks.hasOwnProperty(symbol)) {
        	stocks[symbol] = 0;
    	}
    }
}
ws.close(1000, 'WebSocket connection closed')

5. WebSocket Methods

Các phương thức của WebSocket khá đơn giản, chỉ gồm hai phương thức: send()close().

5.1 Phương thức: Send

Khi sự kết nối được thiết lập, bạn sẽ sẵn sàng để nhận (gửi) thông điệp từ (đến) WebSocket server.

var ws = new WebSocket("ws://localhost:8181");
ws.onopen = function(e) {
	ws.send(JSON.stringify(stock_request));
}

5.2 Phương thức: Close

Sự kết nối WebSocket được đóng thông qua phương thức close(). Sau khi phương thức close() được gọi, sẽ không còn bất cứ dữ liệu nào được trao đổi. Ví dụ:

// Close WebSocket connection
ws.close();
// Close the WebSocket connection with reason.
ws.close(1000, "Goodbye, World!");

6. Xây dựng ứng dụng chat

6.1 Server

Trong phần này chúng ta sẽ xây dựng môt ứng dụng chat hoàn chỉnh với chức năng đơn giản hoàn chỉnh. Trước tiên, chúng ta khởi tạo một WebSocket server với cổng 8181.

var WebSocketServer = require('ws').Server,
	wss = new WebSocketServer({port: 8181});

Giao thức websocket thông thưởng không hỗ trợ các tính năng mặc định, chúng ta phải tự tạo ra chúng. Sau này khi làm việc với Socket.IO thì sẽ có nhiều API để thuận tiện hơn trong khi xây dựng ứng dụng.

Tiếp đó, chúng ta cần nhập vào một module Node để có thể sinh ra một UUID (UUID dung để xác định từng client đã kết nối với server và đưa chúng vào một một sưu tập).

var uuid = require("node-uuid");
var clients = [];
wss.on('connection', function(ws) {
    var client_uuid = uuid.v4();
    clients.push({"id": client_uuid, "ws": ws});
    console.log('client [%s] connected', client_uuid);

Khi server nhận được thông điệp từ client, nó sẽ duyệt lần lượt qua từng client trong bộ sưu tập đã tạo bên trên, và gửi lại một đối tượng JSON chứa thông điệp đã nhận được từ client đã gửi đi. Trên giao diện của các client sẽ chỉ cập nhật lại các thông điệp khi nó đã nhận được từ server. Từ đây, tất cả các client đã kết nối thành công tới server đều có thể nhận được thông điệp trên và trả về cho người dùng:

ws.on('message', function(message) {
    for(var i=0; i<clients.length; i++) {
        var clientSocket = clients[i].ws;
        if(clientSocket.readyState === WebSocket.OPEN) {
            console.log('client [%s]: %s', clients[i].id, message);
            clientSocket.send(JSON.stringify({
                "id": client_uuid,
                "message": message
            }));
        }
    }
});

Cuối cùng server cần xử lý sự kiện đóng kết nối:

ws.on('close', function() {
    for(var i=0; i<clients.length; i++) {
        if(clients[i].id == client_uuid) {
            console.log('client [%s] disconnected', client_uuid);
            clients.splice(i, 1);
        }
    }
});

6.2 WebSocket Client

Client nhận thông điệp từ server ở dạng một đối tượng JSON. Sử dụng hàm dựng sẵn để phân tích thông điệp đó để thể hiện lên giao diện người dùng.

ws.onmessage = function(e) {
    var data = JSON.parse(e.data);
    var messages = document.getElementById('messages');
    var message = document.createElement("li");
    message.innerHTML = data.message;
    messages.appendChild(message);
}

Sự kiện và thông báo

Chúng ta có thể gửi thông báo tới tất cả các client đã kết nối, xử lý trạng thái kết nối:

wss.on('connection', function(ws) {
    ...
    wsSend("message", client_uuid, nickname, message);
    ...
});

Việc gửi thông điệp tới tất cả các client về trạng thái kết nối, ngắt kết nối, hay bất kỳ thông báo nào cũng dựa trên cơ chế tương tự khi server gửi đi thông điệp nhận được từ một client tới tất cả các client đã kết nối.

Sau đây là phần code khá hoàn thiện cho một ứng dụng chat nho nhỏ giúp các bạn tham khảo dễ dàng hơn:

Phía Server

var WebSocket = require('ws');
var WebSocketServer = WebSocket.Server,
    wss = new WebSocketServer({port: 8181});
var uuid = require('node-uuid');

var clients = [];

var clientIndex = 1;

wss.on('connection', function(ws) {
  var client_uuid = uuid.v4();
  var nickname = "AnonymousUser" + clientIndex;
  clientIndex+=1;
  clients.push({"id": client_uuid, "ws": ws, "nickname": nickname});
  console.log('client [%s] connected', client_uuid);
  ws.on('message', function(message) {
    if(message.indexOf('/nick') == 0) {
      var nickname_array = message.split(' ')
      if(nickname_array.length >= 2) {
        var old_nickname = nickname;
        nickname = nickname_array[1];
        for(var i=0; i<clients.length; i++) {
          var clientSocket = clients[i].ws;
          var nickname_message = "Client " + old_nickname + " changed to " + nickname;
          clientSocket.send(JSON.stringify({
            "id": client_uuid,
            "nickname": nickname,
            "message": nickname_message
          }));
        }
      }
    } else {
      for(var i=0; i<clients.length; i++) {
          var clientSocket = clients[i].ws;
          if(clientSocket.readyState === WebSocket.OPEN) {
              console.log('client [%s]: %s', clients[i].id, message);
              clientSocket.send(JSON.stringify({
                  "id": client_uuid,
                  "nickname": nickname,
                  "message": message
              }));
          }
      }
    }
  });

  ws.on('close', function() {
    for(var i=0; i<clients.length; i++) {
        if(clients[i].id == client_uuid) {
            console.log('client [%s] disconnected', client_uuid);
            clients.splice(i, 1);
        }
    }
  });
});

Phía Client

<!DOCTYPE html>
<html lang="en">
<head>
<title>Bi-directional WebSocket Chat Demo</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script>
        var ws = new WebSocket("ws://localhost:8181");
        var nickname = "";
        ws.onopen = function(e) {
          console.log('Connection to server opened');
        }
        function appendLog(nickname, message) {
          var messages = document.getElementById('messages');
          var messageElem = document.createElement("li");
          var preface_label;
          if(nickname==='*') {
              preface_label = "<span class=\"label label-info\">*</span>";
          } else {
              preface_label = "<span class=\"label label-success\">" + nickname + "</span>";
          }
          var message_text = "<h2>" + preface_label + "  " + message + "</h2>";
          messageElem.innerHTML = message_text;
          messages.appendChild(messageElem);
        }
        ws.onmessage = function(e) {
          var data = JSON.parse(e.data);
          nickname = data.nickname;
          appendLog(data.nickname, data.message);
          console.log("ID: [%s] = %s", data.id, data.message);
        }
        ws.onclose = function(e) {
          appendLog("*", "Connection closed");
          console.log("Connection closed");
        }
        function sendMessage() {
          var messageField = document.getElementById('message');
           if(ws.readyState === WebSocket.OPEN) {
               ws.send(messageField.value);
           }
           messageField.value = '';
           messageField.focus();
        }
        function disconnect() {
          ws.close();
        }
    </script>
</head>
<body lang="en">
    <div class="vertical-center">
    <div class="container">
    <ul id="messages" class="list-unstyled">

    </ul>
    <hr />
    <form role="form" id="chat_form" onsubmit="sendMessage(); return false;">
        <div class="form-group">
        <input class="form-control" type="text" name="message" id="message"
          placeholder="Type text to echo in here" value="" autofocus/>
        </div>
        <button type="button" id="send" class="btn btn-primary" onclick="sendMessage();">Send Message</button>
  </form>
  </div>
  </div>
</body>
</html>

Bạn cũng có thể tải code về tại: GitHub

Chúng ta sẽ tiếp tục với WebSocket trong các bài viết sau. Hẹn gặp lại các bạn!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí