be-kan's blog

エンジニアになるため日々コツコツ学んでおります

RubyでHTTPサーバを立てる②

昨日の投稿(https://be-kan.hatenablog.jp/entry/2018/05/24/012445)の続きになります。

元記事はこちら
blog.appsignal.com


内容的には
・HTTPサーバの上でRails Appを動かす
のみです。

当内容、GitHubソースコードをあげています。
https://github.com/be-kan/http_server_in_ruby


HTTPサーバの上でRaila Appを動かす

使用するRails Appはこちらになります。
https://github.com/jeffkreeftmeijer/wups.git

ただデータをpostしその一覧を取得できるだけのものです。
もちろん自分で用意していただいても構いません。

http_server.rbがあるディレクトリで、Rails Appをgit submoduleで管理します。

$ ls
http_server.rb

$ git submodule add https://github.com/jeffkreeftmeijer/wups.git sample_blog

前回、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

  method, full_path = request.split(' ')
  path, query = full_path.split('?')

  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::Lobster Appを動かしていましたが、これをRails Appに変更します。

require 'socket'
require_relative 'sample_blog/config/environment'

app = Rails.application
server = TCPServer.new 5678

これでサーバを立ててアクセスしてみると、以下のようなエラーがきます。

500 Internal Server Error
If you are the administrator of this website, then please read this web application's log file and/or the web server's log file to find out what went wrong.

言われた通りにlogを確認すると、

Error during failsafe response: Missing rack.input

とのことです。
Rack Appを動かすには、REQUEST_METHOD, PATH_INFO, QUERY_STRINGだけで十分だったのですが、Rails Appを動かすにはそれだけでは不十分のようです。

Rackには、Rack::Lintという、RackがRailsに渡すべき変数を用意してくれるものがあるので、
Rails.applicationをRack::Lintでラッピングします。

require 'socket'
require_relative 'sample_blog/config/environment'

app = Rack::Lint.new(Rails.application)
server = TCPServer.new 5678

これでもう一度サーバを立ててlocalhost:5678にアクセスすると、サーバがクラッシュしました。

~path/to/rack/lint.rb:20:in `assert': env missing required key SERVER_NAME (Rack::Lint::LintError)

Rack::LintはRailsに十分に変数を渡しますが、そもそもRack::Lintに必要な変数を渡せていないようです。
エラーで表示された変数を追加して行くと、最終的に以下のようになります。

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

すると、以下のようなメッセージがブラウザに表示されます。
f:id:kambe3141:20180524204811p:plain
エラーとはいえ、ついにRails側のエラーが来ましたね!

ログを確認すると、

IPAddr::InvalidAddressError: invalid address

というエラーです。
クライアントのIPアドレスを指定しなければなりません。

今はlocalhostで動かしているので、以下のように設定します。

status, headers, body = app.call({
      # ...
      'SERVER_PORT' => '5678',
      'REMOTE_ADDR' => '127.0.0.1',
      'rack.version' => [1,3],
      # ...
  })

f:id:kambe3141:20180524205220p:plain

ついにRailsが表示されました。

ここから、Rails Appに対して、POSTを行います。
Rails App的にはすでにpostに対応しているので、あとはサーバとのつなぎこみのみです。

ちなみに、元記事にはありませんが、Rails Appのデータベースがないので、

rake db:migrate

でデータベースを作成しておきましょう。

サーバを立て、http://localhost:5678/postsにアクセスし、New Postから新規にpostを作成しようとすると

ActionController::InvalidAuthenticityToken

と言われます。
通常ならauthenticity tokenはリクエストと共に送信されるのですが、今のコードでは何もポストデータを送っていないようです。

そこで、session.getsで取得したリクエストの情報を、headerにハッシュとして受け取ります。
リクエストの大きさ(何バイトか)をあらかじめ知るためにbodyにContent-Lengthの情報を入れ、さらにrack.inputには先のbodyで初期化したString.IOインスタンスを入れます。
headerのクッキーも指定し、authenticityをクリアします。

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

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

これで無事POSTもできるようになりました!

RubyでHTTPサーバを立てる①

主にこちらの内容の翻訳になります

blog.appsignal.com

また、この投稿の続きを書きました。
RubyでHTTPサーバを立てる② - be-kan's blog




内容的には、以下の4つです。

TCPサーバを立てる

TCPクライアントを作成する

・HTTPサーバを立てる

・HTTPサーバの中でRack Appを動かす

当内容、GitHubソースコードをあげています。
https://github.com/be-kan/http_server_in_ruby/blob/master/README.md


 

TCPサーバを立てる

require 'socket'
server = TCPServer.new(5678)

while session = server.accept
  session.puts "hello time is #{Time.now}"
  session.close
end

上記のコードでは、ポート5678番を開けて、クライアントから接続されるのを待っています。
クライアントから接続があると、クライアントにメッセージを送り、接続を切って次の接続を待つ、というようなコードになっています。


TCPクライアントを作成する

equire 'socket'
server = TCPSocket.new 'localhost', 5678

while line = server.gets
  puts line
end

server.close

上記のコードでは、ポート5678番に接続するクライアントを作成しています。
サーバに接続すると、先ほどのコードによりクライアントに「hello~」というメッセージが送信されており(つまりクライアントはサーバからそのメッセージを受け取り)、それを表示し、サーバとの接続を切ります。


試しにやってみましょう。

$ ruby tcp_server.rb

でサーバを立てた後、

$ ruby tcp_client.rb

とすると、メッセージが返って来るはずです!

HTTPサーバを立てる。

require 'socket'
server = TCPServer.new 5678

while session = server.accept
  request = session.gets
  puts request

  session.print "HTTP/1.1 200\r\n"
  session.print "Content-Type: text/html\r\n"
  session.print "\r\n" 
  session.print "Hello world! The time is #{Time.now}"

  session.close
end

HTTPサーバと言っても、TCPサーバとほとんど変わりません。
異なるのは、HTTPサーバではHTTPプロトコルでメッセージをやり取りする、というところです。
上記のコードでは、そのプロトコル(通信の約束事・フォーマットのようなもの)を手動で書いています。


HTTPサーバの中でRack Appを動かす

require 'socket'
require 'rack'
require 'rack/lobster'

app = Rack::Lobster.new
server = TCPServer.new 5678

while session = server.accept
  request = session.gets
  puts request

  method, full_path = request.split(' ')
  path, query = full_path.split('?')

  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

このサーバをブラウザで開くと、以下のような画面になるはずです。

f:id:kambe3141:20180524012413p:plain

「flip!」をクリックすると、TCPサーバにリクエストが送られます。TCPサーバは、送られてきたリクエストをmethod, path, queryに分解します。
そして分解したmethodなどを、Rack Appに送り、Rack Appからstatus, headers, bodyを受け取ります。
実際に送られたパラメータの値は以下です。

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

そして、TCPサーバは、Rackから受け取った情報を、クライアント側に送っています。



次回は、HTTPサーバの中でRailsを動かしてみたいと思います!

Hello Hatena Blog

ブログ始めました。

 

現在東京大学工学部3年です。

 

1年前にエンジニアに興味を持ち、半年ちょっと前から本格的に勉強し始めました。

 

このブログでは、日々の勉強をそれなりの粒度でアウトプットしていきたいと思います。

(細かなTipsはQiitaに、ってスミワケしようかな)

 

今は(主にインターンで)Railsでバックエンド開発をやっていますが、インフラや低レイヤーまで幅広くやっていきたいです。

 

今のところ大学院に進学するつもりで、院では機械学習をやりたくて今統計やデータサイエンスも勉強しています。

 

勉強すればするほど「世の中にはすごいエンジニアがいるなー。自分クソ雑魚やん」と感じるような、1歩進むごとに3歩先が見えて絶望するような世界ですが、焦らずコツコツやっていきます。