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

Ruby에서 30라인 HTTP 서버 구축

웹 서버와 일반적으로 HTTP는 이해하기 어려울 수 있습니다. 브라우저는 요청을 어떻게 형식화하고 응답은 어떻게 사용자에게 전송됩니까? 이 Ruby Magic 에피소드에서는 30줄의 코드로 Ruby HTTP 서버를 구축하는 방법을 배웁니다. 완료되면 서버에서 HTTP GET 요청을 처리하고 이를 사용하여 Rack 앱을 제공합니다.

HTTP와 TCP가 함께 작동하는 방식

TCP는 서버와 클라이언트가 데이터를 교환하는 방법을 설명하는 전송 프로토콜입니다.

HTTP는 웹 서버가 HTTP 클라이언트 또는 웹 브라우저와 데이터를 교환하는 방법을 구체적으로 설명하는 요청-응답 프로토콜입니다. HTTP는 일반적으로 TCP를 전송 프로토콜로 사용합니다. 본질적으로 HTTP 서버는 HTTP를 "말하는" TCP 서버입니다.

# 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

요청 라인은 네 부분으로 구성됩니다.

  • 메서드 토큰(GET , 이 예에서는)
  • 요청 URI(/ )
  • 프로토콜 버전(HTTP/1.1 )
  • 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 )
  • 본문:"Hello World!"

최소한의 Ruby HTTP 서버

충분한 이야기입니다. 이제 Ruby에서 TCP 서버를 만드는 방법과 일부 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. 응답에 text/html 콘텐츠 유형이 있음을 나타내는 헤더(Content-Type: text/html\r\n )
  3. 줄 바꿈(\r\n )
  4. 본문:"안녕하세요! ..."

이전과 마찬가지로 메시지를 보낸 후 연결을 닫습니다. 아직 요청을 읽지 않고 있으므로 지금은 콘솔에 출력합니다.

서버를 시작하고 브라우저에서 https://localhost:5678을 열면 이전에 TCP 클라이언트에서 수신한 것처럼 현재 시간과 함께 "Hello world! ..." 행이 표시되어야 합니다. 🎉

랙 앱 제공

지금까지 우리 서버는 각 요청에 대해 단일 응답을 반환했습니다. 좀 더 유용하게 만들기 위해 서버에 더 많은 응답을 추가할 수 있습니다. 이들을 서버에 직접 추가하는 대신 Rack 앱을 사용합니다. 우리 서버는 HTTP 요청을 구문 분석하여 Rack 앱으로 전달합니다. 그러면 서버가 클라이언트로 다시 보낼 응답을 반환합니다.

Rack은 Ruby를 지원하는 웹 서버와 Rails 및 Sinatra와 같은 대부분의 Ruby 웹 프레임워크 간의 인터페이스입니다. 가장 단순한 형태의 Rack 앱은 call에 응답하는 객체입니다. HTTP 응답 코드, HTTP 헤더 해시 및 본문의 세 가지 항목이 있는 배열인 "tiplet"을 반환합니다.

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

이 예에서 응답 코드는 "200"이고 헤더를 통해 콘텐츠 유형으로 "text/html"을 전달하고 본문은 문자열이 있는 배열입니다.

서버가 이 앱의 응답을 제공할 수 있도록 하려면 반환된 트리플렛을 HTTP 응답 문자열로 변환해야 합니다. 이전과 같이 항상 정적 응답을 반환하는 대신 이제 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. app.call이 반환한 트리플렛에서 상태 코드, 헤더 및 본문을 가져옵니다. .
  2. 상태 코드를 사용하여 상태 표시줄 작성
  3. 헤더를 반복하고 해시의 각 키-값 쌍에 대한 헤더 행 추가
  4. 본문에서 상태 줄과 헤더를 구분하기 위해 줄 바꿈을 인쇄합니다.
  5. 본체에 루프를 만들고 각 부분을 인쇄합니다. body 배열에는 한 부분만 있으므로 세션을 닫기 전에 "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을 방문하여 "flip!" 링크를 클릭하면 이제 랍스터가 뒤집히고 "crash!"를 클릭합니다. 링크를 클릭하면 웹 서버가 다운됩니다.

우리는 HTTP 서버 구현의 표면을 긁어모았고 우리는 30줄의 코드에 불과하지만 기본 아이디어를 설명합니다. GET 요청을 수락하고 요청 속성을 Rack 앱에 전달하고 브라우저에 응답을 다시 보냅니다. 요청 스트리밍 및 POST 요청과 같은 것을 처리하지는 않지만 이론적으로 우리 서버는 다른 Rack 앱을 제공하는 데에도 사용될 수 있습니다.

이것으로 Ruby에서 HTTP 서버를 구축하는 방법을 간단히 살펴보겠습니다. 우리 서버를 가지고 놀고 싶다면 여기 코드가 있습니다. 더 알고 싶거나 특정 질문이 있는 경우 @AppSignal로 알려주십시오.

이 기사가 도움이 되었다면 Ruby Magic 뉴스레터를 구독하세요. Ruby의 (대략) 월간 분량