Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การสร้างเซิร์ฟเวอร์ HTTP 30 บรรทัดใน Ruby

เว็บเซิร์ฟเวอร์และ 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 เพื่อส่งข้อความกลับไปยังลูกค้า:แทนที่จะเป็นเพียงข้อความของเรา มันนำหน้าการตอบกลับด้วยบรรทัดสถานะ ส่วนหัว และบรรทัดใหม่:

  1. บรรทัดสถานะ (HTTP 1.1 200\r\n ) เพื่อบอกเบราว์เซอร์ว่าเวอร์ชัน HTTP คือ 1.1 และรหัสตอบกลับคือ "200"
  2. ส่วนหัวเพื่อระบุว่าการตอบกลับมีประเภทเนื้อหาข้อความ/html (Content-Type: text/html\r\n )
  3. ขึ้นบรรทัดใหม่ (\r\n )
  4. ร่างกาย:"สวัสดีชาวโลก! …"

เหมือนเมื่อก่อนมันปิดการเชื่อมต่อหลังจากส่งข้อความ เรายังไม่ได้อ่านคำขอจึงพิมพ์ไปที่คอนโซลในตอนนี้

หากคุณเริ่มเซิร์ฟเวอร์และเปิด 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 เราจะทำการเปลี่ยนแปลงบางอย่างกับเซิร์ฟเวอร์ของเรา:

  1. รับรหัสสถานะ ส่วนหัว และเนื้อหาจาก triplet ที่ส่งคืนโดย app.call .
  2. ใช้รหัสสถานะเพื่อสร้างบรรทัดสถานะ
  3. วนรอบส่วนหัวและเพิ่มบรรทัดส่วนหัวสำหรับคู่คีย์-ค่าแต่ละคู่ในแฮช
  4. พิมพ์ขึ้นบรรทัดใหม่เพื่อแยกบรรทัดสถานะและส่วนหัวออกจากเนื้อหา
  5. วนรอบลำตัวและพิมพ์แต่ละส่วน เนื่องจากมีเพียงส่วนเดียวใน 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:

  1. แยกสตริงคำขอเป็นเมธอดและพาธแบบเต็ม
  2. แบ่งพาธแบบเต็มเป็นพาธและเคียวรี
  3. ส่งต่อสิ่งเหล่านั้นไปยังแอปของเราในแฮชของสภาพแวดล้อมแบบแร็ค

ตัวอย่างเช่น คำขอเช่น 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