Blog

16 Aug
Websocket
August 16, 2023

Websocket

   

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.

WebSockets mới xuất hiện trong HTML5, là một kỹ thuật Reverse Ajax. WebSockets cho phép các kênh giao tiếp song song hai chiều và hiện đã được hỗ trợ trong nhiều trình duyệt (Firefox, Google Chrome và Safari). Kết nối được mở thông qua một HTTP request (yêu cầu HTTP), được gọi là liên kết WebSockets với những header đặc biệt. Kết nối được duy trì để bạn có thể viết và nhận dữ liệu bằng JavaScript như khi bạn đang sử dụng một TCP socket đơn thuần.

Dữ liệu truyền tải thông qua giao thức HTTP (thường dùng với kĩ thuật Ajax) chứa nhiều dữ liệu không cần thiết trong phần header. Một header request/response của HTTP có kích thước khoảng 871 byte, trong khi với WebSocket, kích thước này chỉ là 2 byte (sau khi đã kết nối).

Giao thức handshake của WebSocket

Để thực hiện kết nối, client phải gửi một WebSocket handshake request đến server. Server sẽ gửi trả lại WebSocket handshake response.

Ư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ó:

  • Nó là một đặc tả mới của HTML5, nên nó vẫn chưa được tất cả các trình duyệt hỗ trợ.
  • 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.

Các ứng dụng của WebSocket

  • Ứng dụng chat Real-time: Websockets là nền tảng của các ứng dụng trò chuyện hiện đại, cho phép người dùng trao đổi tin nhắn trong thời gian thực mà không cần phải làm mới trang liên tục.
  • Collaborative Tools: Các ứng dụng đòi hỏi nhiều người dùng cộng tác trên một tài liệu hoặc dự án duy nhất có lợi từ Websockets. Những thay đổi được thực hiện bởi một người dùng có thể tức thì phản ánh trên tất cả các máy khách kết nối.
  • Game online: Các trò chơi trực tuyến đa người chơi dựa vào Websockets để cung cấp cho người chơi trải nghiệm chơi game được đồng bộ, đảm bảo rằng các hành động của một người chơi được thấy ngay lập tức bởi những người chơi khác.
  • Các Nền tảng Tài chính: Trong các nền tảng giao dịch tài chính, cập nhật thời gian thực về giá cổ phiếu, tỷ giá hối đoái và dữ liệu thị trường khác rất quan trọng. Websockets đảm bảo rằng những người giao dịch nhận được thông tin cập nhật đến từng giây mà không bị trễ.

Sơ đồ hoạt động của Websocket

Quy trình hoạt động của Websockets là một chuỗi các bước mà máy khách (client) và máy chủ (server) thực hiện để thiết lập và duy trì kết nối liên tục, cho phép truyền thông dữ liệu hai chiều thời gian thực. Dưới đây là quy trình hoạt động cơ bản của Websockets:

  1. Client sẽ gửi 1 Request yêu cầu kết nối TCP lên server và nhận được Response cho phép từ server
  2. Máy khách gửi một yêu cầu HTTP đặc biệt gọi là “WebSocket handshake request” đến máy chủ thông qua một URL cụ thể, chẳng hạn như ws://example.com/socket. Yêu cầu này chứa một số thông tin, bao gồm phiên bản Websockets được sử dụng và một mã ngẫu nhiên gọi là “Sec-WebSocket-Key”.
  3. Máy chủ nhận yêu cầu handshake, xác minh các thông tin và tạo ra một “WebSocket handshake response”.
  4. Máy chủ gửi lại phản hồi HTTP với mã trạng thái 101 (“Switching Protocols”) và các tiêu đề bổ sung như “Upgrade” và “Connection” để thông báo rằng kết nối đã được nâng cấp thành Websockets.

Truyền Tin nhắn:

  • Cả máy khách và máy chủ có thể gửi và nhận tin nhắn qua kết nối Websockets.
  • Tin nhắn Websockets thường được đóng gói trong các khung (frames) đặc biệt. Mỗi khung chứa dữ liệu cùng với các thông tin điều khiển như mã thao tác và mã xác định kiểu dữ liệu.

Đóng Kết nối:

  • Khi một bên muốn đóng kết nối, nó gửi một khung “close frame” để thông báo cho bên còn lại về việc đóng kết nối.
  • Bên nhận cũng sẽ gửi một khung close frame như phản hồi để xác nhận việc đóng kết nối.
  • Sau khi cả hai bên đã nhận được và xử lý khung close frame, kết nối Websockets sẽ được đóng.

Quy trình trên giúp thiết lập và duy trì một kết nối liên tục giữa máy khách và máy chủ, cho phép truyền thông dữ liệu hai chiều thời gian thực. Kết nối này có thể tồn tại trong thời gian dài và cho phép các ứng dụng web tương tác và cập nhật dữ liệu một cách nhanh chóng và linh hoạt.

Sử dụng thư viện `ws` để triển khai ứng dụng chat realtime trên NodeJS và ReactJS:

 

  1. Cài đặt thư viện
npm install ws
  1. Tạo một máy chủ WebSocket trong tệp index.js:
import { createServer } from "http";
import { parse } from "url";
import { WebSocketServer, WebSocket } from "ws";

const server = createServer();
const wss1 = new WebSocketServer({ noServer: true });

wss1.on("connection", function connection(ws) {
  ws.on("error", console.error);
  
  ws.on("open", function () {
    console.log("connection is established");
  });

  ws.on("message", function (data) {
    wss1.clients.forEach(function (client) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data, { binary: false });
      }
    });
  });
});
server.on("upgrade", function upgrade(request, socket, head) {
  const { pathname } = parse(request.url);
  if (pathname === "/chat") {
    wss1.handleUpgrade(request, socket, head, function done(ws) {
      wss1.emit("connection", ws, request);
    });
  } else {
    socket.destroy();
  }
});

console.log("Server is running");
server.listen(8080);

3. Thiết lập ws trên client

  • Tạo 1 file html để hiển thị trang nhắn tin
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.tailwindcss.com"></script>
</head>

<body>
  <body class="flex flex-col items-center justify-center w-screen min-h-screen bg-gray-100 text-gray-800 p-10">
    <!-- Component Start -->
    <div class="flex flex-col flex-grow w-full max-w-xl bg-white shadow-xl rounded-lg overflow-hidden">
      <div class="flex flex-col flex-grow h-0 p-4 overflow-auto" id="chatPanel">
      </div>
      <form action="" id="form">
        <div class="bg-gray-300 p-4 flex">
          <input id="input" class="flex items-center h-10 w-full rounded px-3 text-sm" type="text"
            placeholder="Type your message…">
          <button class="p-2 cursor-pointer" id="sendButton">

            <img src="./img/send-message.png" alt="" width="32" height="32">
          </button>
         </div>
       </form>
      </div>
      <!-- Component End  -->
    </body>
    <script src="./helper.js" type="module"></script>
    <script src="./socket.js" type="module"></script>
</body>

</html>
  • Tạo 1 file helper.js để chứa các component đoạn hội thoại
export function myMessage(user, message, sendAt) {
  return `
    <div class="flex w-full mt-2 space-x-3 max-w-xs ml-auto justify-end">
      <div>
        <div class="bg-blue-500 text-white p-3 rounded-l-lg rounded-br-lg">
          <p class="text-sm font-bold text-green-500">${user.name}</p>
          <p class="text-sm">${message}</p>
        </div>
        <span class="text-xs text-gray-500 leading-none">${getTime(sendAt)}</span>
      </div>
      <div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-300">
      </div>
    </div>
  `
}

export function guestMessage(user, message, sendAt) {
  return `
    <div class="flex w-full mt-2 space-x-3 max-w-xs">
      <div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-300">
      </div>
      <div>
        <div class="bg-gray-300 p-3 rounded-r-lg rounded-bl-lg">
          <p class="text-sm font-bold text-blue-500">${user.name}</p>
          <p class="text-sm">${message}</p>
        </div>
        <span class="text-xs text-gray-500 leading-none">${getTime(sendAt)}</span>
      </div>
    </div>
  `
}

export function getTime(time) {
  time = new Date(time)
  time = [time.getHours(), time.getMinutes(), time.getSeconds()].map((t) => String(t).padStart(2, 0)).join(':')
  return time
}
  • Tạo file socket.js để gửi request và nhận response từ server
import { myMessage, guestMessage } from "./helper.js";

const form = document.getElementById("form");
const input = document.getElementById("input");
const chatPanel = document.getElementById("chatPanel");
const ws = new WebSocket("ws://localhost:8080/chat");

const user = {
  name: "",
};

ws.onopen = function () {
  while (!user?.name) {
    user.name = prompt("Socket connected successfully\nEnter your name: ");
  }
};

ws.onmessage = function (event) {
  // console.log(new Date().getMilliseconds());
  const { user, message, sendAt } = JSON.parse(event.data);
  displayMessage(
    {
      name: user.name,
      isGuest: true,
    },
    message,
    sendAt
  );
};

ws.onclose = function () {
  alert("Websocket connection failed");
};

form.onsubmit = function (e) {
  e.preventDefault();
  // console.log(new Date().getMilliseconds());
  ws.send(
    JSON.stringify({
      user: {
        name: user.name,
      },
      message: input.value,
      sendAt: new Date(),
    })
  );

  displayMessage(
    {
      name: user.name,
      isGuest: false,
    },
    input.value,
    new Date()
  );
  input.value = "";
};

function displayMessage(user, message, sendAt) {
  chatPanel.innerHTML =
    chatPanel.innerHTML +
    (user.isGuest
      ? guestMessage(user, message, sendAt)
      : myMessage(user, message, sendAt));
}

_KhangTPD + AnLVT_