Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Tạo Ruby WebSocket Server Từ Con Số 0: Hướng Dẫn Bước Đi

Websockets đang nhận được nhiều press hơn ngày nay. Chúng ta nghe chúng là "tương lai." Chúng ta nghe chúng dễ dùng hơn bao giờ nhờ ActionCable trong Rails 5. Nhưng websockets chính xác LÀ gì? Chúng hoạt động như thế nào?

Trong bài này chúng ta sẽ trả lời các câu hỏi này bằng cách xây dựng một WebSocket server đơn giản từ con số 0 trong Ruby. Khi xong chúng ta sẽ đạt được bi-directional communication giữa browser và server.

Mã trong bài này dành cho mục đích learning exercise. Nếu bạn muốn implement websockets trong real production app, xem websocket-ruby gem tuyệt vời. Bạn cũng có thể xem WebSocket Spec.

Vậy bạn chưa bao giờ nghe về websockets

Web sockets được phát minh để giải quyết một số vấn đề inherent trong normal HTTP connections. Khi bạn request webpage dùng normal HTTP connection, server gửi content và sau đó close connection. Nếu bạn muốn request page khác, bạn phải tạo connection khác. Điều này normally works fine, nhưng nó không phải best approach cho một số use cases:

  • Với một số ứng dụng, như chat, front end cần được update ngay khi message mới đến. Nếu tất cả bạn có là normal HTTP requests, nghĩa là bạn phải continuously poll server để xem có content mới không.
  • Nếu front-end application cần làm nhiều small request đến server, overhead của việc tạo connection mới cho mỗi request có thể trở thành performance issue. Điều này ít là vấn đề trong HTTP2.

Với Web sockets, bạn tạo một connection đến server được hold open và dùng cho bidirectional communication.

Client side

Web sockets thường dùng cho communication giữa browser và Web server. Browser side được implement trong JavaScript. Trong ví dụ dưới tôi viết một piece JavaScript rất đơn giản để open Web socket đến local server và gửi message đến 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 tôi start up một static server nhỏ và mở file này trong web browser, tôi nhận được error. Điều đó hợp lý, vì chưa có server. Chúng ta vẫn phải build nó. :-)

Tạo Ruby WebSocket Server Từ Con Số 0: Hướng Dẫn Bước Đi

Bắt đầu server

Web socket bắt đầu đời như normal HTTP requests. Chúng có vòng đời hơi kỳ lạ:

  1. Browser gửi normal HTTP request, với một số special headers nói "please make me a websocket."
  2. Server reply với một HTTP response nhất định, nhưng KHÔNG CLOSE CONNECTION.
  3. Browser và server sau đó dùng special websocket protocol để exchange frames of data qua open connection.

Vì vậy step đầu tiên cho chúng ta là build Web server. Trong code dưới, tôi tạo simplest possible web server. Nó không actually serve bất cứ gì. Nó chỉ wait cho request rồi in nó ra STDERR.


require 'socket'
server = TCPServer.new('localhost', 2345)
loop do
  # Wait for a connection
  socket = server.accept
  STDERR.puts "Incoming Request"
  # Read HTTP request. Chúng ta biết nó xong khi thấy line chỉ có \r\n
  http_request = ""
  while (line = socket.gets) && (line != "\r\n")
    http_request += line
  end
  STDERR.puts http_request
  socket.close
end

Nếu tôi chạy server, và refresh websocket testing page, tôi nhận được:


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

Nếu bạn để ý, HTTP request này có một đống headers liên quan đến Web sockets. Đây thực sự là step đầu trong websocket handshake

Handshake

Tất cả Web socket requests bắt đầu với handshake. Điều này để chắc chắn cả client và server đều hiểu Web sockets sắp xảy ra và cả hai agree trên protocol version. Nó hoạt động như sau:

Client gửi HTTP request như này


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ề version modified của value này như proof against XSS attacks và caching proxies.

Server trả lời


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

Server response là boilerplate trừ Sec-WebSocket-Accept header. Header này được generate như sau:


# Lấy value từ client, append magic
# string. Generate SHA1 hash, sau đó base64 encode.
Digest::SHA1.base64digest([sec_websocket_accept, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)

Mắt bạn không nói dối. Có một magic constant involved.

Triển khai handshake

Hãy update server để complete handshake. Đầu tiên, pull security token từ request headers:


# Grab security key từ headers.
# Nếu không present, close 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ờ, dùng security key để generate valid response:


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 websocket test page, tôi thấy bây giờ không còn connection error. Connection đã established!

Tạo Ruby WebSocket Server Từ Con Số 0: Hướng Dẫn Bước Đi

Đây là output từ server, showing security keys và response key:


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

Websocket frame protocol

Một khi WebSocket connection established, HTTP không còn dùng. Thay vào đó, data được exchange qua WebSocket protocol.

Frames là đơn vị cơ bản của WebSocket protocol.

WebSocket protocol là frame-based. Nhưng điều đó nghĩa là gì?

Bất cứ khi nào bạn hỏi web browser gửi data qua WebSocket, hoặc hỏi server respond, data bị broken up thành một series of chunks trong mỗi chunk được wrapped trong một số metadata để làm frame.

Đây là giao diện frame structure. Các số trên top là bits. Và một số fields, như extended payload length có thể không luôn present:


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 ... |
+---------------------------------------------------------------+

Điều đầu tiên có thể nhảy ra mắt bạn là đây là binary protocol. Chúng ta sẽ phải làm bit manipulation, nhưng đừng lo — nó sẽ không khó. Các số trên top của figure là bits. Và một số fields có thể không luôn present. Ví dụ extended payload length sẽ present nếu payload dưới 127 bytes.

Nhận dữ liệu

Bây giờ handshake complete, chúng ta có thể bắt đầu parse binary frame. Để giữ đơn giản, chúng ta sẽ nhìn incoming frame một byte tại một thời điểm. Sau đó, chúng ta sẽ put nó all together để bạn thấy nó in action.

Byte 1: FIN và Opcode

Từ bảng trên, bạn thấy byte đầu (tám bit đầu) chứa vài mảnh data:

  • FIN: 1 bit Nếu false, message bị split vào multiple frames
  • opcode: 4 bits Cho biết payload là text, binary, hoặc chỉ "ping" để keep connection alive.
  • RSV: 3 bits Không dùng trong WebSockets spec hiện tại.

Để lấy byte đầu, chúng ta dùng method IO#getbyte. Và để extract data, chúng ta dùng bitmasking đơn giản. Nếu bạn không quen bitwise operators, xem bài khác của tôi Bitwise hacks in Ruby


first_byte = socket.getbyte
fin = first_byte & 0b10000000
opcode = first_byte & 0b00001111
# Server chỉ support single-frame, text messages.
# Raise exception nếu client thử gửi cái khác.
raise "We don't support continuations" unless fin
raise "We only support opcode 1" unless opcode == 1

Byte 2: MASK và payload length

Byte thứ hai của frame chứa thêm thông tin về payload.

  • MASK: 1 bit Boolean flag chỉ ra nếu payload bị masked. Nếu true, payload sẽ phải "unmasked" trước khi dùng. Điều này LUÔN nên true cho frames đến từ client. Spec nói vậy.
  • payload length: 7 bits Nếu payload dưới 126 bytes, length lưu ở đây. Nếu value lớn hơn 126, nghĩa là thêm bytes sẽ follow để cho length.

Đây là cách handle 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: Masking key

Chúng ta expect payloads của tất cả incoming frames sẽ bị masked. Để unmask content, chúng ta sẽ phải XOR nó against masking key.

Masking key này tạo nên bốn byte tiếp theo. Chúng ta không phải process nó, chỉ read bytes vào array.


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

Cho tôi biết nếu bạn biết cách đẹp hơn để read 4 bytes vào array. times.map hơi lạ, nhưng đó là approach ngắn gọn nhất tôi nghĩ ra. Tôi @StarrHorne trên twitter.

Bytes 8 trở lên: Payload

Ổn, xong metadata. Bây giờ có thể fetch actual payload.


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

Nhớ rằng payload này bị masked. Vì vậy nếu bạn print nó ra, nó sẽ trông như rác. Để unmask, chúng ta đơn giản XOR mỗi byte với corresponding byte của mask. Vì mask chỉ bốn bytes dài, chúng ta repeat nó để match length 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ó array của bytes. Chúng ta cần convert đó thành unicode string. Tất cả text trong Websockets là unicode.


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

Đặt hết vào nhau

Khi bạn put all code này together, bạn nhận được script trông như sau:


require 'socket'
require 'digest/sha1'
server = TCPServer.new('localhost', 2345)
loop do
  # Wait for a connection
  socket = server.accept
  STDERR.puts "Incoming Request"
  # Read 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

Khi tôi refresh WebSocket tester webpage và nó request đến server, đây là output tôi 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

Vậy chúng ta đã successfully gửi test message từ client đến toy WebSocket server. Bây giờ cool sẽ là gửi message từ server đến client.

Điều này hơi less involved, vì chúng ta không phải deal với masking stuff. Frames gửi từ server đến client luôn unmasked.

Giống như chúng ta consume frame một byte tại một thời điểm, chúng ta sẽ construct nó một byte tại một thời điểm.

Byte 1: FIN và opcode

Payload sẽ fit vào một frame, và nó sẽ là text. Nghĩa là FIN = 1, và opcode cũng = 1. Khi combine chúng dùng cùng bit format trước, tôi nhận được số:


output = [0b10000001]

Byte 2: MASKED và payload length

Vì frame này từ server đến client, MASKED = 0. Nghĩa là chúng ta ignore nó. Payload length chỉ là length của string.


output = [0b10000001, response.size]

Bytes 3 trở lên: Payload

Payload không masked, chỉ là string.


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

Bombs away!

Tại điểm này, chúng ta có array chứa data muốn gửi. Chúng ta cần convert này thành string of bytes để send over the wire. Để làm điều này chúng ta sẽ dùng siêu versatile Array#pack method.


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

String lạ "CCA#{ response.size }" bảo Array#pack array chứa hai 8-bit unsigned ints, theo sau là character string của size chỉ định.

Nếu tôi mở network inspector trong chrome, tôi thấy message came through loud and clear.

Tạo Ruby WebSocket Server Từ Con Số 0: Hướng Dẫn Bước Đi

Extra Credit

Đó là nó! Hy vọng bạn học được gì đó về WebSockets. Có nhiều thứ server thiếu. Nếu bạn muốn tiếp tục exercise, bạn có thể xem vào:

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