Echo server
- まずはEcho Serverを作成することから始める。TCP Serverは以下のStepを実行する
- 利用可能なPortを開いてListenする
- そのPortでクライアントからの接続を待ち、受け入れる
- クライアントからの要求を解析し、レスポンスを返す。
- KVServerアプリケーションにこれらの機能を組み込んでみる。
lib/kv_server.ex
を開いて以下のように編集する。
# 指定されたportでListenを開始する
def accept(port) do
# The options below mean:
#
# 1. `:binary` - receives data as binaries (instead of lists)
# 2. `packet: :line` - receives data line by line
# 3. `active: false` - blocks on `:gen_tcp.recv/2` until data is available
# 4. `reuseaddr: true` - allows us to reuse the address if the listener crashes
#
{:ok, socket} = :gen_tcp.listen(port,
[:binary, packet: :line, active: false, reuseaddr: true])
IO.puts "Accepting connections on port #{port}"
loop_acceptor(socket)
end
# socketを受け続けるループ。
defp loop_acceptor(socket) do
{:ok, client} = :gen_tcp.accept(socket)
serve(client)
loop_acceptor(socket)
end
# socketを受け取っったときの処理
defp serve(socket) do
socket
|> read_line()
|> write_line(socket)
serve(socket)
end
# socketを読み込む
defp read_line(socket) do
{:ok, data} = :gen_tcp.recv(socket, 0)
data
end
# TCPを通してResponseを返す
defp write_line(line, socket) do
:gen_tcp.send(socket, line)
end
iex -S mix
からREPLの中でListenを開始してみる
iex> KVServer.accept(4040)
Accepting connections on port 4040
- 開始されたらTerminalからtelnetでローカルホストの4040ポートに接続して、適当な文字を送ってみる
$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello world !!
hello world !!
- 送った
hello world !!
がそのまま返されていることがわかる。
- telnetの接続を終了させると以下のようなエラーがサーバー側に表示される。
iex(1)> KVServer.accept(4040)
Accepting connections on port 4040
** (MatchError) no match of right hand side value: {:error, :closed}
(kv_server) lib/kv_server.ex:50: KVServer.read_line/1
(kv_server) lib/kv_server.ex:43: KVServer.serve/1
(kv_server) lib/kv_server.ex:37: KVServer.loop_acceptor/1
- これは
gen_tcp.recv/2
でsocketを受け取ることを期待しているのに接続が閉じられたためで、その場合のハンドルが足りていない。そして、例外が発生したと同時にサーバーが終了して再起動しないことがわかる。これらを解決するためにはsupervision tree
の元でプロセスを動かす必要があることに気づくだろう。
Tasks
- Taskモジュールを使ってKVServerを
supervision tree
の監督下の元で動かす。再びlib/kv_server.ex
を編集する。
def start(_type, _args) do
import Supervisor.Spec
children = [
worker(Task, [KVServer, :accept, [4040]])
]
opts = [strategy: :one_for_one, name: KVServer.Supervisor]
Supervisor.start_link(children, opts)
end
- これでサーバーは
supervision tree
の一部となったのでmix run --no-halt
でアプリケーションを動かしてみよう。--no-halt
はアプリケーションのプロセスを止めないで起動するオプションである。
- サーバーが立ち上がるはずなので同じくtelnetでローカルホストの4040ポートに接続してメッセージを送ってみる。問題なくメッセージは帰ってくるはずだ。
- さきほどと同じようにtelnetを終了してみる。そうすると今度はエラーメッセージは表示されるが、サーバー自体は終了しない。これは
one_for_one
戦略に従いsupervisor
が終了したサーバーのプロセスを再起動しているため。
- では、別のターミナルで2個目のコネクションを張って同じくメッセージを送ってみる。すると接続はできるがレスポンスは返ってこない。今の作りでは接続中のクライアントがすでに存在していると新しいクライアントは受け入れられていないことがわかる。
Task supervisor
- 複数のコネクションを処理するためにはコネクションを受け入れるプロセスとコネクションを処理するプロセスを分離する必要がある。
- 再び
start/2
を編集する。
def start(_type, _args) do
import Supervisor.Spec
# TaskのSupervisorをsupervision treeに追加
children = [
supervisor(Task.Supervisor, [[name: KVServer.TaskSupervisor]]),
worker(Task, [KVServer, :accept, [4040]])
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: KVServer.Supervisor]
Supervisor.start_link(children, opts)
end
TaskSupervisor
をsupervision tree
の下に追加した。次にコネクションを処理するプロセスをTaskSupervisor
の下に作るようにloop_acceptor/1
を変更する。
defp loop_acceptor(socket) do
{:ok, client} = :gen_tcp.accept(socket)
{:ok, pid} = Task.Supervisor.start_child(KVServer.TaskSupervisor, fn -> serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)
loop_acceptor(socket)
end
:ok = :gen_tcp.controlling_process(client, pid)
が追加された。これは子供のプロセスにクライアントからのソケットの"controlling process"を処理させる。これをしないとソケットはacceptするプロセスに縛られるため、クラッシュするとすべての子プロセスが死ぬ。
mix run --no-halt
でサーバーを開始して複数のクライアントから接続できるようになっているのを確認しよう。また、複数接続した後でどれかのコネクションを終了させても他の接続は保たれているのも確認できる。