Ruby application servers มักใช้ร่วมกับ web server เช่น nginx เมื่อ user request หน้าจาก Rails app nginx จะส่ง request ให้ application server แต่ทำงานจริงอย่างไร? nginx คุยกับ unicorn ยังไง?
ตัวเลือกที่มีประสิทธิภาพคือใช้ unix sockets มาดูการทำงาน! โพสต์นี้เริ่มจากพื้นฐาน sockets จนสร้าง application server เองที่ถูก proxy โดย nginx
Sockets ให้โปรแกรมคุยกันเหมือนเขียน/อ่านไฟล์ Unicorn สร้าง socket และรอ connections Nginx connect ไป socket และคุยกับ Unicorn
Unix Socket คืออะไร?
Unix sockets ให้โปรแกรมหนึ่งคุยกับอีกตัวหนึ่งแบบคล้ายทำงานกับไฟล์ เป็นประเภทของ IPC (inter-process communication)
เพื่อให้เข้าถึงได้ผ่าน socket โปรแกรมสร้าง socket และ "save" ไว้บน disk เหมือนไฟล์ มัน monitor socket รอ incoming connections เมื่อได้ connection ใช้ standard IO methods อ่าน/เขียนข้อมูล
Ruby มีทุกอย่างสำหรับ unix sockets ผ่านคลาสสองตัว:
UNIXServer- สร้าง socket, save ลง disk, monitor connections ใหม่UNIXSocket- เปิด existing sockets สำหรับ IO
หมายเหตุ: มี sockets ชนิดอื่นๆ เช่น TCP sockets แต่โพสต์นี้เน้น unix sockets แยกกันยังไง? Unix sockets มีชื่อไฟล์
Socket เรียบง่ายที่สุด
เราจะดูสองโปรแกรมเล็กๆ
ตัวแรกคือ "server" สร้าง instance ของ UnixServer ใช้ server.accept รอ connection เมื่อได้ connection แลกเปลี่ยน greeting
ควรทราบว่า accept และ readline ทั้งคู่ block execution จนกว่าจะได้สิ่งที่รอ
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
ตอนมี server ต้องการ client
ตัวอย่างด้านล่างเปิด socket ที่ server สร้าง ใช้ IO methods ปกติส่ง/รับ greeting
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
ทดสอบ: รัน server ก่อน แล้วรัน client ผลลัพธ์:
ตัวอย่าง UNIX socket client/server interaction Client ด้านซ้าย Server ด้านขวา
Interface กับ nginx
ตอนรู้สร้าง unix socket "server" แล้ว interface กับ nginx ง่ายมาก
ไม่เชื่อ? ทดสอบ proof-of-concept เร็วๆ ปรับโค้ดข้างต้นให้ print ทุกอย่างที่รับจาก socket
require "socket"
# สร้าง socket และ "save" ลง filesystem
server = UNIXServer.new('/tmp/socktest.sock')
# รอ connection (โดย nginx)
socket = server.accept
# อ่านทุกอย่างจาก socket
while line = socket.readline
puts line.inspect
end
socket.close
ตอนนี้ config nginx forward requests ไป socket ที่ /tmp/socktest.sock (config จะอธิบายต่อไป)
เวลาทำ web request nginx ส่งข้อมูลมาให้ server เล็กๆ นี้:

เก๋! มันแค่ HTTP request ธรรมดาพร้อม headers เพิ่มเติม ตอนนี้พร้อมสร้าง real app server แต่มาคุย nginx config ก่อน
ติดตั้งและ Config Nginx
ยังไม่มี nginx บน development machine ติดตั้งเลย ง่ายบน macOS ผ่าน homebrew:
brew install nginx
config nginx forward requests บน localhost:2048 ไป upstream server ผ่าน socket /tmp/socktest.sock ชื่อนี้ไม่พิเศษ แค่ต้อง match กับ socket name ของ web server
save config ไว้ /tmp/nginx.conf รัน 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 access log to stdout
access_log /dev/stdout;
# Tell nginx external server @app อยู่ที่ 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;
# ถ้า path ไม่อยู่บน disk forward request ไป @app
try_files $uri/index.html $uri @app;
# Config options สำหรับ requests ที่ forward ไป @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;
}
}
}
Config นี้ทำให้ nginx รันเหมือน terminal app ไม่ใช่ daemon เขียน logs ไป stdout ตอนรัน nginx จะประมาณนี้:
Nginx รันใน non-daemon mode
DIY Application Server
ตอนรู้ connect nginx กับโปรแกรมแล้ว สร้าง simple application server ง่ายมาก nginx forward request มาที่ socket เป็น standard HTTP request หลังลองผิดลองถูก พบว่าถ้า socket คืน valid HTTP response จะแสดงใน browser ได้
Application ด้านล่างรับ request ใดๆ แสดง timestamp
require "socket"
# Connection สร้าง socket และ accept 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 log incoming requests และ render view ตอบกลับ
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 ให้ 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
ตอนนี้ run nginx และสคริปต์ ไป localhost:2048 Requests ส่งไป app Responses render บน browser เก๋!
HTTP requests log ไป STDOUT โดย simple app server
ผลงานของเรา: Timestamp!
Server คืน timestamp แสดงบน browser