Các máy chủ ứng dụng Ruby thường được sử dụng cùng với một máy chủ web như nginx. Khi người dùng yêu cầu một trang từ ứng dụng Rails của bạn, nginx sẽ ủy quyền yêu cầu đó cho máy chủ ứng dụng. Nhưng chính xác thì điều đó hoạt động như thế nào? Nginx nói chuyện với kỳ lân như thế nào?
Một trong những lựa chọn hiệu quả nhất là sử dụng ổ cắm unix. Hãy xem chúng hoạt động như thế nào! Trong bài đăng này, chúng tôi sẽ bắt đầu với những kiến thức cơ bản về socket và kết thúc bằng cách tạo máy chủ ứng dụng đơn giản của riêng chúng tôi được hỗ trợ bởi nginx.
Sockets cho phép các chương trình nói chuyện với nhau như thể chúng đang ghi hoặc đọc từ một tệp. Trong ví dụ này, Unicorn tạo socket và giám sát nó để tìm các kết nối. Sau đó, Nginx có thể kết nối với ổ cắm và nói chuyện với Unicorn.
Ổ cắm unix là gì?
Các ổ cắm Unix cho phép một chương trình nói chuyện với một chương trình khác theo cách tương tự như làm việc với các tệp. Chúng là một loại IPC hoặc giao tiếp giữa các quá trình.
Để có thể truy cập thông qua một ổ cắm, trước tiên chương trình của bạn tạo một ổ cắm và "lưu" nó vào đĩa, giống như một tệp. Nó giám sát ổ cắm cho các kết nối đến. Khi nhận được một, nó sử dụng các phương thức IO tiêu chuẩn để đọc và ghi dữ liệu.
Ruby cung cấp mọi thứ bạn cần để làm việc với các ổ cắm unix thông qua một vài lớp:
-
UNIX * Máy chủ * - Nó tạo ra ổ cắm, lưu nó vào đĩa và cho phép bạn theo dõi nó để tìm các kết nối mới.
-
UNIX * Ổ cắm * - Mở các ổ cắm hiện có cho IO.
LƯU Ý: Các loại ổ cắm khác tồn tại. Đáng chú ý nhất là các ổ cắm TCP. Nhưng bài đăng này chỉ đề cập đến các ổ cắm unix. Làm thế nào để bạn biết sự khác biệt? Các ổ cắm Unix có tên tệp.
Ổ cắm Đơn giản nhất
Chúng ta sẽ xem xét hai chương trình nhỏ.
Đầu tiên là "máy chủ". Nó chỉ tạo một phiên bản của UnixServer
lớp, sau đó sử dụng server.accept
để chờ kết nối. Khi nhận được kết nối, nó sẽ trao đổi một lời chào.
Cần lưu ý rằng cả accept
và readline
các phương thức chặn việc thực thi chương trình cho đến khi chúng nhận được những gì chúng đang chờ đợi.
require "socket"
server = UNIXServer.new('/tmp/simple.sock')
puts "==== Waiting for connection"
socket = server.accept
puts "==== Got Request:"
puts socket.readline
puts "==== Sending Response"
socket.write("I read you loud and clear, good buddy!")
socket.close
Vì vậy, chúng tôi có một máy chủ. Bây giờ, tất cả những gì chúng tôi cần là một khách hàng.
Trong ví dụ dưới đây, chúng tôi mở socket được tạo bởi máy chủ của chúng tôi. Sau đó, chúng tôi sử dụng các phương pháp IO thông thường để gửi và nhận lời chào.
require "socket"
socket = UNIXSocket.new('/tmp/simple.sock')
puts "==== Sending"
socket.write("Hello server, can you hear me?\n")
puts "==== Getting Response"
puts socket.readline
socket.close
Để chứng minh, trước tiên chúng ta cần chạy máy chủ. Sau đó, chúng tôi chạy khách hàng. Bạn có thể xem kết quả bên dưới:
Ví dụ về tương tác máy khách / máy chủ ổ cắm UNIX đơn giản. Khách hàng ở bên trái. Máy chủ ở bên phải.
Giao diện với nginx
Bây giờ chúng ta đã biết cách tạo một "máy chủ" unix socket, chúng ta có thể dễ dàng giao tiếp với nginx.
Không tin tôi? Hãy thực hiện một bằng chứng nhanh chóng về khái niệm. Tôi sẽ điều chỉnh mã ở trên để làm cho nó in ra mọi thứ mà nó nhận được từ socket.
require "socket"
# Create the socket and "save it" to the file system
server = UNIXServer.new('/tmp/socktest.sock')
# Wait until for a connection (by nginx)
socket = server.accept
# Read everything from the socket
while line = socket.readline
puts line.inspect
end
socket.close
Bây giờ nếu tôi định cấu hình nginx để chuyển tiếp các yêu cầu tới socket tại /tmp/socktest.sock
Tôi có thể xem dữ liệu nginx đang gửi. (Đừng lo lắng, chúng ta sẽ thảo luận về cấu hình sau một phút nữa)
Khi tôi thực hiện một yêu cầu web, nginx sẽ gửi dữ liệu sau đến máy chủ nhỏ của tôi:
Tuyệt đấy! Đó chỉ là một yêu cầu HTTP bình thường với một vài tiêu đề bổ sung được thêm vào. Bây giờ chúng tôi đã sẵn sàng để xây dựng một máy chủ ứng dụng thực sự. Nhưng trước tiên, hãy thảo luận về cấu hình nginx.
Cài đặt và định cấu hình Nginx
Nếu bạn chưa cài đặt nginx trên máy phát triển của mình, hãy dành một chút thời gian và thực hiện điều đó ngay bây giờ. Nó thực sự dễ dàng trên OSX thông qua homebrew:
brew install nginx
Bây giờ chúng ta cần cấu hình nginx để chuyển tiếp các yêu cầu trên localhost:2048 tới máy chủ ngược dòng thông qua một ổ cắm có tên /tmp/socktest.sock
. Cái tên đó không có gì đặc biệt. Nó chỉ cần khớp với tên ổ cắm được sử dụng bởi máy chủ web của chúng tôi.
Bạn có thể lưu cấu hình này vào /tmp/nginx.conf
và sau đó chạy nginx bằng lệnh nginx -c /tmp/nginx.conf
để tải nó.
# Run nginx as a normal console program, not as a daemon
daemon off;
# Log errors to stdout
error_log /dev/stdout info;
events {} # Boilerplate
http {
# Print the access log to stdout
access_log /dev/stdout;
# Tell nginx that there's an external server called @app living at our socket
upstream app {
server unix:/tmp/socktest.sock fail_timeout=0;
}
server {
# Accept connections on localhost:2048
listen 2048;
server_name localhost;
# Application root
root /tmp;
# If a path doesn't exist on disk, forward the request to @app
try_files $uri/index.html $uri @app;
# Set some configuration options on requests forwarded to @app
location @app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass https://app;
}
}
}
Cấu hình này khiến nginx chạy như một ứng dụng đầu cuối bình thường, không giống như một daemon. Nó cũng ghi tất cả các bản ghi vào stdout. Khi bạn chạy nginx, nó sẽ trông giống như sau:
Nginx đang chạy ở chế độ không phải daemon.
Máy chủ ứng dụng tự làm
Bây giờ chúng ta đã thấy cách kết nối nginx với chương trình của mình, đó là một vấn đề khá đơn giản để xây dựng một máy chủ ứng dụng đơn giản. Khi nginx chuyển tiếp một yêu cầu tới socket của chúng tôi, đó là một yêu cầu HTTP tiêu chuẩn. Sau khi mày mò một chút, tôi đã có thể xác định rằng nếu socket trả về phản hồi HTTP hợp lệ, thì nó sẽ được hiển thị trong trình duyệt.
Ứng dụng dưới đây nhận bất kỳ yêu cầu nào và hiển thị dấu thời gian.
require "socket"
# Connection creates the socket and accepts new connections
class Connection
attr_accessor :path
def initialize(path:)
@path = path
File.unlink(path) if File.exists?(path)
end
def server
@server ||= UNIXServer.new(@path)
end
def on_request
socket = server.accept
yield(socket)
socket.close
end
end
# AppServer logs incoming requests and renders a view in response
class AppServer
attr_reader :connection
attr_reader :view
def initialize(connection:, view:)
@connection = connection
@view = view
end
def run
while true
connection.on_request do |socket|
while (line = socket.readline) != "\r\n"
puts line
end
socket.write(view.render)
end
end
end
end
# TimeView simply provides the HTTP response
class TimeView
def render
%[HTTP/1.1 200 OK
The current timestamp is: #{ Time.now.to_i }
]
end
end
AppServer.new(connection: Connection.new(path: '/tmp/socktest.sock'), view: TimeView.new).run
Bây giờ nếu tôi kích hoạt nginx cũng như tập lệnh của mình, tôi có thể truy cập localhost:2048. Yêu cầu được gửi đến ứng dụng của tôi. Và phản hồi được hiển thị bởi trình duyệt. Khá tuyệt!
Các yêu cầu HTTP được ghi vào STDOUT bởi máy chủ ứng dụng đơn giản của chúng tôi
Và đây là thành quả vinh quang của sự lao động của chúng ta. Hãy chứng kiến! Dấu thời gian!
Máy chủ trả về dấu thời gian được hiển thị trong trình duyệt