Cowboy基礎の基礎
Goal
- Elixir+Cowboy(2系)でHello World(text/plain)を表示する
- PlugからCowboyを使って、起動からリクエストを処理するまでのフローを眺める (Code Reading)
Foreword
お久しぶりです。みなさん。
2016年冬コミ(C91)で原稿とともに人権を落とした敗北主義者です。
2016年冬コミ(C91)で原稿とともに人権を落とした敗北主義者です。
今回は、原稿とはあまり関係ない部分をやっていきたいと思います。
(副声音:どうにも原稿を書くモチベーションが上がらないので息抜きさせてください!)
(副声音:どうにも原稿を書くモチベーションが上がらないので息抜きさせてください!)
内容としては、Cowboyで一番基本的な使い方をやることとPlugからの流れを追うことです。(何番煎じ?)
それでは、楽しんでいただけたら幸いです。
それでは、楽しんでいただけたら幸いです。
Dev-Environment
開発環境は下記のとおりです。
- OS: MacOS X v10.11.6
- Erlang: Eshell V8.2, OTP-Version 19
- Cowboy: v2.0.0-pre4
- Elixir: v1.3.4
- Plug: v1.3.0
Body
Hello from the cowboy
Create elixir project & Install package
Example:
$ mix new --sup cowboy_2_example
$ cd cowboy_2_example
$ mix test
File: mix.exs
defmodule Cowboy2Example.Mixfile do
...
def application do
[applications: [:logger, :cowboy],
mod: {Cowboy2Example, []}]
end
defp deps do
[{:cowboy, github: "ninenines/cowboy", tag: "2.0.0-pre.4"}]
end
end
Example:
$ mix deps.get
$ mix compile
Application & Supervisor setup
File: lib/cowboy_2_example.ex
defmodule Cowboy2Example do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec, warn: false
dispatch = :cowboy_router.compile routes
{:ok, _} = :cowboy.start_clear :http, 100, [{:port, 4000}], %{env: %{dispatch: dispatch}}
# Define workers and child supervisors to be supervised
children = [
# Starts a worker by calling: Cowboy2Example.Worker.start_link(arg1, arg2, arg3)
# worker(Cowboy2Example.Worker, [arg1, arg2, arg3]),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Cowboy2Example.Supervisor]
Supervisor.start_link(children, opts)
end
defp routes do
[{:_, [{"/", Cowboy2Example.Handlers.ExampleHandler, []}]}]
end
end
Create Handler
Example:
$ mkdir lib/handlers
$ touch lib/handlers/example_handler.ex
File: lib/handlers/example_handler.ex
defmodule Cowboy2Example.Handlers.ExampleHandler do
def init(req0, state) do
req = :cowboy_req.reply 200, %{"content-type" => "text/plain"}, "Example", req0
{:ok, req, state}
end
end
Hello World!
Example:
$ iex -S mix
Access URL: http://localhost:4000
Add HTML & JSON response
File: lib/handlers/example_handler.ex
defmodule Cowboy2Example.Handlers.ExampleHandler do
def init(req, opts) do
{:cowboy_rest, req, opts}
end
def content_types_provided(req, state) do
{
[{"text/html", :html_example},
{"application/json", :json_example},
{"text/plain", :text_example}],
req, state
}
end
def html_example(req, state) do
body = """
<html>
<head>
<meta charset=\"utf-8\">
<title>REST Example</title>
</head>
<body>
<h1>REST Example!!</h1>
</body>
</html>
"""
{body, req, state}
end
def json_example(req, state) do
body = "{\"rest\": \"Example!!\"}"
{body, req, state}
end
def text_example(req, state) do
{"REST Example as text!!", req, state}
end
end
- JSON
$ curl -i -H "Accept: application/json" http://localhost:4000
HTTP/1.1 200 OK
content-length: 21
content-type: application/json
date: Tue, 03 Jan 2017 13:34:08 GMT
server: Cowboy
vary: accept
{"rest": "Example!!"}
- text
$ curl -i -H "Accept: text/plain" http://localhost:4000
HTTP/1.1 200 OK
content-length: 22
content-type: text/plain
date: Tue, 03 Jan 2017 13:34:23 GMT
server: Cowboy
vary: accept
REST Example as text!!
- HTML
$ curl -i -H "Accept: text/css" http://localhost:4000
HTTP/1.1 406 Not Acceptable
content-length: 0
date: Tue, 03 Jan 2017 13:34:42 GMT
server: Cowboy
- HTML(use browser)
Access URL: http://localhost:4000
Cowboy to Plug flow
CowboyからPlugのフローを眺めていきましょう。
(読むのはPlugのソースです。Cowboyのソースは・・・Erlangできないんでわからないです!)
(読むのはPlugのソースです。Cowboyのソースは・・・Erlangできないんでわからないです!)
まずは、アプリケーションの起動対象が書いてあるmix.exsから追っていきましょう。
plug/mix.exs
def application do
[applications: [:crypto, :logger, :mime],
mod: {Plug, []}]
end
plug/mix.exs
def deps do
[{:mime, "~> 1.0"},
{:cowboy, "~> 1.0.1 or ~> 1.1", optional: true},
{:ex_doc, "~> 0.12", only: :docs},
{:inch_ex, ">= 0.0.0", only: :docs},
{:hackney, "~> 1.2.0", only: :test}]
end
利用しているCowboyはv1.0.1かv1.1のようです。
起動しているアプリケーションのモジュールはPlugですね。
次は、Plugを追いましょう。
起動しているアプリケーションのモジュールはPlugですね。
次は、Plugを追いましょう。
plug/lib/plug.ex
def start(_type, _args) do
Logger.add_translator {Plug.Adapters.Translator, :translate}
Plug.Supervisor.start_link()
end
start/2の中で起動しているSupervisorはPlug.Supervisorですね。
(さくさく進みますね〜)
(さくさく進みますね〜)
plug/lib/plug/supervisor.ex
def start_link() do
Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
end
plug/lib/plug/supervisor.ex
def init(:ok) do
import Supervisor.Spec
children = [
worker(Plug.Upload, [])
]
Plug.Keys = :ets.new(Plug.Keys, [:named_table, :public, read_concurrency: true])
supervise(children, strategy: :one_for_one)
end
さてさて、workerはPlug.Uploadのみ・・・だと!?
どうやらSupervisorまでではCowboyの「か」の字も出てこないようですね。
どうやらSupervisorまでではCowboyの「か」の字も出てこないようですね。
さて、ここからどうやって追いましょうか・・・。
そういえばPlug+Cowboyで以前に使った時は、起動するとき何かメソッドで実行していたはずです。
http/3だったかな?それを探しましょう。どこだ〜
そういえばPlug+Cowboyで以前に使った時は、起動するとき何かメソッドで実行していたはずです。
http/3だったかな?それを探しましょう。どこだ〜
plug/lib/plug/adapters/cowboy.ex
def http(plug, opts, cowboy_options \\ []) do
run(:http, plug, opts, cowboy_options)
end
余談ですがHTTPSを使うなら、こちらになるみたいですね。
plug/lib/plug/adapters/cowboy.ex
def https(plug, opts, cowboy_options \\ []) do
Application.ensure_all_started(:ssl)
run(:https, plug, opts, cowboy_options)
end
それと、下記のメソッドを使ってworkerに追加すればできるみたいです。
plug/lib/plug/adapters/cowboy.ex
def child_spec(scheme, plug, opts, cowboy_options \\ []) do
[ref, nb_acceptors, trans_opts, proto_opts] = args(scheme, plug, opts, cowboy_options)
ranch_module = case scheme do
:http -> :ranch_tcp
:https -> :ranch_ssl
end
:ranch.child_spec(ref, nb_acceptors, ranch_module, trans_opts, :cowboy_protocol, proto_opts)
end
本筋に戻りましょう。とりあえず、cowboy.exとそのままの名前でありましたね。
http/3内でrun/4を呼び出しています。run/4に行きましょう。
http/3内でrun/4を呼び出しています。run/4に行きましょう。
plug/lib/plug/adapters/cowboy.ex
defp run(scheme, plug, opts, cowboy_options) do
case Application.ensure_all_started(:cowboy) do
{:ok, _} ->
:ok
{:error, {:cowboy, _}} ->
raise "could not start the cowboy application. Please ensure it is listed " <>
"as a dependency both in deps and application in your mix.exs"
end
apply(:cowboy, :"start_#{scheme}", args(scheme, plug, opts, cowboy_options))
end
内容としては、Cowboyが起動しているか確認しているのと:cowboyの起動用メソッドを呼び出しているようです。
ここで終わってしまうのでしょうか?Cowboyに対して用意するHandlerとかはどこに・・・
さきほど出した”Hello World”のソースを思い出してみましょう。
どうやってHandlerを指定していたでしょうか?
ここで終わってしまうのでしょうか?Cowboyに対して用意するHandlerとかはどこに・・・
さきほど出した”Hello World”のソースを思い出してみましょう。
どうやってHandlerを指定していたでしょうか?
Example
defmodule Cowboy2Example do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
dispatch = :cowboy_router.compile routes
{:ok, _} = :cowboy.start_clear :http, 100, [{:port, 4000}], %{env: %{dispatch: dispatch}}
...
end
defp routes do
[{:_, [{"/", Cowboy2Example.Handlers.ExampleHandler, []}]}]
end
end
そうそうルーティングで指定して、その内容を起動用のメソッドに渡していますね。(:dispatchがキー)
先ほどのソースならapplyしているときのargs/4が怪しいです。見にいきましょう。
先ほどのソースならapplyしているときのargs/4が怪しいです。見にいきましょう。
plug/lib/plug/adapters/cowboy.ex
def args(scheme, plug, opts, cowboy_options) do
{cowboy_options, non_keyword_options} =
Enum.partition(cowboy_options, &is_tuple(&1) and tuple_size(&1) == 2)
cowboy_options
|> Keyword.put_new(:max_connections, 16_384)
|> Keyword.put_new(:ref, build_ref(plug, scheme))
|> Keyword.put_new(:dispatch, cowboy_options[:dispatch] || dispatch_for(plug, opts))
|> normalize_cowboy_options(scheme)
|> to_args(non_keyword_options)
end
cowboyのオプションに分解してますね・・・。ビンゴ!見事:dispatchキーを見つけました。
さてさて次はdispatch_for/2ですね。
さてさて次はdispatch_for/2ですね。
plug/lib/plug/adapters/cowboy.ex
defp dispatch_for(plug, opts) do
opts = plug.init(opts)
[{:_, [{:_, Plug.Adapters.Cowboy.Handler, {plug, opts}}]}]
end
よしよし、ようやっとHandlerを見つけたぞ!
このモジュールを追っていきます。
いくつか関数がありますが、まぁinit/3から。
このモジュールを追っていきます。
いくつか関数がありますが、まぁinit/3から。
plug/lib/plug/adapters/cowboy/handler.ex
def init({transport, :http}, req, {plug, opts}) when transport in [:tcp, :ssl] do
{:upgrade, :protocol, __MODULE__, req, {transport, plug, opts}}
end
あぁ、Cowboyのプロトコルアップグレードですね。
参考: Cowboy User Guide(1.0) - Protocol upgrades
参考: Cowboy User Guide(1.0) - Protocol upgrades
なら次は、upgrade/4です。
plug/lib/plug/adapters/cowboy/handler.ex
@connection Plug.Adapters.Cowboy.Conn
@already_sent {:plug_conn, :sent}
plug/lib/plug/adapters/cowboy/handler.ex
def upgrade(req, env, __MODULE__, {transport, plug, opts}) do
conn = @connection.conn(req, transport)
try do
%{adapter: {@connection, req}} =
conn
|> plug.call(opts)
|> maybe_send(plug)
{:ok, req, [{:result, :ok} | env]}
catch
:error, value ->
stack = System.stacktrace()
exception = Exception.normalize(:error, value, stack)
reason = {{exception, stack}, {plug, :call, [conn, opts]}}
terminate(reason, req, stack)
:throw, value ->
stack = System.stacktrace()
reason = {{{:nocatch, value}, stack}, {plug, :call, [conn, opts]}}
terminate(reason, req, stack)
:exit, value ->
stack = System.stacktrace()
reason = {value, {plug, :call, [conn, opts]}}
terminate(reason, req, stack)
after
receive do
@already_sent -> :ok
after
0 -> :ok
end
end
end
おおっと、長いな・・・。細かいところは省きましょう。メインの流れだけ追います。
リクエストをConnに分解(?)しているところから。
リクエストをConnに分解(?)しているところから。
plug/lib/plug/adapters/cowboy/conn.ex
def conn(req, transport) do
{path, req} = :cowboy_req.path req
{host, req} = :cowboy_req.host req
{port, req} = :cowboy_req.port req
{meth, req} = :cowboy_req.method req
{hdrs, req} = :cowboy_req.headers req
{qs, req} = :cowboy_req.qs req
{peer, req} = :cowboy_req.peer req
{remote_ip, _} = peer
%Plug.Conn{
adapter: {__MODULE__, req},
host: host,
method: meth,
owner: self(),
path_info: split_path(path),
peer: peer,
port: port,
remote_ip: remote_ip,
query_string: qs,
req_headers: hdrs,
request_path: path,
scheme: scheme(transport)
}
end
分解してPlug.Connに入れ直しているだけですね。
call/2はいいとして、maybe_send/2は確認しておきましょう。
call/2はいいとして、maybe_send/2は確認しておきましょう。
plug/lib/plug/adapters/cowboy/handler.ex
defp maybe_send(%Plug.Conn{state: :unset}, _plug), do: raise Plug.Conn.NotSentError
defp maybe_send(%Plug.Conn{state: :set} = conn, _plug), do: Plug.Conn.send_resp(conn)
defp maybe_send(%Plug.Conn{} = conn, _plug), do: conn
defp maybe_send(other, plug) do
raise "Cowboy adapter expected #{inspect plug} to return Plug.Conn but got: #{inspect other}"
end
ふむふむ、この先があるのは:stateが:setか・・・Plug.Conn.send_resp/1へ。
plug/lib/plug/conn.ex
def send_resp(conn)def send_resp(%Conn{state: :unset}) do
raise ArgumentError, "cannot send a response that was not set"
end
def send_resp(%Conn{adapter: {adapter, payload}, state: :set, owner: owner} = conn) do
conn = run_before_send(conn, :set)
{:ok, body, payload} = adapter.send_resp(payload, conn.status, conn.resp_headers, conn.resp_body)
send owner, @already_sent
%{conn | adapter: {adapter, payload}, resp_body: body, state: :sent}
end
def send_resp(%Conn{}) do
raise AlreadySentError
end
そろそろ疲れてきたよ〜><
しかし、もう少しだけ頑張ろう!
しかし、もう少しだけ頑張ろう!
3つ目かな。run_before_send/2は情報を取得しているだけだからいいや。
adapter.send_resp/4の部分だな。adapterはPlug.Adapters.Cowboy.Connで生成しているConnにありましたね。
adapter.send_resp/4の部分だな。adapterはPlug.Adapters.Cowboy.Connで生成しているConnにありましたね。
plug/lib/plug/adapters/cowboy/conn.ex
%Plug.Conn{
adapter: {__MODULE__, req},
host: host,
method: meth,
owner: self(),
path_info: split_path(path),
peer: peer,
port: port,
remote_ip: remote_ip,
query_string: qs,
req_headers: hdrs,
request_path: path,
scheme: scheme(transport)
}
やっと・・・やっとたどり着いたぞー。
:cowboy_req.reply/4をやっと確認できました。
:cowboy_req.reply/4をやっと確認できました。
plug/lib/plug/adapters/cowboy/conn.ex
def send_resp(req, status, headers, body) do
status = Integer.to_string(status) <> " " <> Plug.Conn.Status.reason_phrase(status)
{:ok, req} = :cowboy_req.reply(status, headers, body, req)
{:ok, nil, req}
end
もうだめ・・・orz
他にもまだ何かあるかもしれませんが、とりあえずここまでで大丈夫でしょう。
他にもまだ何かあるかもしれませんが、とりあえずここまでで大丈夫でしょう。
Afterword
凡百プログラマの私ではここまでです。
では、またお会いすることがあればノシ
では、またお会いすることがあればノシ
Bibliography
参考にした書籍及びサイトの一覧は下記になります。