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

Running Rack:วิธีที่เซิร์ฟเวอร์ Ruby HTTP เรียกใช้แอป Rails

ในซีรีส์ Ruby Magic เราชอบที่จะแยกซอฟต์แวร์ออกจากกันเพื่อเรียนรู้วิธีทำงานภายใต้ประทุน มันคือทั้งหมดที่เกี่ยวกับกระบวนการ ผลลัพธ์สุดท้ายไม่ใช่สิ่งที่คุณจะใช้ในการผลิต เราเรียนรู้เกี่ยวกับการทำงานภายในของภาษา Ruby และไลบรารียอดนิยม เราเผยแพร่บทความใหม่ประมาณเดือนละครั้ง ดังนั้นอย่าลืมสมัครรับจดหมายข่าวของเราหากคุณชอบเรื่องแบบนี้ด้วย

ใน Ruby Magic รุ่นก่อนหน้า เราได้ติดตั้งเซิร์ฟเวอร์ HTTP 30 บรรทัดใน Ruby โดยไม่ต้องเขียนโค้ดจำนวนมาก เราสามารถจัดการคำขอ HTTP GET และให้บริการแอปพลิเคชัน Rack อย่างง่ายได้ คราวนี้ เราจะพาเซิร์ฟเวอร์ทำเองของเราไปอีกหน่อย เมื่อเสร็จแล้ว เราจะมีเว็บเซิร์ฟเวอร์ที่สามารถให้บริการบล็อกที่มีชื่อเสียงของ Rails เป็นเวลา 15 นาที ซึ่งช่วยให้คุณสร้าง อัปเดต และลบโพสต์ได้

จุดที่เราค้างไว้

ครั้งที่แล้ว เราได้ติดตั้งเซิร์ฟเวอร์ให้เพียงพอสำหรับให้บริการ Rack::Lobster เป็นตัวอย่างแอปพลิเคชัน

  1. การใช้งานของเราได้เปิดเซิร์ฟเวอร์ TCP และรอคำขอเข้ามา
  2. เมื่อสิ่งนั้นเกิดขึ้น บรรทัดคำขอ (GET /?flip=left HTTP/1.1\r\n ) ถูกแยกวิเคราะห์เพื่อรับวิธีการร้องขอ (GET ) เส้นทาง (/ ) และพารามิเตอร์การค้นหา (flip=left )
  3. วิธีการขอ พาธ และสตริงการสืบค้นถูกส่งไปยังแอป Rack ซึ่งส่งคืน Triplet พร้อมสถานะ ส่วนหัวการตอบกลับบางส่วน และเนื้อหาการตอบกลับ
  4. เมื่อใช้สิ่งเหล่านี้ เราสามารถสร้างการตอบสนอง HTTP เพื่อส่งกลับไปยังเบราว์เซอร์ ก่อนที่จะปิดการเชื่อมต่อเพื่อรอคำขอใหม่เข้ามา
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
 
app = Rack::Lobster.new
server = TCPServer.new 5678
 
#1
while session = server.accept
  request = session.gets
  puts request
 
  #2
  method, full_path = request.split(' ')
  path, query = full_path.split('?')
 
  #3
  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query
  })
 
  #4
  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

เราจะดำเนินการต่อด้วยรหัสที่เราเขียนครั้งล่าสุด หากคุณต้องการปฏิบัติตาม นี่คือรหัสที่เราได้ลงท้ายด้วย

ชั้นวางและราง

เฟรมเวิร์กของ Ruby เช่น Rails และ Sinatra นั้นสร้างขึ้นบนอินเทอร์เฟซของ Rack เช่นเดียวกับตัวอย่าง Rack::Lobster เรากำลังทดสอบเซิร์ฟเวอร์ของเราตอนนี้ Rails.application . ของ Rails เป็นวัตถุแอปพลิเคชัน Rack ตามทฤษฎีแล้ว นี่หมายความว่าเซิร์ฟเวอร์ของเราควรจะสามารถให้บริการแอปพลิเคชัน Rails ได้แล้ว

เพื่อทดสอบว่า ฉันได้เตรียมแอปพลิเคชัน Rails อย่างง่าย มาโคลนมันในไดเร็กทอรีเดียวกันกับเซิร์ฟเวอร์ของเรากันเถอะ

$ ls
http_server.rb
$ git clone https://github.com/jeffkreeftmeijer/wups.git blog
Cloning into 'blog'...
remote: Counting objects: 162, done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 162 (delta 32), reused 162 (delta 32), pack-reused 0
Receiving objects: 100% (162/162), 29.09 KiB | 0 bytes/s, done.
Resolving deltas: 100% (32/32), done.
Checking connectivity... done.
$ ls
blog           http_server.rb

จากนั้นในเซิร์ฟเวอร์ของเรา ต้องใช้ไฟล์สภาพแวดล้อมของแอปพลิเคชัน Rails แทน rack และ rack/lobster และใส่ Rails.application ใน app ตัวแปรแทน Rack::Lobster.new .

# http_server.rb
require 'socket'
require_relative 'blog/config/environment'
 
app = Rails.application
server = TCPServer.new 5678
# ...

การเริ่มต้นเซิร์ฟเวอร์ (ruby http_server.rb ) และการเปิด https://localhost:5678 แสดงให้เราเห็นว่าเรายังไปไม่ถึงจุดนั้น เซิร์ฟเวอร์ไม่ขัดข้อง แต่เราได้รับการต้อนรับด้วยข้อผิดพลาดเซิร์ฟเวอร์ภายในในเบราว์เซอร์

เมื่อตรวจสอบบันทึกของเซิร์ฟเวอร์ เราจะพบว่ามีบางสิ่งที่เรียกว่า rack.input ขาดหายไป . ปรากฎว่าเราขี้เกียจในขณะที่ติดตั้งเซิร์ฟเวอร์ของเราครั้งล่าสุด ดังนั้นจึงยังมีงานอีกมากที่ต้องทำก่อนที่เราจะสามารถให้แอปพลิเคชัน Rails นี้ทำงานได้

$ ruby http_server.rb
GET / HTTP/1.1
Error during failsafe response: Missing rack.input
  ...
  http_server.rb:15:in `<main>'

สภาพแวดล้อมของแร็ค

ย้อนกลับไปเมื่อเราใช้งานเซิร์ฟเวอร์ของเรา เราได้มองข้ามสภาพแวดล้อมของ Rack และละเว้นตัวแปรส่วนใหญ่ที่จำเป็นในการให้บริการแอปพลิเคชัน Rack อย่างเหมาะสม เราลงเอยด้วยการใช้ REQUEST_METHOD . เท่านั้น , PATH_INFO และ QUERY_STRING ตัวแปร เนื่องจากสิ่งเหล่านี้เพียงพอสำหรับแอป Rack แบบง่ายของเรา

ดังที่เราได้เห็นจากข้อยกเว้นเมื่อเราพยายามเริ่มแอปพลิเคชันใหม่ Rails ต้องการ rack.input ซึ่งใช้เป็นสตรีมอินพุตสำหรับข้อมูล HTTP POST ดิบ นอกจากนั้น ยังมีตัวแปรอื่นๆ ที่เราต้องส่งผ่าน เช่น หมายเลขพอร์ตของเซิร์ฟเวอร์ และข้อมูลคำขอคุกกี้

โชคดีที่ Rack มี Rack::Lint เพื่อช่วยให้แน่ใจว่าตัวแปรทั้งหมดในสภาพแวดล้อมแร็คมีอยู่และถูกต้อง เราสามารถใช้มันเพื่อทดสอบเซิร์ฟเวอร์ของเราโดยใส่แอป Rails เข้าไปโดยเรียก Rack::Lint.new และส่งผ่าน Rails.application .

# http_server.rb
require 'socket'
require_relative 'blog/config/environment'
 
app = Rack::Lint.new(Rails.application)
server = TCPServer.new 5678
# ...

Rack::Lint จะโยนข้อยกเว้นเมื่อตัวแปรในสภาพแวดล้อมหายไปหรือไม่ถูกต้อง ตอนนี้ การเริ่มเซิร์ฟเวอร์ของเราอีกครั้งและเปิด https://localhost:5678 จะทำให้เซิร์ฟเวอร์ขัดข้องและ Rack::Lint จะแจ้งให้เราทราบถึงข้อผิดพลาดแรก:SERVER_NAME ไม่ได้ตั้งค่าตัวแปร

~/Appsignal/http-server (master) $ ruby http_server.rb
GET / HTTP/1.1
/Users/jeff/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.1/lib/rack/lint.rb:20:in `assert': env missing required key SERVER_NAME (Rack::Lint::LintError)
        ...
        from http_server.rb:15:in `<main>'

โดยการแก้ไขข้อผิดพลาดที่เราส่งเข้ามา เราสามารถเพิ่มตัวแปรได้เรื่อยๆ จนถึง Rack::Lint หยุดหยุดเซิร์ฟเวอร์ของเรา มาดูตัวแปรแต่ละตัวกัน Rack::Lint จำเป็น

  • SERVER_NAME :ชื่อโฮสต์ของเซิร์ฟเวอร์ เรากำลังเรียกใช้เซิร์ฟเวอร์นี้ในเครื่องเท่านั้น ดังนั้นเราจะใช้ "localhost"
  • SERVER_PORT :พอร์ตที่เซิร์ฟเวอร์ของเราทำงานอยู่ เราได้ฮาร์ดโค้ดหมายเลขพอร์ตแล้ว (5678) ดังนั้นเราจะส่งต่อไปยังสภาพแวดล้อมของแร็ค
  • rack.version :แร็คเป้าหมาย โปรโตคอล หมายเลขเวอร์ชันเป็นอาร์เรย์ของจำนวนเต็ม [1,3] ในขณะที่เขียน
  • rack.input :อินพุตสตรีมที่มีข้อมูลโพสต์ HTTP ดิบ เราจะพูดถึงเรื่องนี้ในภายหลัง แต่เราจะส่ง StringIO ที่ว่างเปล่า อินสแตนซ์ (ด้วยการเข้ารหัส ASCII-8BIT) สำหรับตอนนี้
  • rack.errors :สตรีมข้อผิดพลาดสำหรับ Rack::Logger ที่จะเขียนถึง เรากำลังใช้ $stderr .
  • rack.multithread :เซิร์ฟเวอร์ของเราเป็นแบบเธรดเดียว จึงสามารถตั้งค่าเป็น false .
  • rack.multiprocess :เซิร์ฟเวอร์ของเรากำลังทำงานอยู่ในกระบวนการเดียว จึงสามารถตั้งค่าเป็น false เช่นกัน
  • rack.run_once :เซิร์ฟเวอร์ของเราสามารถจัดการคำขอตามลำดับได้หลายรายการในกระบวนการเดียว ดังนั้นนี่คือ false ด้วย
  • rack.url_scheme :ไม่รองรับ SSL จึงสามารถตั้งค่าเป็น "http" แทน "https" ได้

หลังจากเพิ่มตัวแปรที่หายไปทั้งหมดแล้ว Rack::Lint จะแจ้งให้เราทราบถึงปัญหาอื่นในสภาพแวดล้อมของเรา

$ ruby http_server.rb
GET / HTTP/1.1
/Users/jeff/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.1/lib/rack/lint.rb:20:in `assert': env variable QUERY_STRING has non-string value nil (Rack::Lint::LintError)
        ...
        from http_server.rb:18:in `<main>'

เมื่อไม่มีสตริงการสืบค้นในคำขอ เราจะส่ง nil เป็น QUERY_STRING ซึ่งไม่ได้รับอนุญาต ในกรณีนั้น Rack ต้องการสตริงว่างแทน หลังจากใช้ตัวแปรที่หายไปและอัปเดตสตริงการสืบค้น สภาพแวดล้อมของเราจะเป็นดังนี้:

# http_server.rb
# ...
  method, full_path = request.split(' ')
  path, query = full_path.split('?')
 
  input = StringIO.new
  input.set_encoding 'ASCII-8BIT'
 
  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query || '',
    'SERVER_NAME' => 'localhost',
    'SERVER_PORT' => '5678',
    'rack.version' => [1,3],
    'rack.input' => input,
    'rack.errors' => $stderr,
    'rack.multithread' => false,
    'rack.multiprocess' => false,
    'rack.run_once' => false,
    'rack.url_scheme' => 'http'
  })
 
  session.print "HTTP/1.1 #{status}\r\n"
# ...

เมื่อรีสตาร์ทเซิร์ฟเวอร์และไปที่ https://localhost:5678 อีกครั้ง เราจะพบกับหน้า "You're on Rails!" ของ Rails ซึ่งหมายความว่าตอนนี้เรากำลังเรียกใช้แอปพลิเคชัน Rails จริงบนเซิร์ฟเวอร์ทำเองของเรา!

การแยกวิเคราะห์เนื้อหา HTTP POST

แอปพลิเคชันนี้เป็นมากกว่าหน้าดัชนีนั้น การเยี่ยมชม https://localhost:5678/posts จะแสดงรายการโพสต์ว่าง หากเราพยายามสร้างโพสต์ใหม่โดยกรอกแบบฟอร์มโพสต์ใหม่และกด "สร้างโพสต์" คุณจะได้รับการต้อนรับด้วย ActionController::InvalidAuthenticityToken ข้อยกเว้น

โทเค็นความถูกต้องจะถูกส่งไปพร้อมกับการโพสต์แบบฟอร์มและใช้เพื่อตรวจสอบว่าคำขอมาจากแหล่งที่เชื่อถือได้หรือไม่ เซิร์ฟเวอร์ของเราเพิกเฉยต่อข้อมูล POST โดยสิ้นเชิงในขณะนี้ ดังนั้นจึงไม่ส่งโทเค็น และไม่สามารถตรวจสอบคำขอได้

ย้อนกลับไปเมื่อเราติดตั้งเซิร์ฟเวอร์ HTTP ครั้งแรก เราใช้ session.gets เพื่อรับบรรทัดแรก (เรียกว่า Request-Line) และแยกวิเคราะห์เมธอด HTTP และพาธจากนั้น นอกจากการแยกวิเคราะห์ Request-Line แล้ว เรายังเพิกเฉยต่อคำขอที่เหลือ

เพื่อให้สามารถดึงข้อมูล POST ได้ ก่อนอื่นเราต้องเข้าใจว่าคำขอ HTTP มีโครงสร้างอย่างไร เมื่อดูตัวอย่าง เราจะเห็นว่าโครงสร้างคล้ายกับการตอบสนองของ HTTP:

POST /posts HTTP/1.1\r\n
Host: localhost:5678\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: en-us\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Origin: https://localhost:5678\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.14\r\n
Cookie: _wups_session=LzE0Z2hSZFNseG5TR3dEVEwzNE52U0lFa0pmVGlQZGtZR3AveWlyMEFvUHRPeXlQUzQ4L0xlKzNLVWtqYld2cjdiWkpmclZIaEhJd1R6eDhaZThFbVBlN2p6QWpJdllHL2F4Z3VseUZ6NU1BRTU5Y1crM2lLRVY0UzdSZkpwYkt2SGFLZUQrYVFvaFE0VjZmZlIrNk5BPT0tLUpLTHQvRHQ0T3FycWV0ZFZhVHZWZkE9PQ%3D%3D--4ef4508c936004db748da10be58731049fa190ee\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
Referer: https://localhost:5678/posts/new\r\n
Content-Length: 369\r\n
\r\n
utf8=%E2%9C%93&authenticity_token=3fu7e8v70K0h9o%2FGNiXxaXSVg3nZ%2FuoL60nlhssUEHpQRz%2BM4ZIHjQduQMexvXrNoC2pjmhNPI4xNNA0Qkh5Lg%3D%3D&post%5Btitle%5D=My+first+post&post%5Bcreated_at%281i%29%5D=2017&post%5Bcreated_at%282i%29%5D=1&post%5Bcreated_at%283i%29%5D=23&post%5Bcreated_at%284i%29%5D=18&post%5Bcreated_at%285i%29%5D=47&post%5Bbody%5D=It+works%21&commit=Create+Post

เหมือนกับการตอบกลับ คำขอ HTTP ประกอบด้วย:

  • บรรทัดคำขอ (POST /posts HTTP/1.1\r\n ) ประกอบด้วยโทเค็นวิธีการ (POST ) URI คำขอ (/posts/ ) และเวอร์ชัน HTTP (HTTP/1.1 ) ตามด้วย CRLF (การขึ้นบรรทัดใหม่:\r ตามด้วยการป้อนบรรทัด:\n) เพื่อระบุจุดสิ้นสุดของบรรทัด
  • บรรทัดส่วนหัว (Host: localhost:5678\r\n ). คีย์ส่วนหัว ตามด้วยโคลอน ตามด้วยค่า และ CRLF
  • ขึ้นบรรทัดใหม่ (หรือ CRLF คู่) เพื่อแยกบรรทัดคำขอและส่วนหัวออกจากเนื้อหา:(\r\n\r\n )
  • เนื้อหา POST ที่เข้ารหัส URL

หลังจากใช้ session.gets เพื่อรับบรรทัดแรกของคำขอ (บรรทัดคำขอ) เราเหลือบรรทัดส่วนหัวและเนื้อหาบางส่วน ในการรับบรรทัดส่วนหัว เราต้องดึงบรรทัดจากเซสชันจนกระทั่งพบบรรทัดใหม่ (\r\n )

สำหรับแต่ละบรรทัดส่วนหัว เราจะแยกเครื่องหมายทวิภาคแรก ทุกอย่างที่อยู่ข้างหน้าเครื่องหมายทวิภาคคือกุญแจ และทุกอย่างที่อยู่ข้างหลังคือค่า เรา #strip ค่าที่จะลบขึ้นบรรทัดใหม่จากจุดสิ้นสุด

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

# http_server.rb
# ...
  headers = {}
  while (line = session.gets) != "\r\n"
    key, value = line.split(':', 2)
    headers[key] = value.strip
  end
 
  body = session.read(headers["Content-Length"].to_i)
# ...

ตอนนี้ แทนที่จะส่งวัตถุเปล่า เราจะส่ง StringIO ตัวอย่างกับร่างกายที่เราได้รับผ่านคำขอ นอกจากนี้ เนื่องจากขณะนี้เรากำลังแยกวิเคราะห์คุกกี้จากส่วนหัวของคำขอ เราจึงสามารถเพิ่มคุกกี้เหล่านี้ไปยังสภาพแวดล้อม Rack ใน HTTP_COOKIE ตัวแปรที่จะผ่านการตรวจสอบความถูกต้องของคำขอ

# http_server.rb
# ...
  status, headers, body = app.call({
    # ...
    'REMOTE_ADDR' => '127.0.0.1',
    'HTTP_COOKIE' => headers['Cookie'],
    'rack.version' => [1,3],
    'rack.input' => StringIO.new(body),
    'rack.errors' => $stderr,
    # ...
  })
# ...

เราจะไปที่นั่น. หากเรารีสตาร์ทเซิร์ฟเวอร์และพยายามส่งแบบฟอร์มอีกครั้ง คุณจะเห็นว่าเราสร้างโพสต์แรกบนบล็อกของเราสำเร็จแล้ว!

เราอัปเกรดเว็บเซิร์ฟเวอร์ของเราอย่างจริงจังในครั้งนี้ แทนที่จะรับแค่คำขอ GET จากแอป Rack ตอนนี้เราให้บริการแอป Rails แบบสมบูรณ์ที่จัดการคำขอ POST และเรายังไม่ได้เขียนโค้ดเกินห้าสิบบรรทัด!

หากคุณต้องการลองใช้เซิร์ฟเวอร์ใหม่ที่ได้รับการปรับปรุงของเรา นี่คือรหัส แจ้งให้เราทราบที่ @AppSignal หากคุณต้องการทราบข้อมูลเพิ่มเติมหรือมีคำถามเฉพาะ