Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Running Rack:Ruby HTTP 서버가 Rails 앱을 실행하는 방법

Ruby Magic 시리즈에서 우리는 소프트웨어가 내부에서 어떻게 작동하는지 알아보기 위해 소프트웨어를 분해하는 것을 좋아합니다. 모든 것은 프로세스에 관한 것입니다. 최종 결과는 프로덕션에서 사용하는 것이 아니라 Ruby 언어 및 인기 라이브러리의 내부 작동에 대해 배웁니다. 우리는 한 달에 한 번 정도 새 기사를 발행하므로 이런 종류의 일에 관심이 있다면 뉴스레터를 구독하십시오.

Ruby Magic의 이전 버전에서 우리는 Ruby에서 30라인 HTTP 서버를 구현했습니다. 많은 코드를 작성할 필요 없이 HTTP GET 요청을 처리하고 간단한 Rack 애플리케이션을 제공할 수 있었습니다. 이번에는 집에서 만든 서버를 조금 더 발전시켜 보겠습니다. 작업이 완료되면 게시물을 작성, 업데이트 및 삭제할 수 있는 Rails의 유명한 15분 블로그를 제공할 수 있는 웹 서버가 생깁니다.

중단한 부분

지난번에 우리는 Rack::Lobster를 예제 애플리케이션으로 제공할 만큼의 서버를 구현했습니다.

  1. 우리의 구현은 TCP 서버를 열고 요청이 들어올 때까지 기다렸습니다.
  2. 그런 일이 발생하면 요청 라인(GET /?flip=left HTTP/1.1\r\n ) 요청 메서드(GET)를 얻기 위해 구문 분석되었습니다. ), 경로(/ ) 및 쿼리 매개변수(flip=left ).
  3. 요청 방법, 경로 및 쿼리 문자열이 Rack 앱으로 전달되었으며 상태, 일부 응답 헤더 및 응답 본문이 포함된 트리플렛을 반환했습니다.
  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

지난번에 작성한 코드를 계속 사용하겠습니다. 따라하고 싶다면 다음과 같은 코드를 완성하세요.

랙 및 레일

Rails 및 Sinatra와 같은 Ruby 프레임워크는 Rack 인터페이스를 기반으로 합니다. Rack::Lobster의 인스턴스처럼 우리는 지금 서버를 테스트하는 데 사용하고 있습니다. Rails의 Rails.application 랙 응용 프로그램 개체입니다. 이론적으로 이것은 우리 서버가 이미 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

그런 다음 서버에서 rack 대신 Rails 애플리케이션의 환경 파일이 필요합니다. 및 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>'

랙 환경

우리가 서버를 구현할 때 랙 환경은 간과하고 랙 애플리케이션을 적절하게 제공하는 데 필요한 대부분의 변수를 무시했습니다. REQUEST_METHOD만 구현했습니다. , PATH_INFOQUERY_STRING 간단한 Rack 앱에 충분했기 때문에 변수입니다.

새 애플리케이션을 시작하려고 할 때 예외에서 이미 보았듯이 Rails에는 rack.input이 필요합니다. , 원시 HTTP POST 데이터의 입력 스트림으로 사용됩니다. 그 외에도 서버의 포트 번호 및 요청 쿠키 데이터와 같이 전달해야 하는 변수가 더 있습니다.

다행히 Rack은 Rack::Lint를 제공합니다. Rack 환경의 모든 변수가 존재하고 유효한지 확인하는 데 도움이 됩니다. Rack::Lint.new를 호출하여 Rails 앱을 래핑하여 서버를 테스트하는 데 사용할 수 있습니다. 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을 지원하지 않으므로 "https" 대신 "http"로 설정할 수 있습니다.

누락된 모든 변수를 추가한 후 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을 다시 방문하면 Rails의 "You're on 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 )
  • URL 인코딩된 POST 본문

session.gets를 사용한 후 요청의 첫 번째 줄(Request-Line)을 취하기 위해 일부 헤더 줄과 본문이 남습니다. 헤더 행을 얻으려면 개행(\r\n)을 찾을 때까지 세션에서 행을 검색해야 합니다. ).

각 헤더 행에 대해 첫 번째 콜론에서 분할합니다. 콜론 앞의 모든 것이 키이고 뒤에 오는 모든 것이 값입니다. 우리는 #strip 끝에서 줄 바꿈을 제거하는 값입니다.

본문을 가져오기 위해 요청에서 몇 바이트를 읽어야 하는지 알기 위해 브라우저가 요청을 보낼 때 자동으로 포함하는 "Content-Length" 헤더를 사용합니다.

# 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 요청을 통해 받은 본문이 있는 인스턴스입니다. 또한 이제 요청 헤더에서 쿠키를 구문 분석하고 있으므로 HTTP_COOKIE의 Rack 환경에 추가할 수 있습니다. 요청 진위 확인을 통과하기 위한 변수입니다.

# 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,
    # ...
  })
# ...

우리는 거기에 갈. 서버를 다시 시작하고 양식을 다시 제출하려고 하면 블로그에 첫 번째 게시물이 성공적으로 작성되었음을 알 수 있습니다!

이번에는 웹 서버를 심각하게 업그레이드했습니다. Rack 앱에서 GET 요청을 수락하는 대신 이제 POST 요청을 처리하는 완전한 Rails 앱을 제공하고 있습니다. 그리고 아직 총 50줄 이상의 코드를 작성하지 않았습니다!

새롭고 향상된 서버를 가지고 놀고 싶다면 여기 코드가 있습니다. 더 알고 싶거나 특정 질문이 있는 경우 @AppSignal로 알려주십시오.