2014/06/30

erlang - cowboy - examples (2)

cowboy source code 裡面有一個 examples 目錄,列出多個範例程式,接下來我們藉由閱讀程式碼的方式,了解如何使用 cowboy。

rest_basic_auth

  1. rest_basic_auth.app.src
  2. rest_basic_auth_sup.erl
  3. rest_basic_auth_app.erl
  4. toppage_handler.erl

     %% 將 protocol upgrade 到 cowboy_rest
     init(_Transport, _Req, []) ->
         {upgrade, protocol, cowboy_rest}.
    
     %% 檢查 header 裡面的 authorization 欄位,查看是否有 User 資料
     %% 如果有,就把資料放在 State 的地方 return 回去
     is_authorized(Req, State) ->
         {ok, Auth, Req1} = cowboy_req:parse_header(<<"authorization">>, Req),
         case Auth of
             {<<"basic">>, {User = <<"Alladin">>, <<"open sesame">>}} ->
                 {true, Req1, User};
             _ ->
                 {{false, <<"Basic realm=\"cowboy\"">>}, Req1, State}
         end.
    
     %% 提供 plain text mime type 的 response
     content_types_provided(Req, State) ->
         {[
             {<<"text/plain">>, to_text}
         ], Req, State}.
    
     to_text(Req, User) ->
         {<< "Hello, ", User/binary, "!\n" >>, Req, User}.

如果 header 裡面沒有測試

> curl -i http://localhost:8080
HTTP/1.1 401 Unauthorized
connection: keep-alive
server: Cowboy
date: Wed, 23 Apr 2014 06:22:18 GMT
content-length: 0
www-authenticate: Basic realm="cowboy"

以 -u username:password 設定 Basic Authentication 的帳號及密碼

> curl -i -u "Alladin:open sesame" http://localhost:8080
HTTP/1.1 200 OK
connection: keep-alive
server: Cowboy
date: Wed, 23 Apr 2014 06:23:03 GMT
content-length: 16
content-type: text/plain

Hello, Alladin!

rest_pastebin

這個例子會將 form data 儲存到 server 的檔案中,並產生一個獨立的網址,後續可再由瀏覽器取得剛剛發送到 server 的 html/text 的資料。

  1. rest_pastebin.app.src
  2. rest_pastebin_sup.erl
  3. rest_pastebin_app.erl
  4. toppage_handler.erl

    init 時,先以 now() 設定為 random seed,將 protocol upgrade 到 cowboy_rest

     init(_Transport, _Req, []) ->
         % For the random number generator:
         {X, Y, Z} = now(),
         random:seed(X, Y, Z),
         {upgrade, protocol, cowboy_rest}.

    實作四個標準的 callback functions

     %% Standard callbacks.
     -export([init/3]).
     -export([allowed_methods/2]).
     -export([content_types_provided/2]).
     -export([content_types_accepted/2]).
     -export([resource_exists/2]).
    
     %% 設定接受 GET, POST methods
     allowed_methods(Req, State) ->
         {[<<"GET">>, <<"POST">>], Req, State}.
    
     %% 接受的 request content-type
     content_types_accepted(Req, State) ->
         {[{{<<"application">>, <<"x-www-form-urlencoded">>, []}, create_paste}],
             Req, State}.
    
     %% 產生兩種 mime type response
     content_types_provided(Req, State) ->
         {[
             {{<<"text">>, <<"plain">>, []}, paste_text},
             {{<<"text">>, <<"html">>, []}, paste_html}
         ], Req, State}.
    
     %% 判斷網址資源是否已經存在,網址資源 id 是儲存在 cowboy_req 的 bindings 裡面
     resource_exists(Req, _State) ->
         case cowboy_req:binding(paste_id, Req) of
             {undefined, Req2} ->
                 {true, Req2, index};
             {PasteID, Req2} ->
                 case valid_path(PasteID) and file_exists(PasteID) of
                     true -> {true, Req2, PasteID};
                     false -> {false, Req2, PasteID}
                 end
         end.

    3 個 custom callback functions

     %% Custom callbacks.
     -export([create_paste/2]).
     -export([paste_html/2]).
     -export([paste_text/2]).
    
     %% 接收到 form data 之後,就把 form data 寫到檔案中
     %% 檔案的路徑為 cowboy-0.9.0/examples/rest_pastebin/_rel/lib/rest_pastebin-1/priv
     create_paste(Req, State) ->
         PasteID = new_paste_id(),
         {ok, [{<<"paste">>, Paste}], Req3} = cowboy_req:body_qs(Req),
         ok = file:write_file(full_path(PasteID), Paste),
         case cowboy_req:method(Req3) of
             {<<"POST">>, Req4} ->
                 {{true, <<$/, PasteID/binary>>}, Req4, State};
             {_, Req4} ->
                 {true, Req4, State}
         end.
    
     paste_html(Req, index) ->
         {read_file("index.html"), Req, index};
     paste_html(Req, Paste) ->
         {Style, Req2} = cowboy_req:qs_val(<<"lang">>, Req, plain),
         {format_html(Paste, Style), Req2, Paste}.
    
     paste_text(Req, index) ->
         {read_file("index.txt"), Req, index};
     paste_text(Req, Paste) ->
         {Style, Req2} = cowboy_req:qs_val(<<"lang">>, Req, plain),
         {format_text(Paste, Style), Req2, Paste}.

因為用到了 highlight 工具,所以要安裝套件。

> rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
> yum -y install highlight

測試時,以瀏覽器瀏覽網頁 http://localhost:8080/ ,在 form 裡面填入 html 網頁內容,發送到 server 後,會轉向到一個類似下面這樣的網址
http://localhost:8080/pcL2KGq5 ,如果剛剛貼進去的是 html 的code,可以用 http://localhost:8080/pcL2KGq5?lang=html 將 html code 以 highlight 工具呈現出來。

rest_stream_response

  1. rest_stream_response.app.src
  2. rest_stream_response_sup.erl
  3. rest_stream_response_app.erl

    啟動時,以 ets 產生 1000 筆亂數資料

     Table = ets:new(stream_tab, []),
     generate_rows(Table, 1000),
  4. toppage_handler.erl

     -export([init/3]).
     -export([rest_init/2]).
     -export([content_types_provided/2]).
     -export([streaming_csv/2]).
    
     %% upgrade protocol to cowboy_rest
     init(_Transport, _Req, _Table) ->
         {upgrade, protocol, cowboy_rest}.
    
     %% 處理 request 時,一開始就先呼叫 rest_init/2
     %% 這個 function 一定要回傳 {ok, Req, State}
     %% State 是 handler 所有 callbacks 的狀態物件。
     rest_init(Req, Table) ->
         {ok, Req, Table}.
    
     %% 產生 text/csv 的 response data
     content_types_provided(Req, State) ->
         {[
             {{<<"text">>, <<"csv">>, []}, streaming_csv}
         ], Req, State}.
    
     streaming_csv(Req, Table) ->
         {N, Req1} = cowboy_req:binding(v1, Req, 1),
         MS = [{{'$1', '$2', '$3'}, [{'==', '$2', N}], ['$$']}],
         {{stream, result_streamer(Table, MS)}, Req1, Table}.

測試時,瀏覽網頁 http://localhost:8080/ 會取得前 10筆資料的csv。瀏覽網頁 http://localhost:8080/4 會取得所有第二個欄位為 4 的 csv。

compress_response

  1. compress_response.app.src
  2. compress_response_sup.erl
  3. compress_response_app.erl
    在啟動時,指定 {compress, true}

     {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
         {compress, true},
         {env, [{dispatch, Dispatch}]}
     ]),
  4. toppage_handler.erl

測試是由 request header 決定 client 端有沒有支援 gzip 壓縮。當client 端支援 gzip response data 時,回應的 response header 裡面就會多了 content-encoding: gzip 。

> curl -i http://localhost:8080
> curl -i --compressed http://localhost:8080

eventsource

這是 html5 Server-Side Event 的範例。

  1. eventsource.app.src
  2. eventsource_sup.erl
  3. eventsource_app.erl
    compile routing 時,指定 /eventsource 由 eventsource_handler 處理。
     Dispatch = cowboy_router:compile([
             {'_', [
                 {"/eventsource", eventsource_handler, []},
                 {"/", cowboy_static, {priv_file, eventsource, "index.html"}}
             ]}
         ]),
  4. eventsource_handler.erl

     init(_Transport, Req, []) ->
         Headers = [{<<"content-type">>, <<"text/event-stream">>}],
         %% 將 reponse 分成任意長度的 chunked data
         {ok, Req2} = cowboy_req:chunked_reply(200, Headers, Req),
         %% 1s 後,對自己這個 process 發送 Tick message
         erlang:send_after(1000, self(), {message, "Tick"}),
         {loop, Req2, undefined, 5000}.
    
     %% 收到訊息時,就產生 chunk data
     info({message, Msg}, Req, State) ->
         ok = cowboy_req:chunk(["id: ", id(), "\ndata: ", Msg, "\n\n"], Req),
         erlang:send_after(1000, self(), {message, "Tick"}),
         {loop, Req, State}.
    
     terminate(_Reason, _Req, _State) ->
         ok.
    
     %% Id 為 erlang:now()
     id() ->
         {Mega, Sec, Micro} = erlang:now(),
         Id = (Mega * 1000000 + Sec) * 1000000 + Micro,
         integer_to_list(Id, 16).

測試時直接瀏覽網頁 http://localhost:8080/ ,就可以看到 server 定時回傳的資料。

markdown_middleware

server 的靜態網頁可以用 markdown 語法處理,cowboy 會先將 *.md 的檔案轉換為 html,再回傳給 client。

  1. markdown_middleware.app.src
  2. markdown_middleware_sup.erl
  3. markdown_middleware_app.erl
    增加 markdown_converter 這個 middleware 在 routing 與 handler 的中間。

     Dispatch = cowboy_router:compile([
             {'_', [
                 {"/[...]", cowboy_static, {priv_dir, markdown_middleware, ""}}
             ]}
         ]),
         {ok, _} = cowboy:start_http(http, 100, [{port, 8000}], [
             {env, [{dispatch, Dispatch}]},
             {middlewares, [cowboy_router, markdown_converter, cowboy_handler]}
         ]),
  4. markdown_converter.erl
    middleware 只需要實作一個 callback function: execute/2,這是定義在 cowboy_middleware behavior 裡面。

     -behaviour(cowboy_middleware).
    
     -export([execute/2]).
    
     execute(Req, Env) ->
         {[Path], Req1} = cowboy_req:path_info(Req),
         %% 當 request 路徑的副檔名是 .html 的時候,就轉為呼叫 maybe_generate_markdown
         case filename:extension(Path) of
             <<".html">> -> maybe_generate_markdown(resource_path(Path));
             _Ext -> ok
         end,
         {ok, Req1, Env}.
    
     %% 會判斷 video.html 與 video.md 的最後改時間
     %% video.md 如果沒有更新,就不需要重新產生一次 video.html
     maybe_generate_markdown(Path) ->
         ModifiedAt = filelib:last_modified(source_path(Path)),
         GeneratedAt = filelib:last_modified(Path),
         case ModifiedAt > GeneratedAt of
             true -> erlmarkdown:conv_file(source_path(Path), Path);
             false -> ok
         end.
  5. erlmarkdown.erl
    這是用來處理 .md -> .html 的程式

測試時,瀏覽網頁 http://localhost:8080/video.html

2014/06/23

erlang - cowboy - examples

cowboy source code 裡面有一個 examples 目錄,列出多個範例程式,接下來我們藉由閱讀程式碼的方式,了解如何使用 cowboy。

hello_world

主要寫了四支程式

  1. hello_erlang.app.src
    application 設定

  2. hello_erlang_sup.erl
    application 的監督者 supervisor

  3. hello_erlang_app.erl
    application 的 callback module,必須有 start/2 跟 stop/1 function,重點是在 start/2 裡面要 compile Routing 資訊,並啟動 http protocol。

     start(_Type, _Args) ->
         Dispatch = cowboy_router:compile([
             {'_', [
                 {"/", toppage_handler, []}
             ]}
         ]),
         {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
             {env, [{dispatch, Dispatch}]}
         ]),
         hello_world_sup:start_link().
  4. hello_handler.erl
    Cowboy 最基本的 HTTP handler,需要實作 init/3, handle/2 and terminate/3 三個 callback functions,細節可參閱 cowboy_http_handler 文件。

    重點是 handle 裡面直接產生 200 的 response,並回傳 Hello world! 的 text/plain 資料。

     handle(Req, State) ->
         {ok, Req2} = cowboy_req:reply(200, [
             {<<"content-type">>, <<"text/plain">>}
         ], <<"Hello world!">>, Req),
         {ok, Req2, State}.

ssl hello world

  1. ssl_hello_world.app.src
    application 設定

  2. ssl_hello_world_sup_erl
    supervisor

  3. ssl_hello_world_app.erl
    重點是在 start/2 裡面要 compile Routing 資訊,並啟動 https protocol,因為ssl的關係,必須指定keystore檔案位置,這些檔案都放在專案的 priv/ssl 目錄下面。

     Dispatch = cowboy_router:compile([
             {'_', [
                 {"/", toppage_handler, []}
             ]}
         ]),
         PrivDir = code:priv_dir(ssl_hello_world),
         {ok, _} = cowboy:start_https(https, 100, [
             {port, 8443},
             {cacertfile, PrivDir ++ "/ssl/cowboy-ca.crt"},
             {certfile, PrivDir ++ "/ssl/server.crt"},
             {keyfile, PrivDir ++ "/ssl/server.key"}
         ], [{env, [{dispatch, Dispatch}]}]),
         ssl_hello_world_sup:start_link().
  4. toppage_handler.erl
    跟 hello world 的 toppage_handler.erl 完全一樣。

測試時必須要連結 https://localhost:8443/ 這個網址。

chunked_hello_world

chunked data transfer with two one-second delays

  1. chunked_hello_world.app.src
    application 設定

  2. chunked_hello_world_sup.erl
    supervisor

  3. chunked_hello_world_app.erl
    跟 hello world 的 hello_erlang_app.erl 完全一樣。

  4. toppage_handler.erl
    handle 裡面直接產生 200 的 chunked_reply,並分階段回傳 Hello World Chunked! 的 text/plain 資料。

     handle(Req, State) ->
         {ok, Req2} = cowboy_req:chunked_reply(200, Req),
         ok = cowboy_req:chunk("Hello\r\n", Req2),
         ok = timer:sleep(1000),
         ok = cowboy_req:chunk("World\r\n", Req2),
         ok = timer:sleep(1000),
         ok = cowboy_req:chunk("Chunked!\r\n", Req2),
         {ok, Req2, State}.

rest_hello_world

根據 http request header 中可接受的 response data mine type 來決定回傳的 response 資料內容。

  1. rest_hello_world.app.src
    application 設定

  2. rest_hello_world_sup.erl
    supervisor

  3. rest_hello_world_app.erl
    application 的 callback module,跟 hello world 的 hello_erlang_app.erl 完全一樣。

  4. toppage_handler.erl
    init/3 要 upgrade protocol 為 cowboy_rest,增加實作content_types_provided/2,此 function 回傳的資料中,包含了支援的 mime type 與 callback function list。

    這些 hello_to_html, hello_to_json, hello_to_text 這些 callback function 裡面提供了不同 mine type 資料的 response Body。

     init(_Transport, _Req, []) ->
         {upgrade, protocol, cowboy_rest}.
    
     content_types_provided(Req, State) ->
         {[
             {<<"text/html">>, hello_to_html},
             {<<"application/json">>, hello_to_json},
             {<<"text/plain">>, hello_to_text}
         ], Req, State}.
    
     hello_to_html(Req, State) ->
         ...
    
     hello_to_json(Req, State) ->
         Body = <<"{\"rest\": \"Hello World!\"}">>,
         {Body, Req, State}.
     hello_to_text(Req, State) ->
         {<<"REST Hello World as text!">>, Req, State}.

測試時,要區分不同的 mine type Request

  1. html
     curl -i http://localhost:8080
  2. json
     curl -i -H "Accept: application/json" http://localhost:8080
  3. text
     curl -i -H "Accept: text/plain" http://localhost:8080

static world

static file handler

  1. staitc_world.app.src
    application 設定

  2. static_world_sup.erl
    supervisor

  3. static_world_app.erl
    編譯 routing 時,路徑為 "/[...]" 代表符合所有以 / 開頭的網址,handler 為 cowboy_static,後面指定 priv 目錄,並設定支援所有 mime types。

     start(_Type, _Args) ->
         Dispatch = cowboy_router:compile([
             {'_', [
                 {"/[...]", cowboy_static, {priv_dir, static_world, "",
                     [{mimetypes, cow_mimetypes, all}]}}
             ]}
         ]),
         {ok, _} = cowboy:start_http(http, 100, [{port, 8000}], [
             {env, [{dispatch, Dispatch}]}
         ]),
         static_world_sup:start_link().

測試網址為 http://koko.maxkit.com.tw:8000/video.html,此範例頁面是直接用 html5 的 video tag 指向 server 的影片檔位址。

web_server

serves files with lists directory entries

  1. web_server.app.src
    application 設定

  2. web_server_sup.erl
    supervisor

  3. web_server_app.erl
    跟上一個 static world 類似,但在 compile routing 時,增加一個 dir_handler,另外在start_http 中,增加一個 directory_lister middleware。

     start(_Type, _Args) ->
         Dispatch = cowboy_router:compile([
             {'_', [
                 {"/[...]", cowboy_static, {priv_dir, web_server, "", [
                     {mimetypes, cow_mimetypes, all},
                     {dir_handler, directory_handler}
                 ]}}
             ]}
         ]),
         {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
             {env, [{dispatch, Dispatch}]},
             {middlewares, [cowboy_router, directory_lister, cowboy_handler]}
         ]),
         web_server_sup:start_link().
  4. directory_handler.erl
    這是使用 REST handlers,cowboy_rest 裡面支援多個 resource callback functions,

     init(_Transport, _Req, _Paths) ->
         {upgrade, protocol, cowboy_rest}.
    
      %% 處理 request 時,一開始就先呼叫 rest_init/2
     %% 這個 function 一定要回傳 {ok, Req, State}
     %% State 是 handler 所有 callbacks 的狀態物件。
     rest_init(Req, Paths) ->
         {ok, Req, Paths}.
    
     %% 支援的 HTTP methods
     %% 預設值為 [<<"GET">>, <<"HEAD">>, <<"OPTIONS">>]
     allowed_methods(Req, State) ->
         {[<<"GET">>], Req, State}.
    
     %% 是否存在這個檔案路徑的 resource
     resource_exists(Req, {ReqPath, FilePath}) ->
         case file:list_dir(FilePath) of
             {ok, Fs} -> {true, Req, {ReqPath, lists:sort(Fs)}};
             _Err -> {false, Req, {ReqPath, FilePath}}
         end.
    
     %% 支援什麼 response mime type
     %% 這裡設定支援 application/json 與 text/html 兩種
     content_types_provided(Req, State) ->
         {[
             {{<<"application">>, <<"json">>, []}, list_json},
             {{<<"text">>, <<"html">>, []}, list_html}
         ], Req, State}.
    
     list_json(Req, {Path, Fs}) ->
         Files = [[ <<(list_to_binary(F))/binary>> || F <- Fs ]],
         {jsx:encode(Files), Req, Path}.
    
     list_html(Req, {Path, Fs}) ->
         Body = [[ links(Path, F) || F <- [".."|Fs] ]],
         HTML = [<<"<!DOCTYPE html><html><head><title>Index</title></head>",
             "<body>">>, Body, <<"</body></html>\n">>],
         {HTML, Req, Path}.
    
     links(<<>>, File) ->
         ["<a href='/", File, "'>", File, "</a><br>\n"];
     links(Prefix, File) ->
         ["<a href='/", Prefix, $/, File, "'>", File, "</a><br>\n"].
  5. directory_lister.erl
    支援在瀏覽 http://localhost:8080/ 網址時,把網站的檔案以網頁方式呈現出來,而不是直接顯示 404 Not Found。
    這是middleware,主要就是要實作 execute/2 callback function。

     -module(directory_lister).
     -behaviour(cowboy_middleware).
    
     -export([execute/2]).
    
     execute(Req, Env) ->
         case lists:keyfind(handler, 1, Env) of
             {handler, cowboy_static} -> redirect_directory(Req, Env);
             _H -> {ok, Req, Env}
         end.

測試時,就直接瀏覽網頁 http://localhost:8080/。
另外,這個程式實際上還有些問題,例如:

  1. http://localhost:8080// 當網址後面多了一個 / 的時候,網頁上會列印出機器根目錄的目錄及檔案。

  2. 在 server 的 priv 目錄增加一個目錄 test,但是從瀏覽器瀏覽網頁 http://localhost:8080/test/video.html 卻會出現 500 Error response。瀏覽網頁 http://localhost:8080/test/ 雖然可以看到檔案列表,但 URL 卻都是錯誤的,前面有兩個 //。

echo_get

parse and echo a GET query string

  1. echo_get.app.src
    application 設定

  2. echo_get_sup.erl
    supervisor

  3. echo_get_app.erl
    跟 hello world 的 hello_erlang_app.erl 完全一樣。

  4. toppage_handler.erl
    在 handle 中,取出 GET Method 以及 echo 參數。

     handle(Req, State) ->
         {Method, Req2} = cowboy_req:method(Req),
         {Echo, Req3} = cowboy_req:qs_val(<<"echo">>, Req2),
         {ok, Req4} = echo(Method, Echo, Req3),
         {ok, Req4, State}.
     echo(<<"GET">>, undefined, Req) ->
         cowboy_req:reply(400, [], <<"Missing echo parameter.">>, Req);
     echo(<<"GET">>, Echo, Req) ->
         cowboy_req:reply(200, [
             {<<"content-type">>, <<"text/plain; charset=utf-8">>}
         ], Echo, Req);
     echo(_, _, Req) ->
         %% Method not allowed.
         cowboy_req:reply(405, Req).

測試時,要在網址上增加 echo 參數 http://localhost:8080/?echo=hello,如果測試時沒有 echo 參數,就會得到 400 Error response。

echo_post

  1. echo_post.app.src
  2. echo_post_sup.erl
  3. echo_post_app.erl
  4. toppage_handler.erl
    處理時,先判斷有沒有 POST body,然後在確認有沒有 echo 參數。

     handle(Req, State) ->
         {Method, Req2} = cowboy_req:method(Req),
         HasBody = cowboy_req:has_body(Req2),
         {ok, Req3} = maybe_echo(Method, HasBody, Req2),
         {ok, Req3, State}.
    
     maybe_echo(<<"POST">>, true, Req) ->
         {ok, PostVals, Req2} = cowboy_req:body_qs(Req),
         Echo = proplists:get_value(<<"echo">>, PostVals),
         echo(Echo, Req2);
     maybe_echo(<<"POST">>, false, Req) ->
         cowboy_req:reply(400, [], <<"Missing body.">>, Req);
     maybe_echo(_, _, Req) ->
         %% Method not allowed.
         cowboy_req:reply(405, Req).
    
     echo(undefined, Req) ->
         cowboy_req:reply(400, [], <<"Missing echo parameter.">>, Req);
     echo(Echo, Req) ->
         cowboy_req:reply(200, [
             {<<"content-type">>, <<"text/plain; charset=utf-8">>}
         ], Echo, Req).

用以下這樣的方式測試,第二個測試會得到 400 Error response。

curl -i -d echo=test http://localhost:8080
curl -i -d e=test http://localhost:8000

cookie

  1. cookie.app.src
  2. cookie_sup.erl
  3. cookie_app.erl
  4. toppage_handler.erl
    以 cowboy_req:set_resp_cookie 設定 cookie
     handle(Req, State) ->
         NewValue = integer_to_list(random:uniform(1000000)),
         Req2 = cowboy_req:set_resp_cookie(
             <<"server">>, NewValue, [{path, <<"/">>}], Req),
         {ClientCookie, Req3} = cowboy_req:cookie(<<"client">>, Req2),
         {ServerCookie, Req4} = cowboy_req:cookie(<<"server">>, Req3),
         {ok, Body} = toppage_dtl:render([
             {client, ClientCookie},
             {server, ServerCookie}
         ]),
         {ok, Req5} = cowboy_req:reply(200,
             [{<<"content-type">>, <<"text/html">>}],
             Body, Req4),
         {ok, Req5, State}.

erlydtl 編譯一直出現問題,所以我們就先修改 toppage_handler.erl,去掉 erlydtl 的相依性設定。

handle(Req, State) ->
    NewValue = integer_to_list(random:uniform(1000000)),
    Req2 = cowboy_req:set_resp_cookie(
        <<"server">>, NewValue, [{path, <<"/">>}], Req),
    {ClientCookie, Req3} = cowboy_req:cookie(<<"client">>, Req2, <<"default">>),
    {ServerCookie, Req4} = cowboy_req:cookie(<<"server">>, Req3, <<"default">>),
    Body=list_to_binary([<<"<html><body>client cookie=">>, ClientCookie, <<"<br/>server cookie=">>, ServerCookie, <<"</body></html>">>]),
    {ok, Req5} = cowboy_req:reply(200,
        [{<<"content-type">>, <<"text/html">>}],
        Body, Req4),
    {ok, Req5, State}.

重新編譯測試後,第一次瀏覽網頁 http://localhost:8080/ 會看到

client cookie=default
server cookie=default

再瀏覽一次 http://localhost:8080/ 會看到

client cookie=default
server cookie=443585

websocket

  1. websocket.app.src
  2. websocket_sup.erl
  3. websocket_app.erl
    依照網址規則順序,/ 為 index.html 靜態首頁,/websocket 以 module ws_handler 處理,/static/[...] 對應到 priv/static 目錄裡面的靜態頁面。

     Dispatch = cowboy_router:compile([
             {'_', [
                 {"/", cowboy_static, {priv_file, websocket, "index.html"}},
                 {"/websocket", ws_handler, []},
                 {"/static/[...]", cowboy_static, {priv_dir, websocket, "static"}}
             ]}
         ]),
  4. ws_handler.erl
    必須指定 -behaviour(cowboy_websocket_handler)

    將此連線改為 cowboy_websocket protocol

     init({tcp, http}, _Req, _Opts) ->
         {upgrade, protocol, cowboy_websocket}.

    實作 websocket_init, websocket_handle, websocket_info, websocket_terminate 四個 protocol

     websocket_init(_TransportName, Req, _Opts) ->
         %% 1s 後發送回應
         erlang:start_timer(1000, self(), <<"Hello!">>),
         {ok, Req, undefined_state}.
    
     %% 以 websocket_handle 接收 client 發送的資料
     websocket_handle({text, Msg}, Req, State) ->
         {reply, {text, << "That's what she said! ", Msg/binary >>}, Req, State};
     websocket_handle(_Data, Req, State) ->
         {ok, Req, State}.
    
     %% 以 websocket_info 發送系統訊息
     websocket_info({timeout, _Ref, Msg}, Req, State) ->
         erlang:start_timer(1000, self(), <<"How' you doin'?">>),
         {reply, {text, Msg}, Req, State};
     websocket_info(_Info, Req, State) ->
         {ok, Req, State}.
    
     websocket_terminate(_Reason, _Req, _State) ->
         ok.

測試時,不能使用IE,改使用Chrome,瀏覽網頁 http://localhost:8080/ ,網頁中以 WebSocket 連接 ws://localhost:8080/websocket 網址。

error hook

  1. error_hook.app.src
  2. error_hook_sup.erl
  3. error_hook_app.erl
    在啟動時,利用 onresponse 的 callback 機制,針對不同的 error code,提供不同的錯誤畫面,一般的 response code,就不處理,直接回應。

     start(_Type, _Args) ->
         Dispatch = cowboy_router:compile([
             {'_', []}
         ]),
         {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
             {env, [{dispatch, Dispatch}]},
             {onresponse, fun error_hook/4}
         ]),
         error_hook_sup:start_link().
     error_hook(404, Headers, <<>>, Req) ->
         {Path, Req2} = cowboy_req:path(Req),
         Body = ["404 Not Found: \"", Path,
             "\" is not the path you are looking for.\n"],
         Headers2 = lists:keyreplace(<<"content-length">>, 1,     Headers,
             {<<"content-length">>, integer_to_list(iolist_size(Body))}),
         {ok, Req3} = cowboy_req:reply(404, Headers2, Body, Req2),
         Req3;
     error_hook(Code, Headers, <<>>, Req) when is_integer(Code), Code >= 400 ->
         ......
         {ok, Req2} = cowboy_req:reply(Code, Headers2, Body, Req),
         Req2;
     error_hook(_Code, _Headers, _Body, Req) ->
         Req.

測試

> curl -i http://localhost:8080/
HTTP/1.1 404 Not Found
connection: keep-alive
server: Cowboy
date: Wed, 23 Apr 2014 03:17:46 GMT
content-length: 56

404 Not Found: "/" is not the path you are looking for.

2014/06/16

erlang - cowboy (2)

cowboy 的第二篇文章,內容談到 cookie 的使用、靜態網頁、REST、Server Push、Websocket、Hooks、 Middleware。

Using cookies

Setting cookies

%% 預設狀況下,cookie 是定義給 session 使用
SessionID = generate_session_id(),
Req2 = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, [], Req).

%% 可設定 expiration time in seconds
SessionID = generate_session_id(),
Req2 = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, [
    {max_age, 3600}
], Req).

%% 刪除 cookie
Req2 = cowboy_req:set_resp_cookie(<<"sessionid">>, <<>>, [
    {max_age, 0}
], Req).

%% 設定 cookie 時,指定 domain 與 path
Req2 = cowboy_req:set_resp_cookie(<<"inaccount">>, <<"1">>, [
    {domain, "my.example.org"},
    {path, "/account"}
], Req).

%% 限制 cookie 只用在 https
SessionID = generate_session_id(),
Req2 = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, [
    {secure, true}
], Req).

%% 限制 cookie 只用在 client-server 通訊上,這種 cookie 無法使用 client-side script 做任何處理
SessionID = generate_session_id(),
Req2 = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, [
    {http_only, true}
], Req).

Reading cookies

%% 讀取 cookie: lang 的 value
{CookieVal, Req2} = cowboy_req:cookie(<<"lang">>, Req).

%% 讀取 cookie: lang 的 value,不存在時,就回傳預設值 fr
{CookieVal, Req2} = cowboy_req:cookie(<<"lang">>, Req, <<"fr">>).

%% 取得 cookie 的 key/value tuple list
{AllCookies, Req2} = cowboy_req:cookies(Req).

Static files

static file handler 是用一個 built-in REST handler處理的,這可以服務一個檔案或是一個目錄的所有檔案,這些檔案可以用多個 Content distribution Network (CDN) 處理。

Serve one file

%% 處理路徑 / 時,以 應用程式 my_app 的私有目錄服務檔案 static/index.html
{"/", cowboy_static, {priv_file, my_app, "static/index.html"}}

%% 處理路徑 / 時,以檔案絕對路徑 /var/www/index.html 提供服務
{"/", cowboy_static, {file, "/var/www/index.html"}}

Serve all files from a directory

%% 服務 my_app 裡面的 static/assets 目錄裡面的所有檔案,可處理所有 /assets/ 開頭的網址
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets"}}

%% 指定目錄的絕對路徑
{"/assets/[...]", cowboy_static, {dir, "/var/www/assets"}}

Customize the mimetype detection

cowboy 預設會利用 file extension 來辨識檔案的 mimetype,可以覆寫這個 callback function。cowboy 內建兩個 functions,預設的只會處理 web application 用到的 file types,另一個則提供上百個 mimetypes。

%% 使用預設的 function
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{mimetypes, cow_mimetypes, web}]}}

%% 使用所有檔案的 mimetypes
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{mimetypes, cow_mimetypes, all}]}}

%% 改用自訂客製的 callback function
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{mimetypes, Module, Function}]}}

如果 Module:Function 遇到無法識別的檔案 mimetype,就會回傳 {<<"application">>, <<"octet-stream">>, []} ,這就代表是 application/octet-stream。

Generate an etag

預設狀況下,static handler 會根據檔案的 size 與 modified time 產生一個 etag header value,

etag 是用來判斷檔案版本資訊的方法,如果 client 的檔案 etag 跟 server 一樣,server 可直接回應 304,告訴 client 直接使用 cache 裡面的檔案。實際上除了 etag 之外,還要同時觀察 Last-Modified 與 Expires,可參閱這篇文章

%% 改變 etag 的計算方式
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{etag, Module, Function}]}}

%% disabled etag handling
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{etag, false}]}}

REST handlers

跟 Websocket 一樣,REST 是 HTTP 的 sub-protocol,所以需要 protocol upgrade。

init({tcp, http}, Req, Opts) ->
    {upgrade, protocol, cowboy_rest}.

目前 REST handler 可處理以下這些 HTTP methods: HEAD, GET, POST, PATCH, PUT, DELETE, OPTIONS。Diagram for REST 最後面提供了四張 REST 處理的 svg 流程圖,可以先下載後,再拖拉到瀏覽器中觀看,這四個流程圖分別說明了以下的流程。

  1. Beginning part, up to resource_exists
  2. From resource_exists, for HEAD and GET requests
  3. From resource_exists, for POST/PATCH/PUT requests
  4. From resource_exists, for DELETE requests

Callbacks

處理 request 時,一開始就先呼叫 rest_init/2,這個 function 一定要回傳 {ok, Req, State},State 是 handler 所有 callbacks 的狀態物件。在最後,會呼叫 rest_terminate/2,這個 function 不能發送 reply,且一定要回傳 ok。

所有其他的 callbacks 都是 resource callbacks,需要兩個參數: Req 與 State,而且都會回傳 {Value, Req, State}。如果 callbacks 回傳了 {halt, Req, State},就表示要中止這個 request 的處理,接下來直接呼叫 rest_terminate/2。

如果 callback 回傳 skip,就會跳過此步驟,並執行下一步,空白欄位表示沒有預設值。

Callback name Default value
allowed_methods [<<"GET">>, <<"HEAD">>, <<"OPTIONS">>]
allow_missing_post true
charsets_provided skip
content_types_accepted
content_types_provided [{{<<"text">>, <<"html">>, '*'}, to_html}]
delete_completed true
delete_resource false
expires undefined
forbidden false
generate_etag undefined
is_authorized true
is_conflict false
known_content_type true
known_methods [<<"GET">>, <<"HEAD">>, <<"POST">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>, <<"OPTIONS">>]
languages_provided skip
last_modified undefined
malformed_request false
moved_permanently false
moved_temporarily false
multiple_choices false
options ok
previously_existed false
resource_exists true
service_available true
uri_too_long false
valid_content_headers true
valid_entity_length true
variances []

可使用 content_types_accepted/2, content_types_provided/2 產生任意數量的 user-defined callbacks,建議區分成兩個 function,例如 from_html 與 to_html,分別用來代表接受 html 資料與發送 html 資料。

Meta data

cowboy 會在處理過程中設定一些 meta values,可使用 cowboy_req:meta/{2,3} 取得。

Meta key Details
media_type The content-type negotiated for the response entity.
language The language negotiated for the response entity.
charset The charset negotiated for the response entity.

Response headers

cowboy 會在處理 REST 之後自動設定一些 headers。

Header name Details
content-language Language used in the response body
content-type Media type and charset of the response body
etag Etag of the resource
expires Expiration date of the resource
last-modified Last modification date for the resource
location Relative or absolute URI to the requested resource
vary List of headers that may change the representation of the resource

Server Push: using Loop Handlers

當 response 無法馬上回傳時,就可以使用 Loop Handler,它會進入一個 receive loop 等待訊息,並發送 response。這個方式非常適合處理 long-polling。如果 response 是 partially available,且我們需要 stream the response body,也可使用 Loop Handler,這種方式適合處理 server-sent events。

sample

-module(my_loop_handler).
-behaviour(cowboy_loop_handler).

-export([init/3]).
-export([info/3]).
-export([terminate/3]).

init({tcp, http}, Req, Opts) ->
    %% 如果沒有在 60s 內收到 {reply, Body},就會產生 204 No Content 的 response
    {loop, Req, undefined_state, 60000, hibernate}.

%% 等待 {reply, Body},然後才發送 response
info({reply, Body}, Req, State) ->
    {ok, Req2} = cowboy_req:reply(200, [], Body, Req),
    {ok, Req2, State};
info(Message, Req, State) ->
    {loop, Req, State, hibernate}.

terminate(Reason, Req, State) ->
    ok.

Websocket handlers

Websocket 是 HTTP extension,可在 browser 中模擬 plain TCP connection,cowboy 是用 Websocket Handler 處理,client 與 server 兩端都可以在任何時間非同步發送資料。

sample

-module(my_ws_handler).
-behaviour(cowboy_websocket_handler).

-export([init/3]).
-export([websocket_init/3]).
-export([websocket_handle/3]).
-export([websocket_info/3]).
-export([websocket_terminate/3]).

%% 將 cowboy connection 升級到支援 websocket
init({tcp, http}, Req, Opts) ->
    {upgrade, protocol, cowboy_websocket}.

websocket_init(TransportName, Req, _Opts) ->
    erlang:start_timer(1000, self(), <<"Hello!">>),
    {ok, Req, undefined_state}.

%% 以 websocket_handle 接收 client 發送的資料
websocket_handle({text, Msg}, Req, State) ->
    {reply, {text, << "That's what she said! ", Msg/binary >>}, Req, State};
websocket_handle(_Data, Req, State) ->
    {ok, Req, State}.

%% 以 websocket_info 發送系統訊息
websocket_info({timeout, _Ref, Msg}, Req, State) ->
    erlang:start_timer(1000, self(), <<"How' you doin'?">>),
    {reply, {text, Msg}, Req, State};
websocket_info(_Info, Req, State) ->
    {ok, Req, State}.

%% 當連線中斷時,就會呼叫 websocket_terminate
websocket_terminate(_Reason, _Req, _State) ->
    ok.

Hooks

onrequest

當 cowboy 取得 request headers之後,就會呼叫 onrequest hook,這會在所有request相關處理(包含routing)之前發生,我們可用來在繼續處理 request 之前,修改 request object 裡面的資料,如果在 onrequest 裡面就發送了 reply,cowboy就會中止處理這個 request,繼續處理下一個 request。如果 onrequest crash,就不會發送任何 reply 了。

%% 在產生 listener 時,就指定 onrequest 的callback function
cowboy:start_http(my_http_listener, 100,
    [{port, 8080}],
    [
        {env, [{dispatch, Dispatch}]},
        {onrequest, fun ?MODULE:debug_hook/1}
    ]
).

%% 這個 hook 會列印每一個 request object,適合用在 debugging
debug_hook(Req) ->
    erlang:display(Req),
    Req.

onresponse

在 cowboy 發送 response 之前,會呼叫 onresponse hook,通常用來 logging responses 或是修改 response header/body,常見的範例是提供 custom error pages。跟onrequest一樣,如果 onresponse crash,就不會發送 reply 了。

%% 在產生 listener 時,就指定 onresponse 的callback function
cowboy:start_http(my_http_listener, 100,
    [{port, 8080}],
    [
        {env, [{dispatch, Dispatch}]},
        {onresponse, fun ?MODULE:custom_404_hook/4}
    ]
).

%% 提供自訂的 404 error page
custom_404_hook(404, Headers, <<>>, Req) ->
    Body = <<"404 Not Found.">>,
    %% 修改 response header: content-length
    Headers2 = lists:keyreplace(<<"content-length">>, 1, Headers,
        {<<"content-length">>, integer_to_list(byte_size(Body))}),
    {ok, Req2} = cowboy_req:reply(404, Headers2, Body, Req),
    Req2;
custom_404_hook(_, _, _, Req) ->
    Req.

Middlewares

cowboy 將 request processing 交給 middleware components 處理,預設提供了 routing 與 handler 兩個 middlewares。cowboy 會根據 middleware 設定的順序執行。

Usage

middleware 只需要實作一個 callback function: execute/2,這是定義在 cowboy_middleware behavior 裡面。

execute(Req, Env) 可能會回傳四種 values

  1. {ok, Req, Env} : 會繼續執行下一個 middleware
  2. {suspend, Module, Function, Args} : to hibernate,繼續執行下一個 MFA
  3. {halt, Req} : 停止處理這個 request,繼續下一個 request
  4. {error, StatusCode, Req} : 回應 error 並 close the socket

Configuration

Env 裡面保留了兩個值

  1. listener
    包含 name of the listener
  2. result
    包含 result of the processing,如果結果不是 ok ,cowboy 就不會處理這個 connection 後面的所有 requests。

可使用 cowboy:set_env/3 設定或取代 Env 裡面的資料。

Routing middleware

需要 dispatch value,如果 routing compilation 成功,就會把 handler name and options 放在 Env 裡面的 handler 與 handler_opts。

Handler middleware

需要 handler 與 handler_opts values,會把結果放在 Env  的 result 裡面。

high concurency

如果要讓 cowboy 能處理多個連線,必須調整參數。

在 cowboy:start_http 時,要加上 {max_connections, infinity}

cowboy:start_http(my_http_listener, 100,
            [{port, 8000}, {max_connections, infinity}],
            [{env, [{dispatch, Dispatch}]}]
        ),

另外 erlang vm 本身預設有可以建立的 process 數量的上限。預設值為 262144 個。

1> erlang:system_info(process_limit).
262144

這個數量是不夠的,我們必須在啟動 vm 時,設定 +P Number 參數,Number 的數量為 [1024-134217727],實際上測試時,如果把數量設定為最大值 134217727,反而會覺得 vm 啟動的速度變慢了,所以把 process 上限調為 1000萬,這樣子應該夠用了,實際上得到的數量也是接近 10240000,而不是絕對值。

erl +P 10240000

1> erlang:system_info(process_limit).
16777216

Reference

cowboy user guide
100萬並發連接服務器筆記之Erlang完成1M並發連接目標