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

Xây dựng một máy chủ HTTP 30 dòng trong Ruby

Máy chủ web và HTTP nói chung có vẻ khó hiểu. Trình duyệt định dạng một yêu cầu như thế nào và phản hồi được gửi đến người dùng như thế nào? Trong tập Ruby Magic này, chúng ta sẽ tìm hiểu cách xây dựng một máy chủ Ruby HTTP trong 30 dòng mã. Khi chúng tôi hoàn tất, máy chủ của chúng tôi sẽ xử lý các yêu cầu HTTP GET và chúng tôi sẽ sử dụng nó để phân phát ứng dụng Rack.

Cách HTTP và TCP hoạt động cùng nhau

TCP là một giao thức truyền tải mô tả cách máy chủ và máy khách trao đổi dữ liệu.

HTTP là một giao thức phản hồi yêu cầu mô tả cụ thể cách máy chủ web trao đổi dữ liệu với máy khách HTTP hoặc trình duyệt web. HTTP thường sử dụng TCP làm giao thức truyền tải của nó. Về bản chất, máy chủ HTTP là máy chủ TCP "nói" HTTP.

# tcp_server.rb
require 'socket'
server = TCPServer.new 5678
 
while session = server.accept
  session.puts "Hello world! The time is #{Time.now}"
  session.close
end

Trong ví dụ này về máy chủ TCP, máy chủ liên kết với cổng 5678 và đợi một máy khách kết nối. Khi điều đó xảy ra, nó sẽ gửi một thông báo đến máy khách, và sau đó đóng kết nối. Sau khi nói chuyện xong với máy khách đầu tiên, máy chủ đợi một máy khách khác kết nối để gửi lại tin nhắn của nó.

# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678
 
while line = server.gets
  puts line
end
 
server.close

Để kết nối với máy chủ của chúng tôi, chúng tôi sẽ cần một ứng dụng khách TCP. Ứng dụng khách mẫu này kết nối với cùng một cổng (5678 ) và sử dụng server.gets để nhận dữ liệu từ máy chủ, sau đó được in ra. Khi ngừng nhận dữ liệu, nó sẽ đóng kết nối với máy chủ và chương trình sẽ thoát.

Khi bạn khởi động máy chủ máy chủ đang chạy ($ ruby tcp_server.rb ), bạn có thể khởi động ứng dụng khách trong một tab riêng biệt để nhận thông báo của máy chủ.

$ ruby tcp_client.rb
Hello world! The time is 2016-11-23 15:17:11 +0100
$

Với một chút tưởng tượng, máy chủ và máy khách TCP của chúng tôi hoạt động giống như một máy chủ web và một trình duyệt. Máy khách gửi yêu cầu, máy chủ phản hồi và kết nối bị đóng. Đó là cách hoạt động của mẫu yêu cầu-phản hồi, đây chính là những gì chúng ta cần để xây dựng một máy chủ HTTP.

Trước khi bắt đầu phần tốt, hãy xem các yêu cầu và phản hồi HTTP trông như thế nào.

Yêu cầu HTTP GET cơ bản

Yêu cầu HTTP GET cơ bản nhất là một dòng yêu cầu không có bất kỳ tiêu đề bổ sung hoặc nội dung yêu cầu nào.

GET / HTTP/1.1\r\n

Dòng Yêu cầu bao gồm bốn phần:

  • Mã thông báo phương thức (GET , trong ví dụ này)
  • URI Yêu cầu (/ )
  • Phiên bản giao thức (HTTP/1.1 )
  • CRLF (ký tự xuống dòng:\r , theo sau là nguồn cấp dữ liệu dòng:\n ) để chỉ ra cuối dòng

Máy chủ sẽ phản hồi bằng phản hồi HTTP, có thể giống như sau:

HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n\Hello world!

Phản hồi này bao gồm:

  • Dòng trạng thái:phiên bản giao thức ("HTTP / 1.1"), theo sau là khoảng trắng, mã trạng thái của phản hồi ("200") và được kết thúc bằng CRLF (\r\n )
  • Dòng tiêu đề tùy chọn. Trong trường hợp này, chỉ có một dòng tiêu đề ("Content-Type:text / html"), nhưng có thể có nhiều dòng (được phân tách bằng CRLF:\r\n )
  • Một dòng mới (hoặc một CRLF kép) để tách dòng trạng thái và tiêu đề khỏi phần nội dung:(\r\n\r\n )
  • Phần nội dung:"Hello world!"

Máy chủ Ruby HTTP tối thiểu

Nói đủ. Bây giờ chúng ta đã biết cách tạo một máy chủ TCP trong Ruby và một số yêu cầu và phản hồi HTTP trông như thế nào, chúng ta có thể xây dựng một máy chủ HTTP tối thiểu. Bạn sẽ nhận thấy rằng máy chủ web hầu như giống với máy chủ TCP mà chúng ta đã thảo luận trước đó. Ý tưởng chung là giống nhau, chúng tôi chỉ sử dụng giao thức HTTP để định dạng thông điệp của chúng tôi. Ngoài ra, vì chúng tôi sẽ sử dụng trình duyệt để gửi yêu cầu và phân tích cú pháp phản hồi, chúng tôi sẽ không phải triển khai ứng dụng khách lần này.

# http_server.rb
require 'socket'
server = TCPServer.new 5678
 
while session = server.accept
  request = session.gets
  puts request
 
  session.print "HTTP/1.1 200\r\n" # 1
  session.print "Content-Type: text/html\r\n" # 2
  session.print "\r\n" # 3
  session.print "Hello world! The time is #{Time.now}" #4
 
  session.close
end

Sau khi máy chủ nhận được yêu cầu, giống như trước đây, nó sử dụng session.print để gửi lại một tin nhắn cho khách hàng:Thay vì chỉ tin nhắn của chúng tôi, nó đặt tiền tố phản hồi bằng một dòng trạng thái, một tiêu đề và một dòng mới:

  1. Dòng trạng thái (HTTP 1.1 200\r\n ) để cho trình duyệt biết rằng phiên bản HTTP là 1.1 và mã phản hồi là "200"
  2. Một tiêu đề để chỉ ra rằng phản hồi có kiểu nội dung văn bản / html (Content-Type: text/html\r\n )
  3. Dòng mới (\r\n )
  4. Phần nội dung:"Hello world!…"

Giống như trước đây, nó đóng kết nối sau khi gửi tin nhắn. Chúng tôi chưa đọc yêu cầu, vì vậy nó chỉ in nó ra bảng điều khiển ngay bây giờ.

Nếu bạn khởi động máy chủ và mở https:// localhost:5678 trong trình duyệt của mình, bạn sẽ thấy dòng "Hello world!…" Với thời gian hiện tại, giống như chúng tôi đã nhận được từ ứng dụng TCP của chúng tôi trước đó. 🎉

Cung cấp ứng dụng Rack

Cho đến nay, máy chủ của chúng tôi đã trả lại một phản hồi cho mỗi yêu cầu. Để làm cho nó hữu ích hơn một chút, chúng tôi có thể thêm nhiều phản hồi hơn vào máy chủ của mình. Thay vì thêm trực tiếp những thứ này vào máy chủ, chúng tôi sẽ sử dụng ứng dụng Rack. Máy chủ của chúng tôi sẽ phân tích cú pháp các yêu cầu HTTP và chuyển chúng đến ứng dụng Rack, ứng dụng này sau đó sẽ trả lại phản hồi để máy chủ gửi lại cho máy khách.

Rack là một giao diện giữa các máy chủ web hỗ trợ Ruby và hầu hết các khung công tác web Ruby như Rails và Sinatra. Ở dạng đơn giản nhất, ứng dụng Rack là một đối tượng phản hồi call và trả về "tiplet", một mảng có ba mục:mã phản hồi HTTP, băm của tiêu đề HTTP và nội dung.

app = Proc.new do |env|
  ['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end

Trong ví dụ này, mã phản hồi là "200", chúng tôi đang chuyển "text / html" làm loại nội dung thông qua các tiêu đề và nội dung là một mảng với một chuỗi.

Để cho phép máy chủ của chúng tôi phân phát các phản hồi từ ứng dụng này, chúng tôi sẽ cần chuyển bộ ba được trả về thành một chuỗi phản hồi HTTP. Thay vì luôn trả về một phản hồi tĩnh, như chúng ta đã làm trước đây, bây giờ chúng ta sẽ phải tạo phản hồi từ bộ ba được trả về bởi ứng dụng Rack.

# http_server.rb
require 'socket'
 
app = Proc.new do
  ['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
 
server = TCPServer.new 5678
 
while session = server.accept
  request = session.gets
  puts request
 
  # 1
  status, headers, body = app.call({})
 
  # 2
  session.print "HTTP/1.1 #{status}\r\n"
 
  # 3
  headers.each do |key, value|
    session.print "#{key}: #{value}\r\n"
  end
 
  # 4
  session.print "\r\n"
 
  # 5
  body.each do |part|
    session.print part
  end
  session.close
end

Để phục vụ phản hồi mà chúng tôi nhận được từ ứng dụng Rack, chúng tôi sẽ thực hiện một số thay đổi đối với máy chủ của mình:

  1. Nhận mã trạng thái, tiêu đề và nội dung từ bộ ba được trả về bởi app.call .
  2. Sử dụng mã trạng thái để tạo dòng trạng thái
  3. Lặp lại các tiêu đề và thêm một dòng tiêu đề cho từng cặp khóa-giá trị trong hàm băm
  4. In một dòng mới để tách dòng trạng thái và tiêu đề khỏi phần nội dung
  5. Vòng qua phần thân và in từng phần. Vì chỉ có một phần trong mảng nội dung của chúng tôi, nó sẽ chỉ cần in thông báo "Hello world" của chúng tôi vào phiên trước khi đóng nó.

Đọc yêu cầu

Cho đến nay, máy chủ của chúng tôi đã bỏ qua yêu cầu request Biến đổi. Chúng tôi không cần phải làm như vậy vì ứng dụng Rack của chúng tôi luôn trả lại cùng một phản hồi.

Rack::Lobster là một ứng dụng mẫu đi kèm với Rack và sử dụng các tham số URL yêu cầu để hoạt động. Thay vì Proc mà chúng tôi đã sử dụng làm ứng dụng trước đây, chúng tôi sẽ sử dụng nó làm ứng dụng thử nghiệm từ bây giờ.

# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
 
app = Rack::Lobster.new
server = TCPServer.new 5678
 
while session = server.accept
# ...

Mở trình duyệt bây giờ sẽ hiển thị một con tôm hùm thay vì chuỗi nhàm chán mà nó đã in trước đó. Lobstericious!

Cú "lật!" và "sụp đổ!" liên kết liên kết đến /?flip=left/?flip=crash tương ứng. Tuy nhiên, khi làm theo các liên kết, con tôm hùm không bị lật và không có gì bị rơi. Đó là bởi vì máy chủ của chúng tôi không xử lý các chuỗi truy vấn ngay bây giờ. Hãy nhớ yêu cầu request biến chúng tôi đã bỏ qua trước đây? Nếu chúng tôi xem nhật ký máy chủ của mình, chúng tôi sẽ thấy các chuỗi yêu cầu cho từng trang.

GET / HTTP/1.1
GET /?flip=left HTTP/1.1
GET /?flip=crash HTTP/1.1

Các chuỗi yêu cầu HTTP bao gồm phương thức yêu cầu ("GET"), đường dẫn yêu cầu (/ , /?flip=left/?flip=crash ) và phiên bản HTTP. Chúng tôi có thể sử dụng thông tin này để xác định những gì chúng tôi cần phục vụ.

# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
 
app = Rack::Lobster.new
server = TCPServer.new 5678
 
while session = server.accept
  request = session.gets
  puts request
 
  # 1
  method, full_path = request.split(' ')
  # 2
  path, query = full_path.split('?')
 
  # 3
  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query
  })
 
  session.print "HTTP/1.1 #{status}\r\n"
  headers.each do |key, value|
    session.print "#{key}: #{value}\r\n"
  end
  session.print "\r\n"
  body.each do |part|
    session.print part
  end
  session.close
end

Để phân tích cú pháp yêu cầu và gửi các tham số yêu cầu đến ứng dụng Rack, chúng tôi sẽ chia nhỏ chuỗi yêu cầu và gửi đến ứng dụng Rack:

  1. Chia chuỗi yêu cầu thành một phương thức và một đường dẫn đầy đủ
  2. Chia đường dẫn đầy đủ thành một đường dẫn và một truy vấn
  3. Chuyển những thứ đó vào ứng dụng của chúng tôi trong môi trường Rack băm.

Ví dụ:một yêu cầu như GET /?flip=left HTTP/1.1\r\n sẽ được chuyển đến ứng dụng như thế này:

{
  'REQUEST_METHOD' => 'GET',
  'PATH_INFO' => '/',
  'QUERY_STRING' => '?flip=left'
}

Khởi động lại máy chủ của chúng tôi, truy cập https:// localhost:5678 và nhấp vào liên kết "lật!" - bây giờ sẽ lật tôm hùm và nhấp vào "crash!" liên kết sẽ làm hỏng máy chủ web của chúng tôi.

Chúng tôi mới chỉ sơ lược bề mặt của việc triển khai một máy chủ HTTP và của chúng tôi chỉ có 30 dòng mã, nhưng nó giải thích ý tưởng cơ bản. Nó chấp nhận các yêu cầu GET, chuyển các thuộc tính của yêu cầu đến ứng dụng Rack và gửi lại phản hồi cho trình duyệt. Mặc dù nó không xử lý những thứ như yêu cầu phát trực tuyến và yêu cầu POST, máy chủ của chúng tôi về mặt lý thuyết cũng có thể được sử dụng để phục vụ các ứng dụng Rack khác.

Điều này kết thúc cái nhìn nhanh của chúng tôi về việc xây dựng một máy chủ HTTP trong Ruby. Nếu bạn muốn chơi với máy chủ của chúng tôi, đây là mã. Hãy cho chúng tôi biết tại @AppSignal nếu bạn muốn biết thêm hoặc có câu hỏi cụ thể.

Nếu bạn thích bài viết này, hãy đăng ký nhận bản tin Ruby Magic:một (khoảng) số tiền hàng tháng của Ruby.