4
4

続・Crystal の小さな本

Posted at

この本は「Crystal の小さな本」 の続編で、主に HTTP やネットワーク関連について記載しています。

Crystal: HTTP モジュール

HTTP モジュールは HTTP::Client, HTTP::Server, HTTP::WebSocket を含むモジュールであり、require “http” をソースに追加することにより、これらのクラスも同時に利用できるようになる。

HTTP モジュールにはそれ以外に若干のメソッドが含まれており、引用符のエスケープと逆エスケープに関するもの、時刻の文字列化とその逆変換および内部処理で使用されると思われるメソッドである。

HTTP モジュールに含まれるエスケープ処理に関するメソッドの使用例を示す。

# http module https://crystal-lang.org/api/1.9.2/HTTP.html
require "http"
 
qs = %q(\"foo\\bar\")
puts qs
ds = HTTP.dequote_string(qs)  # エスケープされた引用符のエスケープを取り去る。
puts ds
qs = HTTP.quote_string(ds)  # 引用符をエスケープする。
puts qs
 
puts HTTP.format_time(Time.utc(2016, 2, 15)) # 時刻をフォーマットしての文字列化
puts HTTP.parse_time("Sun, 14 Feb 2016 21:00:00 GMT").to_s # 時刻文字列の解析による時刻化

この実行例を下に示す。

$ ../bin/http_module
\"foo\\bar\"
"foo\bar"
\"foo\\bar\"
Mon, 15 Feb 2016 00:00:00 GMT
2016-02-14 21:00:00 UTC
$

Crystal: HTTP/Client

HTTP/Client は HTTP モジュールに含まれるクラスである。そのため、使用する場合は、require “http/client” が必要になる。

このクラスに含まれる Response クラスはクライアントがサーバにリクエストを行ったときに返される応答を処理するサブクラスである。また、HTTP/Server にも Response クラスがあるがそれとは別物である。

HTTP/Client の主な使い方は Crystal のドキュメントに出ているが、その内容をここにも載せておく。
One-Shot Usage

この例はサーバに GET メソッドでリクエストを行い返されたステータスコードと HTML の1行目を表示している。

require "http/client"
 
response = HTTP::Client.get "http://www.example.com"
response.status_code      # => 200
response.body.lines.first # => "<!doctype html>"

Parameters

この例では、ハッシュリテラルを HTTP リクエストのパラメータ形式に変換し、GET メソッドでサーバへリクエストを送り、その応答のステータスコードを表示している。

require "http/client"
 
params = URI::Params.encode({"author" => "John Doe", "offset" => "20"}) # => "author=John+Doe&offset=20"
response = HTTP::Client.get URI.new("http", "www.example.com", query: params)
response.status_code # => 200

Streaming

この例では、ブロック付きの GET メソッドでサーバにリクエストを送り、返された応答をストリーム (response.body_io) として処理している。

require "http/client"
 
HTTP::Client.get("http://www.example.com") do |response|
  response.status_code  # => 200
  response.body_io.gets # => "<!doctype html>"
end

Reusing connection

この例では、Client オブジェクトを作成し、それを使ってリクエストをサーバへ送り応答を取得している。Client オブジェクト (client) は close メソッドを実行しない限り生きているので、そのまま、別のリクエストを行うことができる。(ドキュメントに出ているように複数ファイバでの使いまわしは不可)

require "http/client"
 
client = HTTP::Client.new "www.example.com"
response = client.get "/"
response.status_code      # => 200
response.body.lines.first # => "<!doctype html>"
client.close

次に実際に試してみたテストプログラムを示す。このプログラムではコマンドライン引数として番号を与えることでいくつかの動作確認を行っている。(下記)

  • 1行だけの plain/text の応答を取得して表示
  • 複数行を返す応答を取得して、すべての行を表示
  • クッキーを取得
  • GET メソッドでパラメータを送り応答(エコーが返る)を表示
  • POST メソッドでパラメータを送り応答(エコーを含むHTML)を表示
  • 画像ファイルを取得
  • ストリームから応答を読み取る
# http/client https://crystal-lang.org/api/1.9.2/HTTP/Client.html
require "http/client"
require "mime"
 
def main()
  if ARGV.size() == 0
    STDERR.puts "No arguments that is a number."
    exit 1
  end
 
  begin
    n = ARGV[0].to_i32
  rescue
    STDERR.puts "Must be integer"
    exit 1
  end
 
  url = "http://localhost/cgi-bin/CGI365Lib/"
  case n
  when 1  # 1 行のみ取得
    url += "hello_world.cgi"
    puts "GET #{url}"
    response = HTTP::Client.get url
    puts response.status_code.to_s
    puts response.body.lines.first
  when 2  # すべての行と応答情報を取得
    url += "environ.cgi"
    puts "GET #{url}"
    response = HTTP::Client.get url
    p! response.status_code
    p! response.content_type
    p! response.charset
    p! response.cookies.size
    p! response.headers.size
    response.body.lines.each do |s|
      puts s
    end
  when 3  # クッキーを取得
    url += "cookie.cgi"
    puts "GET #{url}"
    response = HTTP::Client.get url
    p! response.status_code
    p! response.cookies.size
    response.cookies.each do |c|
      puts c.name + ": " + c.value
    end
  when 4  # GET でパラメータを送る
    url += "/Class/echoRaw.cgi?data=123456"
    response = HTTP::Client.get url
    if response.success?
      puts response.body.lines.first
    else
      STDERR.puts "GET failed"   
    end
  when 5  # POST でパラメータを送る
    url += "post_form.cgi"
    puts "POST #{url}"
    response = HTTP::Client.post url, nil, "message=Yes+you+can!"
    if response.success?
      response.body.lines.each do |s|
        puts s
      end
    else
      STDERR.puts "POST failed"
    end
  when 6  # 画像ファイルの取得
    filename = "home_blue.png"
    url = "http://localhost/img/" + filename
    puts "GET #{url}"
    response = HTTP::Client.get url
    if response.success?
      puts response.mime_type
      puts response.status_code.to_s
      fd = File.open("./" + filename, "w")
      fd.write(response.body.to_slice)
      fd.close
    else
      STDERR.puts "GET image file failed."
    end
  when 7  # ブロック付きの get
    url = "http://localhost/index.html"
    HTTP::Client.get(url) do |response|
      if response.success?
        puts response.body_io.gets_to_end
      else
        STDERR.puts "Failed to get the stream."
      end
    end
 
  else
    STDERR.puts "Not supported."
  end
end
 
main()

Crystal: HTTP/Client/Response

HTTP/Client/Response クラスは HTTP クライアントがサーバへリクエストを行ったとき、サーバから返される情報のオブジェクトである。

Response オブジェクトは HTTP::Client.get や HTTP::Client.post でサーバにリクエストを送ったときに、メソッドの関数値として返される。

(例)response = HTTP::Client.get "http://www.example.com"

ブロック付きの get や post メソッドの場合は、ブロックのパラメータとして Response オブジェクトが返される。

(例)HTTP::Client.get("http://www.example.com") {|response| .... }

さらにコンストラクタを使ってインスタンス化して使うことっもできる。

(例)client = HTTP::Client.new "www.example.com"

次に Response に含まれる重要なメソッドやプロパティについて述べる。実際の使用例は HTTP/Client のサンプル参照。

body : String, body? : String | Nil

サーバからの応答文字列が格納される。String 型なので文字列のメソッドが使用できる。

特に行ごとに処理する場合は lines(chomp=true) : Array(String) が便利である。

? 付きのバージョンは応答が空の場合をチェックするのに使用できる。

body_io : IO, body_io? : IO | Nil

サーバからの応答をストリームとして処理する場合に使用する。 ? 付きのバージョンは応答が空の場合をチェックするのに使用できる。

これは、Client.get メソッドなどのブロック付きバージョンを使う際にブロックのパラメータとして返された Ressponse オブジェクトの場合、有効になる。(普通の get などでは Nil)

success? : Bool

リクエストが成功したかどうかを判別するのに使用する。

status_code : Int32

サーバから返されるステータスコード。成功なら 200 になる。

content-type : String | Nil

サーバから返された Content-Type の内容。(例) image/png

cookies : HTTP::Cookies

サーバからクッキーのコレクションが入っている。

charset : String | Nil

サーバから送られてきた HTML などのエンコーディング文字列が入っている。

headers : Headers

サーバから送られてきた応答のヘッダ部が入っている。

Crystal: HTTP/Server

HTTP/Server クラスは HTTP モジュールに含まれる主要なクラスである。

さらに HTTP::Server は次のサブクラスを含む。

  • Context
  • Response
  • ClientError
  • RequestProcessor

このうち、特に重要なのが Response と Context である。

この Response は Server::Response であり、クライアントの Response とは異なる。

Context には request と response プロパティが含まれており、実際の処理ではこの2つのオブジェクトの操作が重要になる。

request は HTTP::Request のインスタンスであり、response は HTTP::Server::Response のインスタンスである。

HTTP/Server には4つのコンストラクタがあって目的により使い分けることができる。

  • .new(handlers : Array(HTTP::Handler), &handler : HTTP::Handler::HandlerProc) : self
  • .new(&handler : HTTP::Handler::HandlerProc) : self
  • .new(handlers : Array(HTTP::Handler)) : self
  • .new(handler : HTTP::Handler | HTTP::Handler::HandlerProc)

コンストラクタ 1 は既存の複数のハンドラとハンドラブロックを持つタイプであり、静的ファイルの処理やエラー処理などの既存のハンドラと独自のハンドラを組み合わせることができる。

コンストラクタ 2 は独自ハンドラですべての処理を行うタイプである。

コンストラクタ 3 は複数の既存のハンドラを組み合わせるタイプである。

コンストラクタ 4 は1つだけの既存ハンドラを使うタイプである。

HTTP モジュールでは次のようなハンドラを含んでいる。

  • CompressHandler
  • ErrorHandler
  • LogHandler
  • StaticFileHandler
  • WebSocketHandler

ハンドラは HTTP/Handler モジュールをインクルードして独自に作ることもできる。

HTTP サーバオブジェクトを構築したら、それを TCP サーバなどにバインドする。HTTP サーバは HTTP レイヤだけを担当し、下位のレイヤは TCP サーバ等が受け持つ。

この下位のレイヤを受け持つのは通常は TCP サーバであるが、UNIX サーバや独自に作ったサーバでもできるようになっている。

TCP サーバにバインドするメソッドは bind_tcp で次の3つのオーバーロードがある。ローカルコンピュータのみでサーバを運用するのであれば2番目のバージョンを使用する。

  • bind_tcp(host : String, port : Int32, reuse_port : Bool = false) : Socket::IPAddress
  • bind_tcp(port : Int32, reuse_port : Bool = false) : Socket::IPAddress
  • bind_tcp(address : Socket::IPAddress, reuse_port : Bool = false) : Socket::IPAddress

なお、SSL を使うバージョンでは bind_tls となる。

バインドによりクライアントからのリクエストを受け付ける準備ができたので、最後に server.listen を実行してリクエスト待ちにする。

以下では4つの HTTP::Server コンストラクタの例を示す。

コンストラクタ 1

# http_server1 https://crystal-lang.org/api/1.9.2/HTTP/Server.html
require "http/server"
 
# コンストラクタ 1
server = HTTP::Server.new([
  HTTP::ErrorHandler.new,
  HTTP::LogHandler.new,
  HTTP::CompressHandler.new,
  HTTP::StaticFileHandler.new("./html"),
]) do |context|
  if context.request.path == "/hello"
    context.response.content_type = "text/plain"
    context.response.print "Hello World!\n"
  end
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

コンストラクタ 2

# http_server2 https://crystal-lang.org/api/1.9.2/HTTP/Server.html
require "http/server"
 
# コンストラクタ 2
server = HTTP::Server.new do |context|
  if context.request.path == "/hello"
    context.response.content_type = "text/plain"
    context.response.print "Hello World!\n"
  else
    context.response.status = HTTP::Status::NOT_FOUND
    context.response.content_type = "text/plain"
    context.response.print "Not Found\n"
  end
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

コンストラクタ 3

# http_server3 https://crystal-lang.org/api/1.9.2/HTTP/Server.html
require "http/server"
 
# コンストラクタ 3
server = HTTP::Server.new [HTTP::StaticFileHandler.new("./html")]
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

コンストラクタ 4

# http_server4 https://crystal-lang.org/api/1.9.2/HTTP/Server.html
require "http"
 
# コンストラクタ 4
server = HTTP::Server.new HTTP::StaticFileHandler.new("./html")
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

Crystal: HTTP/Server/Context

HTTP/Server/Context はサーバ側でリクエストの処理を行うハンドラへ渡されるパラメータの型である。

このパラメータには重要なオブジェクトである request と response を含む。

request は HTTP/Request 型であり、クライアントからのリクエストの情報を含んである。

response は HTTP/Server/Response 型であり、クライアントへ返す応答のための手段を提供する。

Context の使用例を下に示す。

require "http/server"
 
server = HTTP::Server.new do |context|
  if context.request.path == "/hello"
    context.response.content_type = "text/plain"
    context.response.print "Hello world!"
  else
    context.response.status_code = 404
    context.response.content_type = "text/plain"
    context.response.print "Not supported!"
  end
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

Crystal: HTTP/Server/Response

HTTP/Server/Response クラスは HTTP サーバがクライアントに対して応答を返す時に使うオブジェクトであり、HTTP/Client/Response とは異なる。

そして、このクラスは IO クラスを継承しており、IO のメソッドを出力用に使用可能である。

クライアントからのリクエストに対して処理を行うときにハンドラがコールされるがそのパラメータである Context にはこの Response がプロパティとして含まれている。

したがって、ハンドラはその Response (context.response) を使用してクライアントへの応答を作成し出力する。

次に Response クラスの重要なメソッドやプロパティを挙げる。

  • content_type : String
  • cookies : HTTP::Cookies
  • headers : HTTP::Headers
  • output : IO
  • redirect(location, status)
  • respond_with_status(status, message)
  • status : HTTP::Status
  • status_code : Int32
  • write(slice)

さらに、IO クラスの次のようなメソッドが使用できる。

  • print(obj)
  • printf(format_string, *args)
  • puts(str)
  • write_byte(byte)
  • write_string(slice)

Response の使用例を下に示す。

require "http/server"
 
server = HTTP::Server.new do |context|
  context.response.content_type = "text/plain"
  context.response.print "Hello world!"
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

Crystal: HTTP/Request

HTTP/Request は クライアントからのリクエストをカプセル化したオブジェクトである。

サーバ側のリクエストハンドラはパラメータとして HTTP::Server::Context を受け取るが、その中のプロパティとして Request オブジェクト (context.request) が含まれる。

リクエストの生の内容は body に含められている。body は次のようないくつかのオーバーロードがある。

  • body : IO | Nil
  • body=(body : String)
  • body=(body : Bytes)
  • body=(body : IO)
  • body=(body : Nil)

加えて重要なメソッドやプロパティとして次のようなものがある。

  • cookies : HTTP::Cookies
  • form_params : HTTP::Params
  • headers : HTTP::Headers
  • hostname : String
  • method : String
  • path : String
  • query : String | Nil
  • query_params : URI::Params
  • remote_address : Socket::Address | Nil
  • to_io(io)

次に Request の使用例を示す。

# http-server Request https://crystal-lang.org/api/1.9.2/HTTP/Request.html#content_length%3D%28length%3AInt%29-instance-method
require "http/server"
  
server = HTTP::Server.new do |context|
  req = context.request
  res = context.response
  res.content_type = "text/plain"
  if req.path == "/test"
    res.puts req.hostname
    res.puts req.method
  else
    context.response.puts "Not supported!"
  end
end
  
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

この実行例を示す。

$ curl http://localhost:8080/test
localhost
GET
$ curl http://localhost:8080
Not supported!
$

Crystal: HTTP/Params

HTTP/Params はクライアントからのリクエストのパラメータを表す。GET メソッドで言えば URL の ? 以降の部分に当たる。

実は、HTTP / Params は URI / Params の別名である。

実際のパラメータを取得するには Request.query_params にアクセスすることにより簡単にリクエストパラメータを取得できる。

Request.query_params の型は URI::Params であり、HTTP サーバがリクエストがあるとパラメータを解析して、この Request.query_params に値を入れてくれる。

URI::Params は Enumerable({String, String}) を含んでいるので、ハッシュ (連想配列) としても扱うことができる。

要するに、パラメータのキーが key のとき、その値は次のようにして取得できる。

value = query_params[key]

キーが存在しない場合もあるので、その時は fetch(key, default) メソッドを使うほうが便利である。このメソッドを使えば、キーが存在しない時は、デフォルト値を返してくれる。

次に、URI:: Params の使用例を示す。

# HTTP/Params https://crystal-lang.org/api/1.9.2/URI/Params.html
require "http/server"
 
server = HTTP::Server.new do |context|
  req = context.request
  res = context.response
  res.content_type = "text/plain"
  if req.path == "/echo"
    res.puts req.query  # QUERY_STRING の内容
    res.puts req.query_params["message"]  # リクエストパラメータの値を得る。(エコーする)
    if req.query_params["A"]?.nil?  # パラメータがあるか確認 (has_key? のほうが良いが)
      puts "params[A] = Nil"
    else
      puts req.query_params["A"]
    end
  elsif req.path == "/fetch"
    res.puts req.query_params.fetch("A", "Default_A")  # リクエストパラメータがない場合、デフォルト値を返す。
  else
    res.status = HTTP::Status::NOT_FOUND
    res.puts "Not Found\n"
  end
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

(注意) POST メソッドの場合は、req.query_params でなく req.form_params に変更すること。

Crystal: HTTP/Status

HTTP/Status は列挙型で HTTP サーバが返すステータスコードを定義している。

HTTP/Server においては、 Response オブジェクトの status プロパティの値としてこの列挙値のどれかを設定する。ただし、OK (200) はデフォルトなので設定する必要はない。

次に HTTP/Status の使用例を示す。

# server Status https://crystal-lang.org/api/1.9.2/HTTP/Status.html
require "http/server"
 
server = HTTP::Server.new do |context|
  req = context.request
  res = context.response
  res.content_type = "text/plain"
  if req.path == "/echo"
    if req.query_params.has_key?("message")
      res.puts req.query_params["message"]  # リクエストパラメータの値を得る。(そのまま返す)
      # デフォルトではステータス OK (200) が返される。
    else
      # パラメータ message がない場合は未実装。
      res.status = HTTP::Status::NOT_IMPLEMENTED
    end
  else
    # パス /echo 以外は実装していない。
    res.status = HTTP::Status::NOT_FOUND
  end
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

この動作は curl コマンドの -I オプションで確認できる。

$ curl -I http://localhost:8080/M
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Type: text/plain
Content-Length: 0

$ curl -I http://localhost:8080/echo
HTTP/1.1 501 Not Implemented
Connection: keep-alive
Content-Type: text/plain
Content-Length: 0

$ curl -I http://localhost:8080/echo?message=Hello
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: text/plain
Content-Length: 6

Crystal: HTTP/FormData

HTTP/FormData はマルチパートフォームデータ (multipart/form-data) を処理するときに使う。

因みに、マルチパートでないフォームの場合は、Request クラスの form_params でフォーム要素の値を取得できる。

マルチパート・フォームの場合は、境界文字列 (ブラウザのエンジンにより異なる) でフォームデータが別れているので、その境界文字列で別れている塊 (チャンク) ごとに処理を行う。

この処理は HTTP::FormData.parse(request, &block) などのメソッドを使うことで可能である。

このメソッドでは、リクエストボディ (request.body) に含まれるフォームデータをチャンクごとにブロックに渡してくれる。

マルチパートフォームのチャンクには付加的なデータ (メタデータ) が含まれていることがある。これは、parse_content_disposition(content_disposition) : Tuple(String, FileMetadata) により解析・取得できる。

次にマルチパートフォームによりファイルをアップロードする例を示す。

# server FormData https://crystal-lang.org/api/1.9.2/HTTP/FormData.html
# server FormData 
require "http"
 
# Form1 HTML
form1 = <<-EOS
<!doctype html>
<html>
 <head>
  <meta charset="utf-8" />
  <title>Form1</title>
  <style>
   h1 {
     text-align:center;
     padding: 10px;
   }
   form {
     margin-left: 10%;
   }
   .form_row {
     padding: 5px;
   }
  </style>
 </head>
 <body>
  <h1>Form1 (multipart)</h1>
  <form name="form1" method="POST" enctype="multipart/form-data" action="/form1">
    <div class="form_row">name <input type="text" name="name" /></div>
    <div class="form_row">file <input type="file" name="file" /></div>
    <div class="form_row"><button type="submit"> OK </button></div>
    <div class="form_row">result</div>
  </form>
 </body>
</html>
EOS
 
server = HTTP::Server.new do |context|
  req = context.request
  res = context.response
  res.content_type = "text/html; charset=utf-8"
  form = form1
  if req.path == "/form1"  # パス /form1 のときファイルアップロード処理
    if req.method == "GET"  # メソッド GET のときはフォームを表示するだけ
      form = form.sub("result", "")
      res.print form
    elsif req.method == "POST"  # メソッド POST のときはアップロードさあれたファイルを "upload" というファイルに保存
      result = "POST /form1 "
      name = nil
      file = nil
      HTTP::FormData.parse(req) do |part|  # response.body を解析する。
        case part.name  # name チャンクのとき
        when "name"
          name = part.body.gets_to_end
        when "file"  # file チャンクのとき
          file = File.open("./upload", "w") do |f|
            IO.copy(part.body, f)  # アップロードされたファイルを "upload" というファイルにコピーする。
          end
        end
      end
      # name と file のどちらかが無効のときは BAD_REQUEST を返す。
      unless name && file
        res.respond_with_status(:bad_request)
        next
      end
      # フォーム内の文字列 "result" に name を埋め込んでフォームをエコーする。
      result += "name=#{name}"
      form = form.sub("result", result)
      res.print form
    end
  else  # パス /form1 以外は NOT_FOUND を返す。
    res.respond_with_status(HTTP::Status::NOT_FOUND)
  end
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

マルチパートでないフォームは FormData を使用する必要はない。その場合は、Request の form_params メソッドでフォームの値を取得できる。

下の例はマルチパートでないフォーム処理の例である。

# server FormData 
require "http"
require "http/status"
 
# Form1 HTML
form1 = <<-EOS
<!doctype html>
<html>
 <head>
  <meta charset="utf-8" />
  <title>Form1</title>
  <style>
   h1 {
     text-align:center;
     padding: 10px;
   }
   form {
     margin-left: 10%;
   }
  </style>
 </head>
 <body>
  <h1>Form1</h1>
  <form name="form2" method="POST" action="/form1">
    <div>Text1 <input type="text" name="text1" size="60" /></div>
    <div>File1 <input type="checkbox" name="check1" /></div>
    <div><button type="submit"> OK </button></div>
    <p>result</p>
  </form>
 </body>
</html>
EOS

# 普通のシングルパートのフォームの場合
server = HTTP::Server.new do |context|
  req = context.request
  res = context.response
  res.content_type = "text/html; charset=utf-8"
  if req.path == "/form1"  # パス /form1 のときのみフォーム処理
    if req.method == "GET"  # メソッド GET のときはフォームをそのまま返す。
      form = form1.gsub("result", "")
      res.print form
    elsif req.method == "POST"  # メソッド POST のときはフォームの要素を取得
      result = "POST /form1 "
      # name="text1" の値を取得する。
      if req.form_params["text1"]?.nil?
        result += "text1:\"\""
      else
        result += "text1:" + req.form_params["text1"]
      end
      # name="check1" の値を取得する。
      if req.form_params["check1"]?.nil?
        result += " check1:false"
      else
        result += " check1:true"
      end
      # フォーム内の文字列 "result" を変数 result で置換する。
      form = form1.gsub("result", result)
      res.print form
    else # パス /form1 以外はエラー
      res.respond_with_status(HTTP::Status::NOT_FOUND)
    end
  else
    res.status = HTTP::Status::NOT_FOUND
    res.puts "NOT FOUND"
  end
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

Crystal: HTTP/Cookies

HTTP/Cookies はクッキー (HTTP/Cookie) のコレクションであり、HTTP/Request の cookies と HTTP/Server/Response の cookies の型である。

クッキー つまり HTTP::Cookie のキーは name : String で、値は value : String である。

HTTP::Cookie は値以外にも様々なプロパティを持つ。そのうち重要なものを下に挙げる。

  • domain : String
  • creation_time : Time
  • expired? : Bool
  • expires : Time
  • path : String

次にクッキーの簡単な使用例を示す。このサンプルではブラウザ側にクッキーが保存されていない時は Ck1, Ck2 という2つのクッキーをブラウザに送り、クッキーが保存されている時はそれを受け取ってブラウザに表示する。

# server Cookies https://crystal-lang.org/api/1.9.2/HTTP/Cookies.html
require "http/server"
require "http/cookie"
 
server = HTTP::Server.new do |context|
  req = context.request
  res = context.response
  res.content_type = "text/plain"
  if req.cookies.size == 0
    # ブラウザにクッキーが保存されてないとき
    res.cookies["Ck1"] = "CkValue1"
    res.cookies << HTTP::Cookie.new("Ck2", "CkValue2")
    res.puts "Sent Cookies Ck1 and Ck2"
  else
    # ブラウザにクッキーが保存されているとき
    req.cookies.each do |cookie|
      res.puts "#{cookie.name} : #{cookie.value}"
    end
  end
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

Crystal: HTTP/Headers

HTTP/Headers は HTTP::Request の headers プロパティと HTTP::Server::Response の headers プロパティの型である。

クライアントから送られてくるヘッダには次のようなものがある。そして、場合によってはこれ以外のヘッダが含まれることもある。

Host: ["localhost:8080"]
User-Agent: ["curl/7.88.1"]
Accept: ["*/*"]

例えば、Host 情報から禁止されたホストからのリクエストに対し、OK (200) 以外のステータスを返したりできる。

逆に、Headers.add(key, value) メソッドを使いクライアントへ返すヘッダ情報を追加することもできる。

次のサンプルはクライアントから送られてきたヘッダ情報一覧をサーバ側に表示し、クライアントへ返すヘッダ情報に “Allow メソッド” ヘッダを追加している。

# server Headers https://crystal-lang.org/api/1.9.2/HTTP/Headers.html
require "http/server"
require "http/headers"
 
server = HTTP::Server.new do |context|
  req = context.request
  res = context.response
  res.content_type = "text/plain"
  req.headers.each do |key, value|
    puts "#{key}: #{value}"  # サーバ側にクライアントからのヘッダ内容を表示する。
  end
  res.headers.add("Allow", "GET, HEAD")  # Allow ヘッダを追加して応答を返す。
  res.puts "Allow GET, HEAD"
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

このサンプルを実行し、-I オプションを付けた curl コマンドでリクエストを行うと次のように表示される。

$ curl -I http://localhost:8080
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: text/plain
Allow: GET, HEAD
Content-Length: 16
$

Crystal: HTTP/Handler

HTTP/Handler モジュールはハンドラのベースとなるモジュールでカスタムハンドラクラスに include して使う。

そうしてできたカスタムハンドラクラスのインスタンスは HTTP::Server のハンドラ配列をパラメータとして取るタイプのコンストラクタを使って HTTPサーバに組み込む。

次のサンプルは HTTP メソッドとして GET と POST のみを受け入れ、それ以外のメソッドに対して BAD_REQUEST ステータスを返すカスタムハンドラの例である。

# server Handler https://crystal-lang.org/api/1.9.2/HTTP/Handler.html
require "http/server"
require "http/server/handler"
 
# カスタムハンドラ
class CustomHandler
  include HTTP::Handler
 
  # サーバからコールバックされるメソッド
  def call(context)
    # HTTP メソッドをチェック
    if context.request.method == "GET" || context.request.method == "POST"
      call_next(context)  # GET, POST の場合は下流のハンドラへ Context を渡す。
    else
      # GET, POST 以外のメソッドの場合は BAD_REQUEST にする。
      context.response.respond_with_status(HTTP::Status::BAD_REQUEST)
    end
  end
end
 
# サーバのインスタンス化のときにカスタムハンドラを組み込む。
server = HTTP::Server.new([CustomHandler.new]) do |context|
  req = context.request
  res = context.response
  res.content_type = "text/html; charset=utf-8"
  res.print <<-EOS
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>HTTP/Handler</title>
  <style>
   h1 {
     text-align: center;
     padding: 6px;
   }
</head>
 
<body>
 <h1>HTTP/Handler</h1>
 <p style="margin-left:20%;">#{req.method}</p>
</body>
</html>  
EOS
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

次の実行例では HEAD メソッドを送ったので BAD_REQUEST が返される。

$ curl -I http://localhost:8080
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Content-Length: 16
$

Crystal: HTTP/CompressHandler

HTTP/CompressHanlder は HTTP モジュールに含まれているカスタムハンドラで圧縮されたリクエスト処理したり、レスポンスを圧縮してクライアントへ返す処理を行う。

このハンドラを組み込むには HTTP/Server のハンドラ配列をパラメータにするコンストラクタを使って HTTP サーバを構築する。

次に CompressHandler を組み込んだ HTTP サーバの例を示す。

# server CompressHanlder https://crystal-lang.org/api/1.9.2/HTTP/CompressHandler.html
require "http/server"
 
# CompressHandler を組み込んで HTTP サーバを構築する。
server = HTTP::Server.new([HTTP::CompressHandler.new]) do |context|
  req = context.request
  res = context.response
  res.content_type = "text/html; charset=utf-8"
  request_headers = ""
  # ヘッダ一覧を取得して Accept-Encoding ヘッダが含まれているか確認する。
  req.headers.each do |k, v|
    request_headers += "<li>"
    request_headers += (k + ":" + v.to_s)
    request_headers += "</li>\n"
  end
  # クライアントへ応答を返す。
  res.print <<-EOS
<!doctype html>
<html>
  <head>
  <meta charset="utf-8" />
  <title>HTTP/CompressHandler</title>
  <style>
   h1 {
     text-align: center;
     padding: 6px;
   }  
  </head>
   
  <body>
   <h1>CompressHanlder</h1>
   <ul>
   #{request_headers}
   </ul>
  </body>
</html>  
EOS
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

次の例は curl コマンドで Accept-Encoding: gzip ヘッダを追加してリクエストを送ったときの動作である。

$ curl -H "Accept-Encoding: gzip" --output ./save.gz http://localhost:8080
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   256  100   256    0     0   265k      0 --:--:-- --:--:-- --:--:--  250k
$

この結果として save.gz というファイルが保存されるので、それを gunzip コマンドで解凍すると内部にレスポンスとしてサーバが返した HTML が保存されていることがわかる。

Crystal: HTTP/ErrorHandler

HTTP/ErrorHandler はサーバ内部のエラーをログとして保存する場合に使用する。

コンストラクタは次のようになっている。verbose を true にすると詳しいエラー情報を保存できる。log は Log クラスのインスタンスでデフォルトでは標準出力にログが出されるので、ファイルにログを保存したい場合は、バックエンドを作成してそれをバインドする必要がある。

.new(verbose : Bool = false, log : Log = Log.for("http.server"))

HTTP サーバへの組み込みは他のハンドラ同様、ハンドラの配列をパラメータとするコンストラクタを使う。つまり、パラメータとして配列の中にこのハンドラのインスタンスを含めて HTTP サーバを構築する。

次に ErrorHandler の使用例を示す。この例では、ログの出力先を log.txt というファイルにしている。また、すべてのリクエストに対して例外を発生させ、ErrorHanlder を動作させている。

# http ErrorHanlder https://crystal-lang.org/api/1.9.2/HTTP/ErrorHandler.html
require "log"
require "http/server"
 
# ログのバックエンドを作成
backend = ::Log::IOBackend.new(File.new("./log.txt", "a"))
# ログの出力先 (backend) をバインドする。
::Log.setup do |c|
  c.bind("*", :info, backend)  # 任意のログソース "*" に対してバインドを行う。
end
# 名前 "http.server" のログオブジェクトを構築
log = ::Log.for("http.server")
 
# ErrorHandler を組み込んで HTTP サーバを構築する。
server = HTTP::Server.new([HTTP::ErrorHandler.new(true, log)]) do |context|
  req = context.request
  res = context.response
  res.content_type = "text/html; charset=utf-8"
  raise Exception.new("Fatal Error")  # 常にエラーを発生させる。
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

この HTTP サーバを起動し、curl コマンドでリクエストを行うと次のように表示される。

$ curl http://localhost:8080
ERROR: Fatal Error (Exception)
  from server_error.cr:19:3 in '->'
  ........
  ........
$

このとき、log.txt の内容を表示すると次のようになる。

$ cat log.txt
2023-09-30T07:30:58.502558Z  ERROR - http.server: Unhandled exception
Fatal Error (Exception)
  from server_error.cr:19:3 in '->'
 ...................
 ...................
$

Crystal: HTTP/LogHandler

HTTP/LogHandler クラスは HTTP サーバでログを取るためのハンドラである。

ログはデフォルトでは標準出力に出力されるが、ファイルに保存するためにはバックエンドを作成し、それをログにバインドする必要がある。

ログオブジェクトを HTTP サーバに連携させるには、HTTP/Server のハンドラの配列をパラメータとするコンストラクタを使う。

下に、LogHandler を使う HTTP サーバの例を示す。

# http LogHandler  https://crystal-lang.org/api/1.9.2/HTTP/LogHandler.html
require "http"
require "log"
 
# ログのバックエンドを作成
backend = ::Log::IOBackend.new(File.new("./log.txt", "a"))
# ログの出力先 (backend) をバインドする。
::Log.setup do |c|
  c.bind("*", :info, backend)  # 任意のログソース "*" に対してバインドを行う。
end
# 名前 "http.server" のログオブジェクトを構築
log = ::Log.for("http.server")
 
# LogHandler を組み込んで HTTP サーバを構築する。
server = HTTP::Server.new([HTTP::LogHandler.new(log)]) do |context|
  req = context.request
  res = context.response
  res.content_type = "text/plain; charset=utf-8"
  s = %(method: "#{req.method}", path: "#{req.path}",  hostname: "#{req.hostname}")  # ログ内容
  res.puts s  # 応答を返す。
  Log.info {s}  # ログを取る。
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

このサンプルに対してリクエストを行うと ./log.txt というファイルにログが追加される。

そのファイルの内容の例を下に示す。

2023-09-30T10:52:28.587283Z   INFO - method: "GET", path: "/take",  hostname: "localhost"
2023-09-30T10:52:28.587325Z   INFO - http.server: 127.0.0.1 - GET /take HTTP/1.1 - 200 (78.69µs)
2023-09-30T10:52:36.939774Z   INFO - method: "GET", path: "/put",  hostname: "localhost"
2023-09-30T10:52:36.939802Z   INFO - http.server: 127.0.0.1 - GET /put HTTP/1.1 - 200 (19.69µs)

Crystal: HTTP/StaticFileHandler

HTTP/StaticFileHandler を使うと、HTML ファイルや画像ファイルのような静的ファイルをレスポンスとして返すことができる。

StaticFileHandler クラスのコンストラクタは次のように定義されている。ここで public_dir に静的ファイルのあるフォルダを指定する。

.new(public_dir : String, fallthrough = true, directory_listing = true)

そして、StaticFileHandler をインスタンス化して、HTTP サーバのハンドラ配列をパラメータとするコンストラクタに渡すことで、HTTP サーバとの連携ができる。

fallthrough は必ず true にする。そうしないと、GET メソッドしか受け付けなくなる。

次に StaticFileHandler の使用例を示す。この例では、./html というフォルダ内に HTML ファイルなどを配置しておけばリクエストに対して StaticFileHandler がそのファイルをレスポンスとして返す。

# server StaticFileHandler https://crystal-lang.org/api/1.9.2/HTTP/StaticFileHandler.html
require "http/server"
 
# StaticFileHandler を組み込んで HTTP サーバを構築する。
server = HTTP::Server.new([HTTP::StaticFileHandler.new("./html")]) do |context|
  context.response.respond_with_status(HTTP::Status::BAD_REQUEST, "The requests must be static files only.")
end
 
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

例えば、./html フォルダに index.html というファイルがある場合は、http://localhost:8080/index.html という URL でリクエストを行う。

Crystal: HTTP/WebSocketHandler

HTTP / WebSocketHandler は HTTP サーバでウェブソケットをサポートするときに使用する。

ウェブソケットはチャットのような双方向でリアルタイム性が必要なウェブアプリケーションを実現するのに使用する。

WebSocketHandler も他のハンドラ同様に HTTP サーバのハンドラ配列をパラメータとするコンストラクタを使用して HTTP サーバに連携させる。

次の例は、クライアントから PING があったとき、そのリクエストパスを PONG する WebSocketServer を構築し、HTTP サーバに連携させている。

# server WebSocketHandler https://crystal-lang.org/api/1.9.2/HTTP/WebSocketHandler.html
require "http/server"
 
# WebSocketHandler を構築する。
ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
  ws.on_ping { ws.pong ctx.request.path }  # PING に対しリクエストパスを PONG する。
end
 
# WebSocketHandler を連携させた HTTP サーバを構築する。
server = HTTP::Server.new [ws_handler] do |context|
  res = context.response
  res.content_type = "text/plain; charset=utf-8"
  res.puts "ws://localhost:8080"  # 普通の HTTP リクエストに応答する。
end
 
# クライアントからのリクエストをリスン
address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

このウェブソケットクライアントの例は、WebSocket 参照。

Crystal: HTTP/WebSocket

HTTP / WebSocket は主にウェブソケットクライアントを作るときに使用する。

ただ、ブラウザ上でウェブソケットを使う場合は、JavaScript を使うのが普通である。

ここでは、HTTP / WebSocketHandler の記事に出ているサンプルの WebSocketServer に対してリクエストを行う簡単な例を示す。

# WebSocket https://crystal-lang.org/api/1.9.2/HTTP/WebSocket.html
require "http/web_socket"
 
# WebSocket を構築する。
ws = HTTP::WebSocket.new(URI.parse("ws://localhost:8080"))
 
# PONG を受け取ったら表示
ws.on_pong do |m|
  puts "on_pong: " + m
end
 
# PING を送る。
ws.ping("Hello!")
 
# メッセージ待ちループ
ws.run

これを実行すると、次のように表示される。

$ ../bin/client_websocket 
on_pong: /
on_pong: Hello!
^C
$

Crystal: HTML

HTML モジュールには HTML の特殊文字をエスケープしたり逆エスケープする次のメソッドが含まれている。

  • escape(string : String, io : IO) : Nil
  • escape(string : Bytes, io : IO) : Nil
  • escape(string : String) : String
  • unescape(string : String) : String

エスケープされる文字は &, <, >, “, ‘ である。

次に簡単な例を示す。

# HTML module https://crystal-lang.org/api/1.9.2/HTML.html
require "html"
 
es = HTML.escape(%{<h1>"&esc" '/H1'})
p es
p HTML.unescape(es)

これを実行すると次のように表示される。

$ ../bin/html_module 
"&lt;h1&gt;&quot;&amp;esc&quot; '/H1'"
"<h1>\"&esc\" '/H1'"
$

Crystal: URI

URI クラスは URI の構築・分析および URL エンコード・デコードを行うメソッドを持つ。

URI のコンストラクタは次の2つの形式を持つ。

new(scheme : Nil | String = nil, host : Nil | String = nil, port : Int32 | Nil = nil, path : String = “”, query : String | Params | Nil = nil, user : Nil | String = nil, password : Nil | String = nil, fragment : Nil | String = nil)
parse(raw_url : String) : URI

また、クラスメソッドとして次のようなものを持つ。

  • decode(string : String, *, plus_to_space : Bool = false) : String
  • decode(string : String, io : IO, *, plus_to_space : Bool = false) : Nil
  • decode(string : String, io : IO, *, plus_to_space : Bool = false, &) : Nil
  • decode_www_form(string : String, io : IO, *, plus_to_space : Bool = true) : Nil
  • decode_www_form(string : String, *, plus_to_space : Bool = true) : String
  • encode(string : String, io : IO, space_to_plus : Bool = false, &) : Nil
  • encode_path(io : IO, string : String) : Nil
  • encode_path(string : String) : String
  • encode_path_segment(io : IO, string : String) : Nil
  • encode_path_segment(string : String) : String

次に URI の簡単な使用例を示す。

# URI https://crystal-lang.org/api/1.9.2/URI.html
require "uri"
 
# コンストラクタ1
uri1 = URI.new(scheme: "http", host: "localhost", port: 8080, path: "/form1")
 
# コンストラクタ2
uri2 = URI.parse("http://127.0.0.1/query/?name=foo")
 
p! uri1.to_s  # URI -> String
p! uri2.host # ホスト名を得る
p! uri2.query # クエリ文字列を得る。
p! uri2.port # ポート番号を得る。
 
# URL エンコードとデコード
p URI.encode_path(%{name=鈴木&address=city jp})
p URI.decode("name%3DNAME%20address")

このサンプルプログラムの実行例を示す。

$ ../bin/uri
uri1.to_s # => "http://localhost:8080/form1"
uri2.host # => "127.0.0.1"
uri2.query # => "name=foo"
uri2.port # => nil
"name%3D%E9%88%B4%E6%9C%A8%26address%3Dcity%20jp"
"name=NAME address"

Crystal: MIME

MIME モジュールは、MIME タイプつまり HTTP サーバからレスポンスを返す際に Content-Type ヘッダにセットする文字列を管理する。

Linux では /etc/mime.types というファイルにより MIME タイプが管理されており、そのファイルを検索することで、ファイル拡張子から MIME タイプを取得する。

また、既存の MIME タイプ一覧にないものは登録することもできる。

次に MIME モジュールの簡単な使用例を示す。

# MIME https://crystal-lang.org/api/1.9.2/MIME.html
require "mime"
 
p! MIME.from_extension(".jpg") # 拡張子から MIME タイプを得る。
p! MIME.from_filename("/var/www/html/default.html") # ファイル名から MIME タイプを得る。
 
# 未登録の MIME タイプをデータベース登録する。
MIME.from_extension?(".cr")     # => nil
MIME.extensions("text/crystal") # => Set(String).new
MIME.register(".cr", "text/crystal")
p! MIME.from_extension?(".cr")     # => "text/crystal"
p! MIME.extensions("text/crystal") # => Set(String){".cr"}

この実行例を下に示す。

$ ../bin/mime
MIME.from_extension(".jpg") # => "image/jpeg"
MIME.from_filename("/var/www/html/default.html") # => "text/html"
MIME.from_extension?(".cr") # => "text/crystal"
MIME.extensions("text/crystal") # => Set{".cr"}

Crystal: ECR

ECR とは Embeded Crystal の略のことで、文書に Crystal コードを埋め込むためのテンプレート言語を指す。

つまり、Ruby で言えば、erb に相当するテンプレート言語である。

ECR を使うことにより、HTTP サーバをウェブアプリケーションサーバとして利用できるようになる。

次の例は product.ecr というテンプレートファイル (HTML) にクラスのインスタンス変数を埋め込む例である。

# ECR https://crystal-lang.org/api/1.9.2/ECR.html
require "ecr"
 
class ProductEmbed
  def initialize(@product : String, @price : Int32, @amount : Int32)
  end
   
  ECR.def_to_s "./product.ecr"
end
 
print ProductEmbed.new("pin", 5, 100).to_s

product.ecr ファイルの内容は次のようである。

<!doctype html>
<html>
 <head>
  <meta charset="utf8" />
  <title><%= @product %></title>
 </head>
  
 <body>
  <h1><%= @product %></h1>
  <ul>
    <li>price = <%= @price %></li>
    <li>amount = <%= @amount %></li>
  </ul>
 </body>
</html>

このプログラムを実行すると、次のように表示される。

$ ../bin/ecr
<!doctype html>
<html>
 <head>
  <meta charset="utf8" />
  <title>pin</title>
 </head>
 
 <body>
  <h1>pin</h1>
  <ul>
    <li>price = 5</li>
    <li>amount = 100</li>
  </ul>
 </body>
</html>
$

ECR は変数を文書に埋め込むだけでなく Crystal のコードも定義できる。次の例では埋め込むデータはタプルの配列であり、配列の each メソッドを使ってリストとして HTML に埋め込んでいる。

# ECR https://crystal-lang.org/api/1.9.2/ECR.html
require "ecr"
 
class ProductEmbed
  def initialize(@products : Array({String, Int32, Int32}))
  end
   
  ECR.def_to_s "./products.ecr"
end
 
products = Array({String, Int32, Int32}).new
products << {"Hook", 5, 10}
products << {"Pin", 1, 20}
products << {"Coil", 4, 12}
products << {"Wire", 5, 8}
print ProductEmbed.new(products).to_s

この products.ecr というテンプレートファイルは次のとおりである。

<!doctype html>
<html>
 <head>
  <meta charset="utf8" />
  <title>生産物一覧</title>
 </head>
  
 <body>
  <h1>生産物一覧</h1>
  <div>
    <% @products.each do |p| %>
    <ul>
    <li>product = <%= p[0] %></li>
    <li>price = <%= p[1] %></li>
    <li>amount = <%= p[2] %></li>
    </ul>
    <% end %>
  </div>
 </body>
</html>

このプログラムを実行すると、下のように表示される。

$ ../bin/ecr_for
<!doctype html>
<html>
 <head>
  <meta charset="utf8" />
  <title>生産物一覧</title>
 </head>
 
 <body>
  <h1>生産物一覧</h1>
  <div>
    
    <ul>
    <li>product = Hook</li>
    <li>price = 5</li>
    <li>amount = 10</li>
    </ul>
    
    <ul>
    <li>product = Pin</li>
    <li>price = 1</li>
    <li>amount = 20</li>
    </ul>
    
    <ul>
    <li>product = Coil</li>
    <li>price = 4</li>
    <li>amount = 12</li>
    </ul>
    
    <ul>
    <li>product = Wire</li>
    <li>price = 5</li>
    <li>amount = 8</li>
    </ul>
    
  </div>
 </body>
</html>
$

クラスを作らずに埋め込みたいとき

render メソッドを使うと、簡単に変数を .ecr ファイルに埋め込むことができる。

ECR ファイルの例

# greeting.ecr
Hello <%= name %>!

コードの例

require "ecr/macros"
 
name = "World"
 
rendered = ECR.render "greeting.ecr"
rendered # => "Hello World!"

Crystal: TCPサーバとクライアント

TCPServer クラスは TCP サーバを作るときに便利なクラスである。一方、TCPSocket クラスは TCP クライアントを作るときに使用するクラスである。

TCPServer は HTTP サーバを作るときにもベースとなるサーバとして利用される。

次のコードは TCPServer クラスを使った TCP サーバの簡単な例である。

# TCP Server https://crystal-lang.org/api/1.9.2/TCPServer.html
require "socket"
 
def handle_client(client)
  message = client.gets
  client.puts "Echo: #{message}"  # クライアントからのメッセージをそのまま返す。
end
 
# TCP サーバを構築
server = TCPServer.new("localhost", 1234)
puts "Waiting connection .."
while client = server.accept?  # クライアントの接続を待つ。
  puts "Accepted."
  spawn handle_client(client)  # Fiber として handle_client を実行する。
end

次のコードは TCPSocket クラスを使った TCP クライアントの簡単な例である。

# TCP Client https://crystal-lang.org/api/1.9.2/TCPSocket.html
require "socket"
 
client = TCPSocket.new("localhost", 1234)
client << "This is the message from TCPClient\n"  # TCP サーバへ送信。
puts client.gets  # TCP サーバから文字列を受信
client.close

次にこれら TCP サーバと TCP クライアントの使用例を示す。

TCP サーバ側

$ ../bin/tcp_server
Waiting connection ..
Accepted.

TCP クライアント側

$ ../bin/tcp_client
Echo: This is the message from TCPClient
$

Crystal: UNIX サーバとクライアント

UNIXServer は、TCP サーバと異なり、他のコンピュータ上のプロセスとは通信できない。

したがって、同じコンピュータ上で動作しているプロセス間通信に使用される。

同じコンピュータ内のプロセスとの通信なので、IP アドレスやポートは使用せず、その代わりにファイルパスを使用する。このファイルパスはユニークでなけらばならず、そのファイルがすでに存在した場合はエラーとなる。

UNIX サーバは UNIXServer クラスを使用するが、UNIX クライアントは UNIXSocket クラスを使用する。

次に簡単な例を示す。

UNIX サーバ

# UNIXServer https://crystal-lang.org/api/1.9.2/UNIXServer.html
require "socket"
 
# クライアントからのメッセージを受信してそのままエコーする。
def handle_client(client)
  message = client.gets
  puts message
  client.puts message
end
 
# UNIX サーバを構築
PATH = "/tmp/myapp1.sock"
if File.delete?(PATH)  # 以前のセッションの PATH が残っていたら削除する。
  puts "Deleted former #{PATH}."
end
puts "Started with #{PATH}."
server = UNIXServer.new(PATH)
# クライアントからの接続待ち
while client = server.accept?
  puts "Accepted .."
  spawn handle_client(client)  # 接続したら handle_client メソッドをファイバで起動する。
end

UNIX クライアント

# UNIXSocket https://crystal-lang.org/api/1.9.2/UNIXSocket.html
require "socket"
PATH = "/tmp/myapp1.sock"
 
# UNIXSocket オブジェクトを作成する。
sock = UNIXSocket.new(PATH)
sock.puts "The Message from UNIX client."  # メッセージを送る。
response = sock.gets  # 応答メッセージ
p response
sock.close
puts "The UNIX client closed."

次にこれらのプログラムの動作例を示す。

UNIX サーバ

$ ./bin/unix_server
Deleted former /tmp/myapp1.sock.
Started with /tmp/myapp1.sock.
Accepted ..
The Message from UNIX client.

UNIX クライアント

$  ./bin/unix_client
"The Message from UNIX client."
The UNIX client closed.

Crystal: UDP サーバとクライアント

UDPSocket クラスは UDP 通信を行うためのソケットである。TCPSocket が TCP 層で動作するのに対して UDPSocket は IP 層で動作する。

TCP 層は IP 層の上に構築されていてより上位のプロトコルであり、ストリームを扱いエラー監視なども行えるが、UDP はパケット通信でエラー監視はより簡略である。

そのため、インターネット上で UDP 通信は特殊なプロトコルだけで使われ、信頼性の高い通信には TCP が使用される。

次に、UDPSocket を使った簡単なプログラムを示す。

UDP サーバ

# UDPSocket server https://crystal-lang.org/api/1.9.2/UDPSocket.html
require "socket"
 
# サーバを作成
server = UDPSocket.new
server.bind "localhost", 1234
puts "Create server localhost:1234"
 
# クライアントからのメッセージを受信する。
message, client_addr = server.receive
p message
p client_addr
 
# 接続を閉じる
server.close
puts "The connection closed."

UDP クライアント

# UDPSocket client https://crystal-lang.org/api/1.9.2/UDPSocket.html
require "socket"
 
# クライアントを作成してサーバに接続する。
client = UDPSocket.new
client.connect "localhost", 1234
puts "Create the client localhost:1234"
 
# サーバへメッセージを送る。
begin
  client.send "Message from the client."
rescue ex : Socket::ConnectError
  STDERR.puts ex.message
end
 
# 接続を閉じる。
client.close
puts "The connection closed."

これらのプログラムの使用例を下に示す。

UDP サーバ

$ ./bin/udp_server 
Create server localhost:1234
"Message from the client."
Socket::IPAddress(127.0.0.1:48096)
The connection closed.
$

UDP クライアント

$ ./bin/udp_client 
Create the client localhost:1234
The connection closed.
$
4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4