+1

Ruby - WebSockets (Phần 2)

Tài liệu: Building a simple websockets server from scratch in Ruby

Nội dung bài viết: Xây dựng một ứng dụng đơn giản về websockets server trong Ruby

Xây dựng một ứng dụng đơn giản về websockets server trong Ruby

Ngày nay websockets ngày càng được sử dụng nhiều, chúng ta đã từng nghe thấy rằng websockets là một công nghệ trong tương lai và chúng dễ dàng sử dụng hơn bao giờ hết nhờ vào ActionCable trong Rails 5. Nhưng chính xác thì websockets là gì? và chúng làm việc như thế nào?

Trong bài viết này chúng tôi sẽ đi trả lời cho các câu hỏi đó thông qua việc xây dựng một ứng dụng đơn giản về websockets server trong Ruby. Khi kết thúc bài viết chúng ta sẽ hiểu thêm về giao tiếp bi-directional(theo hai hướng) giữa browser và server.

Code trong bài viết là một bài tập mẫu đề tìm hiều học tập. Trong các ứng dụng thực tế nếu bạn muốn cài đặt websockets thì trong ruby có hỗ trợ gem websocket-ruby hoặc bạn có thể tìm kiếm ở WebSocket Spec

Nếu bạn chưa bao giờ nghe về websockets:

Web sockets được ra đời đề giải quyết một số vấn đề tồn đọng trong các kết nối HTTP đó là khi bạn request một webpage sử dụng kết nối HTTP thông thường thì server sẽ gửi trả một nội dung sau đó đóng kết nối. Nếu bạn request tới webpage khác, bạn sẽ tạo ra một kết nối khác. Bình thường nó làm việc tốt, nhưng nó không phải là các tiếp cận tốt nhất cho một số trường hợp:

Cho một số ứng dụng, giống như là chat front-end cần update messages mới đến nhanh nhất có thể. Nếu bạn sử dụng các request HTTP thì bạn phải liên tục poll server để nhìn thấy messages mới nhất.

Trong ứng dụng front-end của bạn cần gửi nhiều request tới server. Việc tạo ra kết nối mới mỗi lần request ảnh hưởng tới vấn đề hiệu xuất. Điều này đã được giảm trong HTTP2.

Với web sockets, bạn có thể tạo ra một kết nối tới server và sau đó kết nối mở để sử dụng cho giao tiếp hai chiều(bi-directional).

Client Side

Web sockets được sử dụng để giao tiếp giữa browser và web server. phía browser được cài đặt là JavaScript.

Trong ví dụ bên dưới tôi viết một đoạn code JavaScript để mở một web socket tới local server và gửi đi một message tới nó.

<!doctype html>
<html lang="en">
<head>
  <title>Websocket Client</title>
</head>
<body>
  <script>
    var exampleSocket = new WebSocket("ws://localhost:2345");
    exampleSocket.onopen = function (event) {
      exampleSocket.send("Can you hear me?");
    };
    exampleSocket.onmessage = function (event) {
      console.log(event.data);
    }
  </script>
</body>
</html>

Nếu mở file này trong trình duyệt web thì sẽ thấy lỗi. Điều đó có nghĩa là bởi vì ở đó không có server nào cả. Chúng ta phải đi xây dựng server cho nó.

Bắt đầu xây dựng server với ruby

Vòng đời của websockets: Browser sẽ gửi một HTTP request với phần headers là một chuỗi "please make me a websocket."

Server sẽ trả lời với một HTTP response, nhưng không đóng kết nối.

Browser và Server sử dụng một giao thức websocket cụ thể để trao đổi các frame dữ liệu trên kết nối mở.

Bước đầu tiên chúng ta sẽ xây dựng một webserver. Trong code ở phía dưới, tôi tạo ra một webserver đơn giản nhất. Nó không thực sự phục vụ bất cứ thứ gì nó chỉ đơn giản là đợi request sau đó in nó tới STDERR.

require 'socket'

server = TCPServer.new('localhost', 2345)

loop do

  # Wait for a connection
  socket = server.accept
  STDERR.puts "Incoming Request"

  # Read the HTTP request. We know it's finished when we see a line with nothing but \r\n
  http_request = ""
  while (line = socket.gets) && (line != "\r\n")
    http_request += line
  end
  STDERR.puts http_request
  socket.close
end

Nếu chạy server và refresh trang websockets testing của tôi, tôi sẽ có điều này

$ ruby server1.rb
Incoming Request
GET / HTTP/1.1
Host: localhost:2345
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: cG8zEwcrcLnEftn2qohdKQ==

Bạn sẽ nhận thấy HTTP request có một nhóm các headers liên quan tới web sockets. Đây là bước đầu tiên trong websocket handshake.

The handshake

Tất cả các request của web sockets đều bắt đầu với handshake. Điều này để đảm bảo rằng cả server và client đều biết một web sockets đang hoạt động và chúng đều đồng ý cùng một version của giao thức. Nó làm việc giống như điều này:

Client gửi một HTTP request như sau:

GET / HTTP/1.1
Host: localhost:2345
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: E4i4gDQc1XTIQcQxvf+ODA==
Sec-WebSocket-Version: 13

=> phần quan trọng nhất của request này là Sec-WebSocket-Key. Client mong đợi server trả về một gía trị đã được chỉnh sửa của Sec-WebSocket-Key để ngăn chặn tấn công XSS và caching proxies.

Server responds

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: d9WHst60HtB4IvjOVevrexl0oLA=

=> Server responds là một boilerplate ngoại trừ cho header Sec-WebSocket-Accept. Header này được phát sinh giống như sau:

# Take the value provided by the client, append a magic
# string to it. Generate the SHA1 hash, then base64 encode it.
Digest::SHA1.base64digest([sec_websocket_accept, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)

Implementing the handshake

Thêm phần code sau đến server để hòan thành handshake. Đầu tiên, chúng ta lấy ra token bảo mật của request headers.

# Grab the security key from the headers.
# If one isn't present, close the connection.
if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
  websocket_key = matches[1]
  STDERR.puts "Websocket handshake detected with key: #{ websocket_key }"
else
  STDERR.puts "Aborting non-websocket connection"
  socket.close
  next
end

Bây giờ chúng ta sẽ sử dụng key bảo mật đó để phát sinh ra một chuỗi hợp lệ trả về.

response_key = Digest::SHA1.base64digest([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
STDERR.puts "Responding to handshake with key: #{ response_key }"

socket.write <<-eos
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: #{ response_key }

eos

STDERR.puts "Handshake completed."

Khi tôi refresh trang websockets testing của tôi, thì lúc này không còn nhìn thấy lỗi connection. Connection đã được thiết lập.

Và đây là output từ server, chỉ ra key bảo mật và trả về key

$ ruby server2.rb
Incoming Request
Websocket handshake detected with key: Fh06+WnoTQQiVnX5saeYMg==
Responding to handshake with key: nJg1c2upAHixOmXz7kV2bJ2g/YQ=
Handshake completed.

The websocket frame protocol

Một kết nối websocket được thiết lập, HTTP không còn được sử dụng. Thay vào đó dữ liệu được trao đổi thông qua giao thức websocket.

Frames là đơn vị cơ bản của giao thức Websocket.

Giao thức Websocket được dựa trên frame. Nhưng điều đó nghĩa là gì?

Bất cứ khi nào bạn hỏi trình duyệt web của mình gửi dữ liệu trên WebSocket hay hỏi server trả về dữ liệu, dữ liệu được chia thành các phận nhỏ nhất(chunks) được đóng gói trong metadata tạo nên frame.

Dưới đây chỉ ra cấu trúc frame trông như thế nào. Những con số trên cùng là bits. Và một số các trường giống như là mở rộng chiều dài của phần tải dữ liệu có thể không hiển thị.

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

Bạn nhìn thấy giao thức nhị phân. Chúng ta sẽ làm một số thao với bit, nhưng không qúa lo lắng - nó không qúa khó. Các con số trên cùng là các bit. Và một số trường có thể không hiển thị. Cho ví dụ là extended payload length sẽ hiển thị nếu payload dưới 127 bytes.

Receiving data Bây giờ handshake đã hoàn thành, chúng ta bắt đầu phân tích frame nhị phân. Để giữ cho mọi thứ đơn giản, chúng ta tập chung đến mỗi frame 1 byte ỏ tại một thời điểm. Sau đó, chúng ta đặt tất cả chúng cùng nhau để nhìn thấy nó trong một hành động.

Byte 1: FIN and OpCode Từ bảng bên trên, bạn có thể nhìn thấy byte đầu tiên( tám bít đầu tiên) chứa một ít dữ liệu.

FIN: 1 bit nếu là false, sau đó messages được chia thành nhiều frame

OpCode: 4bits điều đó có nghĩa là payload là text, binary, hay nó chỉ là một cái ping để duy trì kết nối.

RSV: 3 bits không được sử dụng trong websockets spec hiện tại.

Đề lấy byte đầu tiên, chúng ta sử dụng phương thức IO#getbyte. và xuất ra dữ liệu, chúng ta sử dụng một bitmasking đơn giản.

first_byte = socket.getbyte
fin = first_byte & 0b10000000
opcode = first_byte & 0b00001111

# Our server will only support single-frame, text messages.
# Raise an exception if the client tries to send anything else.
raise "We don't support continuations" unless fin
raise "We only support opcode 1" unless opcode == 1

Byte 2: MASK and payload length Byte 2 của frame chứa nhiều thông tin về payload(phần tải dữ liệu).

MASK: 1 bit, Boolean flag được xác định nếu payload là masked đi. Nếu nó true thì payload là unmasked trước khi dùng. Điều này luôn luôn đúng đối

payload length: 7 bits, nếu payload nhỏ hơn 126 bytes, chiều dài được lưu trữ ở đây. Nếu gía trị lớn hơn 126, nó có nghĩa là nhiều bytes sẽ được cung cấp.

Đây là cách chúng tôi xử lý byte thứ hai:

second_byte = socket.getbyte
is_masked = second_byte & 0b10000000
payload_size = second_byte & 0b01111111

raise "All frames sent to a server should be masked according to the websocket spec" unless is_masked
raise "We only support payloads < 126 bytes in length" unless payload_size < 126

STDERR.puts "Payload size: #{ payload_size } bytes"

Bytes 3-7: The masking key Chúng ta mong đợi payload của tất cả frame đến sẽ là masked. Để unmask nội dung, chúng ta sẽ phải XOR nó dựa trên masking key.

Masking key làm ra 4 bytes tiếp. Chúng ta không cần xử lý nó, chúng ta chỉ cần đọc các bytes thành một mảng.

mask = 4.times.map { socket.getbyte }
STDERR.puts "Got mask: #{ mask.inspect }"

Bytes 8 and up: The payload chúng ta đã hoàn thành với metadata. Bây giờ có thề lẩy ra payload thực.

data = payload_size.times.map { socket.getbyte }
STDERR.puts "Got masked data: #{ data.inspect }"

Nhớ rằng payload là masked, bởi vậy nếu in nó ra, nó sẽ trông giống là rác. Để unmask nó, chúng ta đơn giản là dùng XOR mỗi byte với byte tương ứng của mask. Bởi vì mask chỉ dài có 4 bytes, chúng ta sẽ lặp nó phù hợp với chiều dài của payload:

unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] }
STDERR.puts "Unmasked the data: #{ unmasked_data.inspect }"

Bây giờ chúng ta đã có một mảng các byte. Chúng ta cần chuyển nó thành chuỗi UNICODE. Tất cả các text trong WebSocket là UNICODE.

STDERR.puts "Converted to a string: #{ unmasked_data.pack('C*').force_encoding('utf-8').inspect }"

Đặt tất cả Code cùng nhau

require 'socket' # Provides TCPServer and TCPSocket classes
require 'digest/sha1'

server = TCPServer.new('localhost', 2345)

loop do

  # Wait for a connection
  socket = server.accept
  STDERR.puts "Incoming Request"

  # Read the HTTP request. We know it's finished when we see a line with nothing but \r\n
  http_request = ""
  while (line = socket.gets) && (line != "\r\n")
    http_request += line
  end

  # Grab the security key from the headers. If one isn't present, close the connection.
  if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
    websocket_key = matches[1]
    STDERR.puts "Websocket handshake detected with key: #{ websocket_key }"
  else
    STDERR.puts "Aborting non-websocket connection"
    socket.close
    next
  end

  response_key = Digest::SHA1.base64digest([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
  STDERR.puts "Responding to handshake with key: #{ response_key }"

  socket.write <<-eos
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: #{ response_key }

  eos

  STDERR.puts "Handshake completed. Starting to parse the websocket frame."

  first_byte = socket.getbyte
  fin = first_byte & 0b10000000
  opcode = first_byte & 0b00001111

  raise "We don't support continuations" unless fin
  raise "We only support opcode 1" unless opcode == 1

  second_byte = socket.getbyte
  is_masked = second_byte & 0b10000000
  payload_size = second_byte & 0b01111111

  raise "All incoming frames should be masked according to the websocket spec" unless is_masked
  raise "We only support payloads < 126 bytes in length" unless payload_size < 126

  STDERR.puts "Payload size: #{ payload_size } bytes"

  mask = 4.times.map { socket.getbyte }
  STDERR.puts "Got mask: #{ mask.inspect }"

  data = payload_size.times.map { socket.getbyte }
  STDERR.puts "Got masked data: #{ data.inspect }"

  unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] }
  STDERR.puts "Unmasked the data: #{ unmasked_data.inspect }"

  STDERR.puts "Converted to a string: #{ unmasked_data.pack('C*').force_encoding('utf-8').inspect }"

  socket.close
end

Bây giờ khi refresh WebSocket testing webpage của tôi, nó sẽ tạo một request tới server, Ở đây là output mà chúng ta nhìn thấy:

$ ruby websocket_server.rb
Incoming Request
Websocket handshake detected with key: E4i4gDQc1XTIQcQxvf+ODA==
Responding to handshake with key: d9WHst60HtB4IvjOVevrexl0oLA=
Handshake completed. Starting to parse the websocket frame.
Payload size: 16 bytes
Got mask: [80, 191, 161, 254]
Got masked data: [19, 222, 207, 222, 41, 208, 212, 222, 56, 218, 192, 140, 112, 210, 196, 193]
Unmasked the data: [67, 97, 110, 32, 121, 111, 117, 32, 104, 101, 97, 114, 32, 109, 101, 63]
Converted to a string: "Can you hear me?"
Gửi dữ liệu trở lại Client

Chúng ta đã gửi thành công một message từ client của chúng ta tới server qua WebSocket. Bây giờ chúng ta sẽ gửi message quay trở lại client từ server.

Frame được gửi từ server tới client luôn luôn unmasked.

Giống như là chúng ta tiêu thụ một byte frame ở một thời điểm, chúng ta đi xây dựng một byte ở một thời điểm.

Byte 1: FIN and OPCODE

Dự liệu được tải vào trong một frame ở một thời điểm và nó là text. Điều đó có nghĩa là FIN bằng 1, OPCOPE cũng bằng 1. Khi tôi nối chúng sử dụng định dạng bit tương tự mà chúng ta sử dụng trước đó, tôi có một số:

output = [0b10000001]
Byte 2: MASKED and payload length

Bởi vì frame này là đi từ server tới client, MASKED sẽ bằng 0. Điều này có nghĩa là chúng ta có thể bỏ qua nó. chiều dài của payload chỉ là chiều dài của chuỗi.

output = [0b10000001, response.size]
Bytes 3 and up: The payload

payload không phải là masked, nó chỉ là một chuỗi.

response = "Loud and clear!"
STDERR.puts "Sending response: #{ response.inspect }"
output = [0b10000001, response.size, response]

Chúng ta đã có một mảng chứa dữ liệu mà chúng ta muốn gửi. Chúng ta cần chuyển đổi mảng này thành chuỗi các byte mà chúng ta có thể gửi qua dây dẫn. Để làm điều này chúng ta sử dụng phương thức Array#pack.

socket.write output.pack("CCA#{ response.size }")

Chuỗi "CCA#{ response.size }" nói cho Array#pack biết rằng mảng này chứa 8 bit số nguyên không dấu theo sau bởi chuỗi ký tự của kích thước xác định.

Nếu tôi mở network inspector của chrome, tôi có thể nhìn thấy message được trả về từ server là: "Loud and clear!".

Bài đọc thêm

Đó là tất cả, Tôi hy vọng các bạn có thể học thêm được một số thứ về WebSockets.

Ờ đây còn có nhiều thứ về server mà chưa đề cập tới, nếu các bạn muốn tiếp tục thực hành, các bạn có thể tìm kiếm chúng:

Support for multi-frame payloads Binary payload support Ping / Pong support Long payload support Closing handshake


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í