この本は「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
"<h1>"&esc" '/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.
$