โดยทั่วไปแล้วแอปพลิเคชันเซิร์ฟเวอร์ Ruby จะใช้ร่วมกับเว็บเซิร์ฟเวอร์เช่น nginx เมื่อผู้ใช้ร้องขอหน้าจากแอป Rails ของคุณ nginx จะมอบหมายคำขอไปยังแอปพลิเคชันเซิร์ฟเวอร์ แต่มันทำงานอย่างไรกันแน่? nginx คุยกับยูนิคอร์นอย่างไร?
หนึ่งในตัวเลือกที่มีประสิทธิภาพมากที่สุดคือการใช้ซ็อกเก็ตยูนิกซ์ มาดูกันว่ามันทำงานอย่างไร! ในโพสต์นี้ เราจะเริ่มต้นด้วยพื้นฐานของซ็อกเก็ต และจบลงด้วยการสร้างแอปพลิเคชันเซิร์ฟเวอร์อย่างง่ายของเราเองที่พร็อกซี nginx
ซ็อกเก็ตช่วยให้โปรแกรมสามารถพูดคุยกันราวกับว่าพวกเขากำลังเขียนหรืออ่านจากไฟล์ ในตัวอย่างนี้ Unicorn จะสร้างซ็อกเก็ตและตรวจสอบการเชื่อมต่อ จากนั้น Nginx สามารถเชื่อมต่อกับซ็อกเก็ตและพูดคุยกับยูนิคอร์นได้
ซ็อกเก็ตยูนิกซ์คืออะไร
ซ็อกเก็ต Unix ช่วยให้โปรแกรมหนึ่งพูดคุยกับอีกโปรแกรมหนึ่งในลักษณะที่คล้ายกับการทำงานกับไฟล์ เป็นประเภทของ IPC หรือการสื่อสารระหว่างกระบวนการ
เพื่อให้สามารถเข้าถึงได้ผ่านซ็อกเก็ต ก่อนอื่นโปรแกรมของคุณจะสร้างซ็อกเก็ตและ "บันทึก" ลงในดิสก์ เช่นเดียวกับไฟล์ จะตรวจสอบซ็อกเก็ตสำหรับการเชื่อมต่อขาเข้า เมื่อได้รับก็จะใช้วิธี IO มาตรฐานในการอ่านและเขียนข้อมูล
Ruby มีทุกสิ่งที่คุณต้องการในการทำงานกับยูนิกซ์ซ็อกเก็ตผ่านคลาสต่างๆ:
-
UNIX*เซิร์ฟเวอร์ * - สร้างซ็อกเก็ต บันทึกลงในดิสก์ และให้คุณตรวจสอบการเชื่อมต่อใหม่ได้
-
UNIX*ซ็อกเก็ต * - เปิดซ็อกเก็ตที่มีอยู่สำหรับ IO
หมายเหตุ: มีซ็อกเก็ตประเภทอื่นอยู่ ซ็อกเก็ต TCP ที่โดดเด่นที่สุด แต่โพสต์นี้เกี่ยวข้องกับซ็อกเก็ตยูนิกซ์เท่านั้น คุณจะบอกความแตกต่างได้อย่างไร? ซ็อกเก็ต Unix มีชื่อไฟล์
ซ็อกเก็ตที่ง่ายที่สุด
เราจะดูสองโปรแกรมเล็กๆ น้อยๆ กัน
อย่างแรกคือ "เซิร์ฟเวอร์" มันเพียงแค่สร้างอินสแตนซ์ของ UnixServer
class แล้วใช้ server.accept
เพื่อรอการเชื่อมต่อ เมื่อได้รับการเชื่อมต่อก็จะแลกเปลี่ยนคำทักทาย
เป็นที่น่าสังเกตว่าทั้ง accept
และ readline
เมธอดจะบล็อกการทำงานของโปรแกรมจนกว่าจะได้รับสิ่งที่รอคอย
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
ดังนั้นเราจึงมีเซิร์ฟเวอร์ ตอนนี้ทั้งหมดที่เราต้องการคือลูกค้า
ในตัวอย่างด้านล่าง เราเปิดซ็อกเก็ตที่สร้างโดยเซิร์ฟเวอร์ของเรา จากนั้นเราจะใช้วิธี IO ปกติในการส่งและรับคำทักทาย
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
ในการสาธิต ก่อนอื่นเราต้องเปิดเซิร์ฟเวอร์ จากนั้นเราเรียกใช้ไคลเอนต์ คุณสามารถดูผลลัพธ์ด้านล่าง:
ตัวอย่างการโต้ตอบกับไคลเอ็นต์ซ็อกเก็ต/เซิร์ฟเวอร์ UNIX อย่างง่าย ลูกค้าอยู่ทางซ้าย เซิฟเวอร์อยู่ทางขวา
เชื่อมต่อกับ nginx
ตอนนี้เรารู้วิธีสร้าง "เซิร์ฟเวอร์" ซ็อกเก็ตยูนิกซ์แล้ว เราก็สามารถเชื่อมต่อกับ nginx ได้อย่างง่ายดาย
ไม่เชื่อฉัน? ลองทำการพิสูจน์แนวคิดอย่างรวดเร็ว ฉันจะปรับโค้ดด้านบนให้พิมพ์ทุกอย่างที่ได้รับจากซ็อกเก็ต
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
ตอนนี้ถ้าฉันกำหนดค่า nginx เพื่อส่งต่อคำขอไปยังซ็อกเก็ตที่ /tmp/socktest.sock
ฉันสามารถดูว่าข้อมูลที่ nginx กำลังส่งคืออะไร (ไม่ต้องกังวล เราจะหารือเกี่ยวกับการกำหนดค่าในไม่กี่นาที)
เมื่อฉันส่งคำขอทางเว็บ nginx จะส่งข้อมูลต่อไปนี้ไปยังเซิร์ฟเวอร์เล็กๆ ของฉัน:
สวยเย็น! เป็นเพียงคำขอ HTTP ปกติที่มีการเพิ่มส่วนหัวพิเศษสองสามรายการ ตอนนี้เราพร้อมที่จะสร้างเซิร์ฟเวอร์แอปจริงแล้ว แต่ก่อนอื่น เรามาพูดถึงการกำหนดค่า nginx กันก่อน
การติดตั้งและกำหนดค่า Nginx
หากคุณยังไม่ได้ติดตั้ง nginx บนเครื่องพัฒนาของคุณ ใช้เวลาสักครู่แล้วดำเนินการทันที OSX เป็นเรื่องง่ายมากผ่าน homebrew:
brew install nginx
ตอนนี้ เราต้องกำหนดค่า nginx เพื่อส่งต่อคำขอบน localhost:2048 ไปยังเซิร์ฟเวอร์อัปสตรีมผ่านซ็อกเก็ตชื่อ /tmp/socktest.sock
. ชื่อนั้นไม่ได้พิเศษอะไร เพียงแค่ต้องตรงกับชื่อซ็อกเก็ตที่ใช้โดยเว็บเซิร์ฟเวอร์ของเรา
คุณสามารถบันทึกการกำหนดค่านี้ไปที่ /tmp/nginx.conf
แล้วรัน nginx ด้วยคำสั่ง nginx -c /tmp/nginx.conf
เพื่อโหลด
# 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;
}
}
}
การกำหนดค่านี้ทำให้ nginx ทำงานเหมือนแอปเทอร์มินัลทั่วไป ไม่ใช่เหมือนภูต นอกจากนี้ยังเขียนบันทึกทั้งหมดไปยัง stdout เมื่อคุณเรียกใช้ nginx ควรมีลักษณะดังนี้:
Nginx ทำงานในโหมดที่ไม่ใช่ภูต
เซิร์ฟเวอร์แอปพลิเคชัน DIY
ตอนนี้เราได้เห็นวิธีเชื่อมต่อ nginx กับโปรแกรมของเราแล้ว การสร้างแอปพลิเคชันเซิร์ฟเวอร์อย่างง่ายเป็นเรื่องง่าย เมื่อ nginx ส่งต่อคำขอไปยังซ็อกเก็ตของเรา จะเป็นคำขอ HTTP มาตรฐาน หลังจากแก้ไขเล็กน้อย ฉันสามารถระบุได้ว่าซ็อกเก็ตส่งคืนการตอบสนอง HTTP ที่ถูกต้อง ซ็อกเก็ตนั้นจะแสดงในเบราว์เซอร์หรือไม่
แอปพลิเคชันด้านล่างรับคำขอและแสดงการประทับเวลา
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
ตอนนี้ถ้าฉันเปิด nginx และสคริปต์ของฉัน ฉันสามารถไปที่ localhost:2048 คำขอจะถูกส่งไปยังแอพของฉัน และการตอบสนองจะแสดงโดยเบราว์เซอร์ เจ๋งมาก!
คำขอ HTTP ถูกบันทึกไปยัง STDOUT โดยเซิร์ฟเวอร์แอปธรรมดาของเรา
และนี่คือผลงานอันรุ่งโรจน์ของเรา ดูเถิด! ประทับเวลา!
เซิร์ฟเวอร์ส่งคืนการประทับเวลาที่แสดงในเบราว์เซอร์