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

สร้าง Ruby HTTP Server ในแค่ 30 บรรทัดโค้ด

Web servers และ HTTP โดยทั่วไปอาจดูยากเข้าใจ Browser format request ยังไง และ response ส่งไป user ยังไง? ใน Ruby Magic episode นี้เราจะเรียนรู้สร้าง Ruby HTTP server ใน 30 lines of code เมื่อเสร็จ server ของเราจะ handle HTTP GET requests และเราจะใช้ serve Rack app

How HTTP และ TCP ทำงานร่วมกัน

TCP is a transport protocol that describes how a server and a client exchange data

HTTP is a request-response protocol that specifically describes how web servers exchange data with HTTP clients or web browsers HTTP commonly uses TCP as its transport protocol ใน essence HTTP server คือ TCP server ที่ "speaks" 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 server example นี้ server bind ไป port 5678 และรอ client connect เมื่อเกิด มันส่ง message ไป client แล้ว close connection หลัง done talking กับ first client server รอ client อีกตัว connect ส่ง message อีกครั้ง

# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678
while line = server.gets
  puts line
end
server.close

เพื่อ connect ไป server เราต้องการ TCP client Example client นี้ connect ไป port เดียวกัน (5678) และใช้ server.gets รับ data จาก server ซึ่งพิมพ์ต่อ เมื่อหยุดรับ data มัน close connection ไป server และ program จะ exit

เมื่อ start server server running ($ ruby tcp_server.rb) คุณ start client ใน separate tab รับ message จาก server

$ ruby tcp_client.rb
Hello world! The time is 2016-11-23 15:17:11 +0100
$

ด้วย imagination เล็กน้อย TCP server และ client ของเราทำงานคล้าย web server และ browser Client ส่ง request server respond และ connection closed นั่นคือ request-response pattern works ซึ่งเป็นสิ่งที่เราต้องการสร้าง HTTP server

ก่อนไปส่วนดี มาดู HTTP requests และ responses เป็นอย่างไร

A Basic HTTP GET Request

Most basic HTTP GET request คือ request-line โดยไม่มี additional headers หรือ request body

GET / HTTP/1.1\r\n

Request-Line ประกอบ 4 ส่วน:

  • Method token (GET ในตัวอย่างนี้)
  • Request-URI (/)
  • Protocol version (HTTP/1.1)
  • CRLF (carriage return: \r, followed by line feed: \n) indicate end of line

Server จะ respond ด้วย HTTP response ซึ่งอาจเป็นแบบนี้:

HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n\Hello world!

Response นี้ประกอบด้วย:

  • Status line: protocol version ("HTTP/1.1"), ตามด้วย space, response's status code ("200"), terminated with CRLF (\r\n)
  • Optional header lines ในกรณีนี้มีแค่ one header line ("Content-Type: text/html") แต่อาจมี multiple (separated with CRLF: \r\n)
  • Newline (double CRLF) separate status line และ header จาก body: (\r\n\r\n)
  • Body: "Hello world!"

A Minimal Ruby HTTP Server

พอพูดแล้ว ตอนนี้เรารู้สร้าง TCP server ใน Ruby และ HTTP requests/responses เป็นอย่างไร เราสร้าง minimal HTTP server ได้ คุณจะสังเกต web server ดูเหมือน TCP server ที่พูดถึงก่อนหน้านี้ general idea เหมือนกัน เราแค่ใช้ HTTP protocol format message เพราะเราใช้ browser ส่ง requests และ parse responses เราไม่ต้อง implement client ครั้งนี้

# 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

หลัง server รับ request เหมือนก่อน มันใช้ session.print ส่ง message กลับ client: แทนที่จะแค่ message มัน prefix response ด้วย status line, header และ newline:

  1. Status line (HTTP 1.1 200\r\n) บอก browser ว่า HTTP version 1.1 และ response code "200"
  2. Header indicate response มี text/html content type (Content-Type: text/html\r\n)
  3. Newline (\r\n)
  4. Body: "Hello world! …"

เหมือนก่อน มัน close connection หลังส่ง message เราไม่ได้อ่าน request เลย มันแค่พิมพ์ไป console ตอนนี้

หาก start server และเปิด https://localhost:5678 ใน browser คุณควรเห็น "Hello world! …"-line พร้อม current time เหมือนเรารับจาก TCP client 🎉

Serving a Rack App

จนตอนนี้ server return single response สำหรับแต่ละ request เพื่อทำให้ useful มากขึ้น เราอาจเพิ่ม responses ให้ server แทนที่จะเพิ่มเข้า server โดยตรง เราจะใช้ Rack app Server จะ parse HTTP requests ส่งไป Rack app ซึ่งจะคืน response ให้ server ส่งกลับ client

Rack คือ interface ระหว่าง web servers ที่ support Ruby และ most Ruby web frameworks เช่น Rails และ Sinatra ใน simplest form Rack app คือ object ที่ responds to call และคืน "tiplet" array 3 items: HTTP response code, hash of HTTP headers และ body

app = Proc.new do |env|
  ['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end

ในตัวอย่างนี้ response code "200" เราส่ง "text/html" เป็น content type ผ่าน headers และ body เป็น array พร้อม string

เพื่อให้ server serve responses จาก app นี้ เราต้อง turn returned triplet เป็น HTTP response string แทนที่จะ return static response เสมอเหมือนก่อน เราต้อง build response จาก triplet ที่ Rack app คืน

# 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

เพื่อ serve response ที่รับจาก Rack app มี changes เราทำกับ server:

  1. Get status code, headers, และ body จาก triplet ที่คืนโดย app.call
  2. Use status code build status line
  3. Loop over headers เพิ่ม header line สำหรับแต่ละ key-value pair ใน hash
  4. Print newline separate status line และ headers จาก body
  5. Loop over body print each part เนื่องจากมีแค่ one part ใน body array มันแค่พิมพ์ "Hello world" message ไป session ก่อน close

Reading Requests

จนตอนนี้ server ignore request variable เราไม่ต้อง เพราะ Rack app คืน response เดียวกันเสมอ

Rack::Lobster เป็น example app ที่ ship with Rack และใช้ request URL parameters เพื่อ function แทน Proc ที่เราใช้เป็น app ก่อน เราจะใช้มันเป็น testing app ตั้งแต่ตอนนี้

# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
while session = server.accept
  # ...

เปิด browser ตอนนี้จะแสดง lobster แทน boring string ที่พิมพ์ก่อน Lobstericious!

"flip!" และ "crash!" links link ไป /?flip=left และ /?flip=crash ตามลำดับ อย่างไรก็ตาม เมื่อ follow links lobster ไม่ flip และไม่ crash yet นั่นเพราะ server ไม่ handle query strings ตอนนี้ จำได้ request variable ที่เรา ignore ก่อน? หากดู server logs เราจะเห็น request strings สำหรับแต่ละ page

GET / HTTP/1.1
GET /?flip=left HTTP/1.1
GET /?flip=crash HTTP/1.1

HTTP request strings รวม request method ("GET"), request path (/, /?flip=left และ /?flip=crash), และ HTTP version เราใช้ information นี้ determine ว่า serve อะไร

# 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

เพื่อ parse request และส่ง request parameters ไป Rack app เรา split request string และส่งไป Rack app:

  1. Split request string เป็น method และ full path
  2. Split full path เป็น path และ query
  3. ส่งไป app ใน Rack environment hash

ตัวอย่าง request อย่าง GET /?flip=left HTTP/1.1\r\n จะส่งไป app แบบนี้:

{
  'REQUEST_METHOD' => 'GET',
  'PATH_INFO' => '/',
  'QUERY_STRING' => '?flip=left'
}

Restart server เปิด https://localhost:5678 และกด "flip!"-link จะ flip lobster และกด "crash!" link จะ crash web server

เราเพิ่งขูด surface ของ implementing HTTP server และของเราแค่ 30 lines of code แต่解釋 basic idea มัน accept GET requests ส่ง request's attributes ไป Rack app และส่ง responses กลับ browser แม้ไม่ handle request streaming และ POST requests server ของเราอาจ theoretically ใช้ serve Rack apps อื่นได้

นี่คือสรุป quick look สร้าง HTTP server ใน Ruby หากอยากเล่นกับ server ของเรา นี่คือโค้ด บอกเราที่ @AppSignal หากอยากรู้เพิ่ม หรือมีคำถามเฉพาะ

หากชอบ article นี้ subscribe Ruby Magic newsletter: a (roughly) monthly po(r)tion of Ruby.