เว็บเซิร์ฟเวอร์และ HTTP โดยทั่วไปอาจดูเหมือนเข้าใจยาก เบราว์เซอร์จัดรูปแบบคำขออย่างไร และคำตอบจะถูกส่งไปยังผู้ใช้อย่างไร ในตอน Ruby Magic นี้ เราจะเรียนรู้วิธีสร้างเซิร์ฟเวอร์ Ruby HTTP ในโค้ด 30 บรรทัด เมื่อเสร็จแล้ว เซิร์ฟเวอร์ของเราจะจัดการคำขอ HTTP GET และเราจะใช้เพื่อให้บริการแอป Rack
วิธีที่ HTTP และ TCP ทำงานร่วมกัน
TCP เป็นโปรโตคอลการขนส่งที่อธิบายวิธีที่เซิร์ฟเวอร์และไคลเอนต์แลกเปลี่ยนข้อมูล
HTTP เป็นโปรโตคอลตอบสนองคำขอที่อธิบายวิธีที่เว็บเซิร์ฟเวอร์แลกเปลี่ยนข้อมูลกับไคลเอนต์ HTTP หรือเว็บเบราว์เซอร์โดยเฉพาะ HTTP มักใช้ TCP เป็นโปรโตคอลการขนส่ง โดยพื้นฐานแล้ว เซิร์ฟเวอร์ HTTP คือเซิร์ฟเวอร์ TCP ที่ "พูด" 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
ในตัวอย่างนี้ของเซิร์ฟเวอร์ TCP เซิร์ฟเวอร์จะผูกกับพอร์ต 5678
และรอให้ลูกค้าเชื่อมต่อ เมื่อสิ่งนี้เกิดขึ้น มันจะส่งข้อความไปยังไคลเอนต์ แล้วปิดการเชื่อมต่อ หลังจากที่พูดคุยกับไคลเอนต์แรกเสร็จแล้ว เซิร์ฟเวอร์จะรอให้ไคลเอนต์อื่นเชื่อมต่อเพื่อส่งข้อความถึงอีกครั้ง
# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678
while line = server.gets
puts line
end
server.close
ในการเชื่อมต่อกับเซิร์ฟเวอร์ของเรา เราจำเป็นต้องมีไคลเอนต์ TCP ไคลเอนต์ตัวอย่างนี้เชื่อมต่อกับพอร์ตเดียวกัน (5678
) และใช้ server.gets
เพื่อรับข้อมูลจากเซิร์ฟเวอร์ซึ่งจะถูกพิมพ์ออกมา เมื่อหยุดรับข้อมูล จะปิดการเชื่อมต่อกับเซิร์ฟเวอร์และโปรแกรมจะออก
เมื่อคุณเริ่มเซิร์ฟเวอร์เซิร์ฟเวอร์กำลังทำงาน ($ ruby tcp_server.rb
) คุณสามารถเริ่มไคลเอนต์ในแท็บแยกต่างหากเพื่อรับข้อความของเซิร์ฟเวอร์
$ ruby tcp_client.rb
Hello world! The time is 2016-11-23 15:17:11 +0100
$
ด้วยจินตนาการเล็กน้อย เซิร์ฟเวอร์ TCP และไคลเอนต์ของเราทำงานเหมือนกับเว็บเซิร์ฟเวอร์และเบราว์เซอร์ ไคลเอนต์ส่งคำขอ เซิร์ฟเวอร์ตอบสนอง และการเชื่อมต่อถูกปิด นั่นคือวิธีการทำงานของรูปแบบการตอบกลับคำขอ ซึ่งเป็นสิ่งที่เราจำเป็นในการสร้างเซิร์ฟเวอร์ HTTP
ก่อนที่เราจะไปถึงส่วนที่ดี เรามาดูกันว่าคำขอ HTTP และการตอบสนองเป็นอย่างไร
คำขอ HTTP GET พื้นฐาน
คำขอ HTTP GET พื้นฐานที่สุดคือสายคำขอที่ไม่มีส่วนหัวหรือเนื้อหาคำขอเพิ่มเติม
GET / HTTP/1.1\r\n
Request-Line ประกอบด้วยสี่ส่วน:
- โทเค็นวิธีการ (
GET
, ในตัวอย่างนี้) - คำขอ-URI (
/
) - เวอร์ชันโปรโตคอล (
HTTP/1.1
) - A CRLF (การขึ้นบรรทัดใหม่:
\r
ตามด้วยฟีดบรรทัด:\n
) เพื่อระบุจุดสิ้นสุดของบรรทัด
เซิร์ฟเวอร์จะตอบสนองด้วยการตอบสนอง HTTP ซึ่งอาจมีลักษณะดังนี้:
HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n\Hello world!
คำตอบนี้ประกอบด้วย:
- บรรทัดสถานะ:เวอร์ชันโปรโตคอล ("HTTP/1.1") ตามด้วยช่องว่าง รหัสสถานะของการตอบสนอง ("200") และสิ้นสุดด้วย CRLF (
\r\n
) - ตัวเลือกบรรทัดส่วนหัว ในกรณีนี้ มีหัวเรื่องเพียงบรรทัดเดียว ("Content-Type:text/html") แต่อาจมีได้หลายบรรทัด (คั่นด้วย CRLF:
\r\n
) - ขึ้นบรรทัดใหม่ (หรือ CRLF คู่) เพื่อแยกบรรทัดสถานะและส่วนหัวออกจากเนื้อหา:(
\r\n\r\n
) - ร่างกาย:"สวัสดีชาวโลก!"
เซิร์ฟเวอร์ HTTP Ruby ขั้นต่ำ
คุยกันพอ ตอนนี้เรารู้วิธีสร้างเซิร์ฟเวอร์ TCP ใน Ruby แล้วและคำขอ HTTP และการตอบสนองเป็นอย่างไร เราก็สามารถสร้างเซิร์ฟเวอร์ HTTP ขั้นต่ำได้ คุณจะสังเกตเห็นว่าเว็บเซิร์ฟเวอร์ส่วนใหญ่มีลักษณะเหมือนกับเซิร์ฟเวอร์ TCP ที่เรากล่าวถึงก่อนหน้านี้ แนวคิดทั่วไปก็เหมือนกัน เราแค่ใช้โปรโตคอล HTTP เพื่อจัดรูปแบบข้อความของเรา นอกจากนี้ เนื่องจากเราจะใช้เบราว์เซอร์เพื่อส่งคำขอและแยกวิเคราะห์คำตอบ เราจึงไม่ต้องติดตั้งไคลเอนต์ในครั้งนี้
# 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
หลังจากที่เซิร์ฟเวอร์ได้รับคำขอเหมือนเมื่อก่อน เซิร์ฟเวอร์จะใช้ session.print
เพื่อส่งข้อความกลับไปยังลูกค้า:แทนที่จะเป็นเพียงข้อความของเรา มันนำหน้าการตอบกลับด้วยบรรทัดสถานะ ส่วนหัว และบรรทัดใหม่:
- บรรทัดสถานะ (
HTTP 1.1 200\r\n
) เพื่อบอกเบราว์เซอร์ว่าเวอร์ชัน HTTP คือ 1.1 และรหัสตอบกลับคือ "200" - ส่วนหัวเพื่อระบุว่าการตอบกลับมีประเภทเนื้อหาข้อความ/html (
Content-Type: text/html\r\n
) - ขึ้นบรรทัดใหม่ (
\r\n
) - ร่างกาย:"สวัสดีชาวโลก! …"
เหมือนเมื่อก่อนมันปิดการเชื่อมต่อหลังจากส่งข้อความ เรายังไม่ได้อ่านคำขอจึงพิมพ์ไปที่คอนโซลในตอนนี้
หากคุณเริ่มเซิร์ฟเวอร์และเปิด https://localhost:5678 ในเบราว์เซอร์ของคุณ คุณจะเห็น "Hello world! …"-สอดคล้องกับเวลาปัจจุบัน เหมือนที่เราได้รับจากไคลเอนต์ TCP ของเราก่อนหน้านี้ 🎉
การให้บริการแอปแร็ค
จนถึงขณะนี้ เซิร์ฟเวอร์ของเราได้ส่งคืนการตอบกลับเดียวสำหรับแต่ละคำขอ เพื่อให้มีประโยชน์มากขึ้นอีกเล็กน้อย เราอาจเพิ่มการตอบกลับไปยังเซิร์ฟเวอร์ของเรา แทนที่จะเพิ่มสิ่งเหล่านี้ไปยังเซิร์ฟเวอร์โดยตรง เราจะใช้แอป Rack เซิร์ฟเวอร์ของเราจะแยกวิเคราะห์คำขอ HTTP และส่งไปยังแอป Rack ซึ่งจะส่งคืนการตอบกลับสำหรับเซิร์ฟเวอร์เพื่อส่งกลับไปยังไคลเอนต์
Rack เป็นอินเทอร์เฟซระหว่างเว็บเซิร์ฟเวอร์ที่รองรับ Ruby และเว็บเฟรมเวิร์กของ Ruby ส่วนใหญ่ เช่น Rails และ Sinatra ในรูปแบบที่ง่ายที่สุด แอป Rack เป็นวัตถุที่ตอบสนองต่อ call
และส่งคืน "tiplet" อาร์เรย์ที่มีสามรายการ:รหัสตอบกลับ HTTP แฮชของส่วนหัว HTTP และเนื้อหา
app = Proc.new do |env|
['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
ในตัวอย่างนี้ รหัสตอบกลับคือ "200" เรากำลังส่ง "text/html" เป็นประเภทเนื้อหาผ่านส่วนหัว และเนื้อหาเป็นอาร์เรย์ที่มีสตริง
เพื่อให้เซิร์ฟเวอร์ของเราแสดงการตอบกลับจากแอปนี้ เราจะต้องเปลี่ยน triplet ที่ส่งคืนเป็นสตริงการตอบกลับ HTTP แทนที่จะส่งคืนการตอบกลับแบบคงที่เหมือนที่เคยทำก่อนหน้านี้ เราจะต้องสร้างการตอบสนองจาก triplet ที่ส่งคืนโดยแอป 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
เพื่อตอบสนองการตอบกลับที่เราได้รับจากแอป Rack เราจะทำการเปลี่ยนแปลงบางอย่างกับเซิร์ฟเวอร์ของเรา:
- รับรหัสสถานะ ส่วนหัว และเนื้อหาจาก triplet ที่ส่งคืนโดย
app.call
. - ใช้รหัสสถานะเพื่อสร้างบรรทัดสถานะ
- วนรอบส่วนหัวและเพิ่มบรรทัดส่วนหัวสำหรับคู่คีย์-ค่าแต่ละคู่ในแฮช
- พิมพ์ขึ้นบรรทัดใหม่เพื่อแยกบรรทัดสถานะและส่วนหัวออกจากเนื้อหา
- วนรอบลำตัวและพิมพ์แต่ละส่วน เนื่องจากมีเพียงส่วนเดียวใน body array ของเรา จึงเพียงแค่พิมพ์ข้อความ "Hello world" ของเราไปยังเซสชันก่อนที่จะปิด
คำขออ่าน
จนถึงขณะนี้ เซิร์ฟเวอร์ของเราได้เพิกเฉยต่อ request
ตัวแปร. เราไม่ต้องการเพราะแอป Rack ของเราตอบกลับการตอบกลับแบบเดิมเสมอ
Rack::Lobster
เป็นแอปตัวอย่างที่มาพร้อมกับ Rack และใช้พารามิเตอร์ URL คำขอเพื่อให้ทำงานได้ แทน Proc ที่เราเคยเป็นแอปมาก่อน เราจะใช้เป็นแอปทดสอบของเราต่อจากนี้ไป
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
while session = server.accept
# ...
การเปิดเบราว์เซอร์จะแสดงกุ้งมังกรแทนที่จะเป็นสตริงที่น่าเบื่อที่พิมพ์มาก่อน กุ้งมังกร!
"พลิก!" และ "พัง!" ลิงค์ ลิงค์ไปยัง /?flip=left
และ /?flip=crash
ตามลำดับ อย่างไรก็ตาม เมื่อติดตามลิงก์ กุ้งก้ามกรามไม่พลิกและยังไม่มีอะไรขัดข้อง นั่นเป็นเพราะเซิร์ฟเวอร์ของเราไม่ได้จัดการสตริงการสืบค้นในขณะนี้ จำ request
ตัวแปรที่เราละเลยมาก่อน? หากเราดูบันทึกของเซิร์ฟเวอร์ เราจะเห็นสตริงคำขอสำหรับแต่ละหน้า
GET / HTTP/1.1
GET /?flip=left HTTP/1.1
GET /?flip=crash HTTP/1.1
สตริงคำขอ HTTP ประกอบด้วยวิธีการร้องขอ ("GET") เส้นทางคำขอ (/
, /?flip=left
และ /?flip=crash
) และเวอร์ชัน HTTP เราสามารถใช้ข้อมูลนี้เพื่อกำหนดสิ่งที่เราต้องให้บริการ
# 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
หากต้องการแยกวิเคราะห์คำขอและส่งพารามิเตอร์คำขอไปยังแอป Rack เราจะแยกสตริงคำขอและส่งไปยังแอป Rack:
- แยกสตริงคำขอเป็นเมธอดและพาธแบบเต็ม
- แบ่งพาธแบบเต็มเป็นพาธและเคียวรี
- ส่งต่อสิ่งเหล่านั้นไปยังแอปของเราในแฮชของสภาพแวดล้อมแบบแร็ค
ตัวอย่างเช่น คำขอเช่น GET /?flip=left HTTP/1.1\r\n
จะถูกส่งต่อไปยังแอปดังนี้:
{
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/',
'QUERY_STRING' => '?flip=left'
}
การรีสตาร์ทเซิร์ฟเวอร์ของเรา ไปที่ https://localhost:5678 และคลิกลิงก์ "พลิก!" ตอนนี้จะพลิกล็อบสเตอร์ และคลิก "แครช!" ลิงก์จะทำให้เว็บเซิร์ฟเวอร์ของเราขัดข้อง
เราเพิ่งขีดข่วนพื้นผิวของการติดตั้งเซิร์ฟเวอร์ HTTP และโค้ดของเรามีเพียง 30 บรรทัด แต่มันอธิบายแนวคิดพื้นฐาน ยอมรับคำขอ GET ส่งแอตทริบิวต์คำขอไปยังแอป Rack และส่งการตอบกลับไปยังเบราว์เซอร์ แม้ว่าจะไม่ได้จัดการสิ่งต่างๆ เช่น คำขอสตรีมมิงและคำขอ POST แต่เซิร์ฟเวอร์ของเราก็สามารถใช้ในทางทฤษฎีเพื่อให้บริการแอป Rack อื่นๆ ได้เช่นกัน
นี่เป็นการสรุปภาพรวมอย่างรวดเร็วของเราในการสร้างเซิร์ฟเวอร์ HTTP ใน Ruby หากคุณต้องการเล่นกับเซิร์ฟเวอร์ของเรา นี่คือรหัส แจ้งให้เราทราบที่ @AppSignal หากคุณต้องการทราบข้อมูลเพิ่มเติมหรือมีคำถามเฉพาะ
หากคุณชอบบทความนี้ โปรดสมัครรับจดหมายข่าว Ruby Magic:ข้อเสนอรายเดือน (โดยประมาณ) ของ Ruby