Computer >> Máy Tính >  >> Lập trình >> Ruby

Xây dựng một máy chủ websockets đơn giản từ đầu trong Ruby

Websockets ngày càng được báo chí nhiều hơn trong những ngày này. Chúng tôi nghe rằng chúng là "tương lai". Chúng tôi nghe nói rằng chúng dễ sử dụng hơn bao giờ hết 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 đăng này, chúng tôi sẽ trả lời những câu hỏi này bằng cách xây dựng một máy chủ WebSocket đơn giản từ đầu trong Ruby. Khi chúng tôi hoàn tất, chúng tôi sẽ đạt được giao tiếp hai chiều giữa trình duyệt và máy chủ của chúng tôi.

Mã trong bài đăng này có nghĩa là một bài tập học tập. Nếu bạn muốn triển khai websockets trong một ứng dụng sản xuất thực tế, hãy xem websocket-ruby gem tuyệt vời. Bạn cũng có thể xem qua Thông số kỹ thuật của WebSocket.

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

Web socket được phát minh để giải quyết một số vấn đề vốn có trong các kết nối HTTP thông thường. Khi bạn yêu cầu một trang web bằng kết nối HTTP thông thường, máy chủ sẽ gửi cho bạn nội dung và sau đó đóng kết nối. Nếu bạn muốn yêu cầu một trang khác, bạn phải thực hiện một kết nối khác. Điều này thường hoạt động tốt, nhưng nó không phải là cách tốt nhất cho một số trường hợp sử dụng:

  • Đối với một số ứng dụng, chẳng hạn như trò chuyện, giao diện người dùng cần được cập nhật ngay khi có thư mới. Nếu tất cả những gì bạn có là các yêu cầu HTTP bình thường, điều đó có nghĩa là bạn phải liên tục thăm dò máy chủ để xem có nội dung mới.
  • Nếu ứng dụng front-end của bạn cần thực hiện nhiều yêu cầu nhỏ tới máy chủ, thì việc tạo kết nối mới cho mỗi yêu cầu có thể trở thành một vấn đề về hiệu suất. Đây ít vấn đề hơn trong HTTP2.

Với ổ cắm Web, bạn tạo một kết nối với máy chủ, sau đó được giữ ở chế độ mở và được sử dụng để giao tiếp hai chiều.

Phía khách hàng

Ổ cắm web thường được sử dụng để giao tiếp giữa trình duyệt và máy chủ Web. Phía trình duyệt được thực hiện bằng JavaScript. Trong ví dụ dưới đây, tôi đã viết một đoạn JavaScript rất đơn giản để mở một ổ cắm Web đến máy chủ cục bộ của tôi và gửi tin nhắn đến nó.

      Ứng dụng khách Websocket        

Nếu tôi khởi động một máy chủ tĩnh nhỏ và mở tệp này trong trình duyệt web của mình, tôi sẽ gặp lỗi. Điều đó có ý nghĩa, bởi vì vẫn chưa có máy chủ. Chúng tôi vẫn phải xây dựng một cái. :-)

Xây dựng một máy chủ websockets đơn giản từ đầu trong Ruby

Khởi động máy chủ

Ổ cắm web bắt đầu hoạt động như các yêu cầu HTTP bình thường. Chúng có một vòng đời kỳ lạ:

  1. Trình duyệt gửi một yêu cầu HTTP bình thường, với một số tiêu đề đặc biệt có nội dung "vui lòng tạo cho tôi một websocket".
  2. Máy chủ trả lời bằng một phản hồi HTTP nhất định, nhưng KHÔNG ĐÓNG KẾT NỐI.
  3. Sau đó, trình duyệt và máy chủ sử dụng giao thức websocket đặc biệt để trao đổi các khung dữ liệu qua kết nối mở.

Vì vậy, bước đầu tiên đối với chúng tôi là xây dựng một máy chủ Web. Trong đoạn mã dưới đây, tôi đang tạo một máy chủ web đơn giản nhất có thể. Nó không thực sự phục vụ bất cứ điều gì. Nó chỉ cần đợi một yêu cầu sau đó in nó ra STDERR.

Vòng lặp
  request 'socket'server =TCPServer.new (' localhost ', 2345) do # Chờ kết nối socket =server.accept STDERR.puts "Yêu cầu đến" # Đọc yêu cầu HTTP. Chúng tôi biết nó đã kết thúc khi chúng tôi thấy một dòng không có gì ngoài \ r \ n http_request ="" while (line =socket.gets) &&(line! ="\ R \ n") http_request + =line end STDERR.puts http_request socket .closeend  

Nếu tôi chạy máy chủ và làm mới trang thử nghiệm websocket của mình, tôi nhận được điều này:

  $ ruby ​​server1.rbIncoming RequestGET / HTTP / 1.1Host:localhost:2345Connection:UpgradeUpgrade:websocketSec-WebSocket-Version:13Sec-WebSocket-Key:cG8zEwcrcLnEftn2qohdKQ == 

Nếu bạn để ý, yêu cầu HTTP này có một loạt các tiêu đề liên quan đến các ổ cắm trên Web. Đây thực sự là bước đầu tiên trong quá trình bắt tay websocket

Cái bắt tay

Tất cả các yêu cầu về ổ cắm web đều bắt đầu bằng một cái bắt tay. Điều này là để đảm bảo rằng cả máy khách và máy chủ đều hiểu rằng các ổ cắm Web sắp xảy ra và cả hai đều đồng ý về phiên bản giao thức. Nó hoạt động như thế này:

Ứng dụng khách gửi một yêu cầu HTTP như thế này

  GET / HTTP / 1.1Host:localhost:2345Nâng cấp:websocketConnection:UpgradeSec-WebSocket-Key:E4i4gDQc1XTIQcQxvf + ODA ==Sec-WebSocket-Phiên bản:13  

Phần quan trọng nhất của yêu cầu này là Sec-WebSocket-Key . Máy khách mong đợi máy chủ trả về phiên bản đã sửa đổi của giá trị này để làm bằng chứng chống lại các cuộc tấn công XSS và proxy bộ nhớ đệm.

Máy chủ phản hồi

  HTTP / 1.1 101 Giao thức chuyển đổi Nâng cấp:websocketConnection:UpgradeSec-WebSocket-Accept:d9WHst60HtB4IvjOVevrexl0oLA = 

Phản hồi của máy chủ là bản soạn sẵn ngoại trừ Sec-WebSocket-Accept đầu trang. Tiêu đề này được tạo như sau:

  # Lấy giá trị do máy khách cung cấp, nối chuỗi # ma thuật vào đó. Tạo hàm băm SHA1, sau đó mã hóa base64. 

Đôi mắt của bạn không nói dối bạn. Có một hằng số ma thuật liên quan.

Triển khai cái bắt tay

Hãy cập nhật máy chủ của chúng tôi để hoàn tất quá trình bắt tay. Đầu tiên, chúng tôi sẽ lấy mã thông báo bảo mật ra khỏi tiêu đề yêu cầu:

  # Lấy khóa bảo mật từ các tiêu đề. # Nếu không có khóa, hãy đóng kết nối.if match =http_request.match (/ ^ Sec-WebSocket-Key:(\ S +) /) websocket_key =khớp với [1] STDERR.puts "Đã phát hiện bắt tay websocket với khóa:# {websocket_key}" else STDERR.puts "Hủy kết nối không phải websocket" socket.close nextend  

Bây giờ, chúng tôi sử dụng khóa bảo mật để tạo phản hồi hợp lệ:

  response_key =Digest ::SHA1.base64digest ([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"]. tham gia) STDERR.puts "Phản hồi bắt tay với key:# {response_key}" socket. write <<- eosHTTP / 1.1 101 Giao thức chuyển đổi Nâng cấp:websocketConnection:UpgradeSec-WebSocket-Accept:# {response_key} eosSTDERR.puts "Bắt tay đã hoàn tất."  

Khi tôi làm mới trang kiểm tra websocket, tôi thấy bây giờ không còn lỗi kết nối nữa. Kết nối đã được thiết lập!

Xây dựng một máy chủ websockets đơn giản từ đầu trong Ruby

Đây là kết quả từ máy chủ, hiển thị các khóa bảo mật và khóa phản hồi:

  $ ruby ​​server2.rbIncoming RequestWebsocket đã phát hiện bắt tay bằng khóa:Fh06 + WnoTQQiVnX5saeYMg ==Đáp ứng bắt tay bằng khóa:nJg1c2upAHixOmXz7kV2bJ2g / YQ =Đã hoàn tất bắt tay.  

Giao thức khung websocket

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

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

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

Bất cứ khi nào bạn yêu cầu trình duyệt web của mình gửi dữ liệu qua WebSocket hoặc yêu cầu máy chủ của bạn phản hồi, dữ liệu sẽ được chia thành một loạt các phần trong mỗi phần đó được bao bọc trong một số siêu dữ liệu để tạo khung.

Đây là cấu trúc khung trông như thế nào. Các số dọc theo đầu là các bit. Và một số trường, như độ dài tải trọng mở rộng có thể không phải lúc nào cũng có:

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 | Chiều dài tải trọng mở rộng || I | S | S | S | (4) | A | (7) | (16/64) || N | V | V | V | | S | | (nếu tải trọng len ==126/127) || | 1 | 2 | 3 | | K | | | + - + - + - + - + ------- + - + ------------- + - - - - - - - - - - - - - - - + | Độ dài tải trọng mở rộng tiếp tục, nếu tải trọng len ==127 | + - - - - - - - - - - - - - - - + --------------------- ---------- + | | Masking-key, nếu MASK được đặt thành 1 | + --------------------------------- + ------ ------------------------- + | Masking-key (tiếp theo) | Dữ liệu tải trọng | + -------------------------------- - - - - - - - - - - - - - - - +:Tải trọng Dữ liệu tiếp tục ...:+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Dữ liệu trọng tải tiếp tục ... | + ------------------------------------------ --------------------- +

Điều đầu tiên có thể khiến bạn giật mình là đây là một giao thức nhị phân. Chúng ta sẽ phải thực hiện một số thao tác nhỏ, nhưng đừng lo lắng - nó sẽ không khó lắm đâu. Các số dọc theo đầu của hình là các bit. Và một số trường có thể không phải lúc nào cũng có. Ví dụ:độ dài trọng tải mở rộng sẽ hiển thị nếu trọng tải dưới 127 byte.

Đang nhận dữ liệu

Bây giờ quá trình bắt tay của họ đã hoàn tất, chúng ta có thể bắt đầu phân tích cú pháp khung nhị phân. Để đơn giản hóa mọi thứ, chúng ta sẽ xem xét frame đến từng byte một. Sau đó, chúng tôi sẽ tổng hợp tất cả lại với nhau để bạn có thể thấy nó hoạt động.

Byte 1:FIN và Opcode

Từ bảng trên, bạn có thể thấy rằng byte đầu tiên (tám bit đầu tiên) chứa một số phần dữ liệu:

  • FIN:1 bit Nếu điều này là sai, thì thông báo được chia thành nhiều khung
  • opcode:4 bit Cho chúng tôi biết liệu trọng tải là văn bản, nhị phân hay đây chỉ là "ping" để duy trì kết nối.
  • RSV:3 bit Những thứ này không được sử dụng trong thông số kỹ thuật WebSockets hiện tại.

Để lấy byte đầu tiên, chúng tôi sẽ sử dụng IO # getbyte phương pháp. Và để trích xuất dữ liệu, chúng tôi sẽ sử dụng một số bitmasking đơn giản. Nếu bạn không quen thuộc với các toán tử bitwise, hãy xem bài viết khác của tôi Các thủ thuật bitwise trong Ruby

  first_byte =socket.getbytefin =first_byte &0b10000000opcode =first_byte &0b00001111 # Máy chủ của chúng tôi sẽ chỉ hỗ trợ tin nhắn văn bản, khung đơn. # Nâng cao ngoại lệ nếu khách hàng cố gắng gửi bất kỳ thứ gì khác. t hỗ trợ tính liên tục "trừ khi giải thích" Chúng tôi chỉ hỗ trợ opcode 1 "trừ khi opcode ==1  

Byte 2:MASK và độ dài trọng tải

Byte thứ hai của khung chứa thêm thông tin về tải trọng.

  • MASK:1 bit Cờ Boolean cho biết liệu trọng tải có bị che hay không. Nếu đó là sự thật, thì trọng tải sẽ phải được "hiển thị" trước khi sử dụng. Điều này LUÔN LUÔN đúng đối với các khung đến từ khách hàng của chúng tôi. Thông số kỹ thuật nói như vậy.
  • độ dài tải trọng:7 bit Nếu trọng tải của chúng tôi nhỏ hơn 126 byte, độ dài được lưu trữ ở đây. Nếu giá trị này lớn hơn 126, điều đó có nghĩa là sẽ có nhiều byte hơn để cung cấp cho chúng tôi độ dài.

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

  second_byte =socket.getbyteis_masked =second_byte &0b10000000payload_size =second_byte &0b01111111raise "Tất cả các khung được gửi đến máy chủ phải được che theo thông số kỹ thuật websocket" trừ khi is_maskedraise "Chúng tôi chỉ hỗ trợ tải trọng có kích thước <126 byte" trừ khi payload_size <126STDERR.puts "Kích thước tải trọng:# {payload_size} byte"  

Byte 3-7:Khóa che

Chúng tôi hy vọng rằng trọng tải của tất cả các khung đến sẽ bị che. Để hiển thị nội dung, chúng tôi sẽ phải XOAY nội dung đó dựa trên khóa che.

Khóa che này tạo thành bốn byte tiếp theo. Chúng tôi không phải xử lý nó, chúng tôi chỉ đọc các byte vào một mảng.

  mask =4.times.map {socket.getbyte} STDERR.puts "Có mặt nạ:# {mask.inspect}"  

Vui lòng cho tôi biết nếu bạn biết cách tốt hơn để đọc 4 byte vào một mảng. times.map hơi lạ, nhưng đó là cách tiếp cận ngắn gọn nhất mà tôi có thể nghĩ ra. Tôi là @StarrHorne trên twitter.

Bytes 8 trở lên:Trọng tải

Được rồi, chúng ta đã hoàn tất với siêu dữ liệu. Bây giờ có thể tìm nạp tải trọng thực tế.

  data =payload_size.times.map {socket.getbyte} STDERR.puts "Có dữ liệu bị che:# {data.inspect}"  

Hãy nhớ rằng tải trọng này được che giấu. Vì vậy, nếu bạn in nó ra, nó sẽ giống như rác. Để hiển thị nó, chúng tôi chỉ cần XOR từng byte với byte tương ứng của mặt nạ. Vì mặt nạ chỉ dài bốn byte, chúng tôi lặp lại nó để khớp với độ dài của trọng tải:

  unmasked_data =data.each_with_index.map {| byte, i | byte ^ mask [i% 4]} STDERR.puts "Đã hiển thị dữ liệu:# {unmasked_data.inspect}"  

Bây giờ chúng ta có một mảng các byte. Chúng ta cần chuyển đổi nó thành một chuỗi unicode. Tất cả văn bản trong Websockets là unicode.

  STDERR.puts "Được chuyển đổi thành chuỗi:# {unmasked_data.pack ('C *'). force_encoding ('utf-8'). verify}"  

Tập hợp tất cả lại với nhau

Khi bạn đặt tất cả mã này lại với nhau, bạn sẽ nhận được một tập lệnh trông giống như sau:

  request 'socket' # Cung cấp các lớp TCPServer và TCPSocketrequire 'dig / sha1'server =TCPServer.new (' localhost ', 2345) vòng lặp do # Chờ kết nối socket =server.accept STDERR.puts "Incoming Yêu cầu "# Đọc yêu cầu HTTP. Chúng tôi biết rằng nó đã hoàn tất khi chúng tôi thấy một dòng không có gì ngoài \ r \ n http_request ="" while (line =socket.gets) &&(line! ="\ R \ n") http_request + =line end # Lấy khóa bảo mật từ các tiêu đề. Nếu không có, hãy đóng kết nối. if match =http_request.match (/ ^ Sec-WebSocket-Key:(\ S +) /) websocket_key =match [1] STDERR.puts "Đã phát hiện bắt tay Websocket với khóa:# {websocket_key}" else STDERR.puts "Hủy bỏ kết nối websocket "socket.close next end response_key =Digest ::SHA1.base64digest ([websocket_key," 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 "]. join) STDERR.puts" Phản hồi bắt tay bằng phím:# {response_key} " socket.write <<- eosHTTP / 1.1 101 Giao thức chuyển đổi Nâng cấp:websocketConnection:UpgradeSec-WebSocket-Accept:# {response_key} eos STDERR.puts "Bắt tay đã hoàn tất. Bắt đầu phân tích cú pháp khung websocket". first_byte =socket.getbyte fin =first_byte &0b10000000 opcode =first_byte &0b00001111 raise "Chúng tôi không hỗ trợ liên tục" trừ khi tăng fin "Chúng tôi chỉ hỗ trợ opcode 1" trừ khi opcode ==1 second_byte =socket.getbyte is_masked =second_byte &0b10000000 payload_size =second_byte &0b01111111 raise "Tất cả các khung đến phải được che theo thông số websocket" trừ khi tăng is_masked "Chúng tôi chỉ hỗ trợ các trọng tải có độ dài <126 byte" trừ khi payload_size <126 STDERR.puts "Kích thước tải trọng:# {payload_size} byte" mặt nạ =4.times.map {socket.getbyte} STDERR.puts "Có mặt nạ:# {mask.inspect}" data =payload_size.times.map {socket.getbyte} STDERR.puts "Có dữ liệu bị che:# {data.inspect } "unmasked_data =data.each_with_index.map {| byte, i | byte ^ mask [i% 4]} STDERR.puts "Đã hiển thị dữ liệu:# {unmasked_data.inspect}" STDERR.puts "Được chuyển đổi thành chuỗi:# {unmasked_data.pack ('C *'). force_encoding ('utf- 8 '). Kiểm tra} "socket.closeend  

Khi tôi làm mới trang web của trình kiểm tra WebSocket và nó yêu cầu máy chủ của tôi, đây là kết quả mà tôi thấy:

  $ ruby ​​websocket_server.rbIncoming RequestWebsocket bắt tay được phát hiện bằng khóa:E4i4gDQc1XTIQcQxvf + ODA ==Đáp ứng bắt tay bằng khóa:d9WHst60HtB4IvjOVevrexl0oLA =Đã hoàn tất bắt tay. Bắt đầu phân tích cú pháp khung websocket. Kích thước tải trọng:16 byteGot mask:[80, 191, 161, 254] Đã nhận dữ liệu bị che:[19, 222, 207, 222, 41, 208, 212, 222, 56, 218, 192, 140, 112, 210, 196, 193] Đã hiển thị dữ liệu:[67, 97, 110, 32, 121, 111, 117, 32, 104, 101, 97, 114, 32, 109, 101, 63] Được chuyển đổi thành a string:"Bạn có nghe thấy tôi nói không?"  

Gửi lại dữ liệu cho khách hàng

Vì vậy, chúng tôi đã gửi thành công một thông báo thử nghiệm từ khách hàng của chúng tôi đến máy chủ WebSocket đồ chơi của chúng tôi. Bây giờ sẽ rất tuyệt nếu gửi lại một tin nhắn từ máy chủ đến máy khách.

Điều này ít liên quan hơn một chút, vì chúng tôi không phải xử lý bất kỳ thứ gì làm mặt nạ. Các khung được gửi từ máy chủ đến máy khách luôn được hiển thị.

Giống như chúng ta sử dụng frame từng byte một, chúng ta sẽ xây dựng nó từng byte một.

Byte 1:FIN và opcode

Trọng tải của chúng tôi sẽ vừa với một khung và nó sẽ là văn bản. Điều đó có nghĩa là FIN sẽ bằng 1 và opcode cũng bằng một. Khi tôi kết hợp chúng bằng cách sử dụng cùng một định dạng bit mà chúng tôi đã sử dụng trước đây, tôi nhận được một số:

  output =[0b10000001]  

Byte 2:MASKED và độ dài trọng tải

Vì khung này đi từ máy chủ đến máy khách, MASKED sẽ bằng không. Điều đó có nghĩa là chúng ta có thể bỏ qua nó. Độ dài trọng tải chỉ là độ dài của chuỗi.

  output =[0b10000001, response.size]  

Bytes 3 trở lên:Trọng tải

Trọng tải không bị che, nó chỉ là một chuỗi.

  response ="To và rõ ràng!" STDERR.puts "Đang gửi phản hồi:# {response.inspect}" output =[0b10000001, response.size, response]  

Bỏ bom!

Tại thời điểm này, chúng ta có một mảng chứa dữ liệu mà chúng ta muốn gửi. Chúng tôi cần chuyển đổi nó thành một chuỗi byte mà chúng tôi có thể gửi qua dây. Để làm điều này, chúng tôi sẽ sử dụng gói Array # siêu đa năng phương pháp.

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

Chuỗi kỳ lạ đó "CCA # {response.size}" nói với Array # pack rằng mảng chứa hai số nguyên 8 bit không dấu, theo sau là một chuỗi ký tự có kích thước được chỉ định.

Nếu tôi mở trình kiểm tra mạng trong chrome, tôi có thể thấy thông báo phát ra rất to và rõ ràng.

Xây dựng một máy chủ websockets đơn giản từ đầu trong Ruby

Tín dụng bổ sung

Đó là nó! Tôi hy vọng bạn đã học được điều gì đó về WebSockets. Có rất nhiều thứ mà máy chủ còn thiếu. Nếu bạn muốn tiếp tục tập thể dục, bạn có thể xem chúng:

  • Hỗ trợ tải trọng nhiều khung hình
  • Hỗ trợ tải trọng nhị phân
  • Hỗ trợ Ping / Pong
  • Hỗ trợ tải trọng dài
  • Bắt tay kết thúc