2014年6月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年6月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年6月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並發連接目標

2014年6月12日

使用wxPython開發跨平台視窗程式


wxPythonPython的GUI toolkit,顧名思義,其包裝了知名的C++ GUI toolkit - wxWidget

Python語言擁有簡潔的語法以及豐富的package可以使用,是快速開發跨平台視窗程式的好選擇之一。

安裝Python

Python官方網站下載Python直譯器的安裝檔。 一點進去官方網站就可以看到抖大的下載按鈕。但預設是32bit的版本。
我建議可以到https://www.python.org/download/這頁下載,可以自行選擇32bit或64bit的版本。
安裝好Python直譯器後,可以在console下測試python指令。
若無法執行python指令,記得檢查Python直譯器的可執行檔是否加入環境變數中。

安裝wxPython

wxPython官方網站下載wxPython。
這邊要注意的是,Python直譯器與wxPython必須要同樣是32bit或64bit的版本。
在Windows環境以及Mac OS環境下都有Binaries可以直接執行安裝程式。
若需要自行由Source Code建立wxPython,可以參考:http://www.wxpython.org/BUILD.html

安裝完畢後,進入console下執行python指令, 並輸入:import wx
若沒有出現錯誤,表示wxPython安裝成功。
或是執行以下範例:
#<path_to_python>
# -*- coding:utf-8 -*-
import wx

app = wx.App(False)  # Create a new app, don't redirect stdout/stderr to a window.
frame = wx.Frame(None, wx.ID_ANY, "Hello World") # A Frame is a top-level window.
frame.Show(True)     # Show the frame.
app.MainLoop()

安裝wxPython demo

wxPython有相當多的範例demo程式, 在windows或OS X平台下都可以透過安裝檔安裝demo程式碼。 安裝完畢後,在Python目錄底下可以看到wxPython demo的程式原始碼, 裡面有豐富的範例,大多數也可單一執行; 也可將範例複製到其他有Python直譯器的任何平台執行。

使用pyinstaller建立特定平台可執行檔

pyinstaller可以將python程式轉成特定平台的可執行檔, 執行時更為便利。
而pyinstaller本身也支援Windows, Linux及OS X 等平台。
以下介紹如何使用pyinstaller建立特定平台可執行檔。

在Windows下安裝pyinstaller

安裝PyWin32
Windows環境需要先安裝PyWin32 http://sourceforge.net/projects/pywin32/files/ 可以在SourceForge找到最新的build, 選適合自己平台的安裝檔
使用pip-Win安裝及執行pyinstaller
接著下載pip-Win這個工具。
下載後,直接執行即可。
並輸入:
venv -c -i  pyi-env-name
如下圖:
 會產生一個命令列視窗。




第一次執行時會自動安裝需要的相關套件,待執行完畢後,
在命令列視窗輸入:
pip install pyinstaller
即可安裝好pyinstaller。
爾後,執行pyinstaller時,也需要在此命令列視窗中執行。

在Linux下安裝pyinstaller

安裝pip
首先至pip網站下載get-pip.py
並執行:
python get-pip.py
即可安裝好pip,
接著再執行:
pip install pyinstaller
即可安裝好pyinstaller。

使用pyinstaller

使用pyinstaller建立spec檔案

(提醒:在Windows環境下,以下步驟要在pip-Win產生的命令列視窗中執行)
在產生可執行檔之前,
pyinstaller會先分析python程式碼,並產生spec檔案。
假設有個python程式名為testWx.py,
則執行:
pyinstaller -w -F testWx.py
即會產生一個testWx.spec的檔案。
-w參數表示此python程式是視窗程式,產生的可執行檔在執行時,不要跑出命令列視窗;
-F參數表示產生單一的可執行檔。

使用pyinstaller建立可執行檔

產生了spec檔案之後,
只要執行:
pyinstaller -w -F testWx.spec
即可產生該平台可執行檔。

2014年6月11日

JAVA LDAP分頁查詢處理

這幾天遇到一個問題,是關於LDAP匯入的部分,客戶說有些人員沒有被匯入進來,查了一下log沒看到拋出任何Exception,透過LDAP Client軟體去下查詢,也查的到該位沒被匯入的人員,資料驗證的部分也都正確無誤,一時之間不知道問題在哪,找了幾個小時之後在log內發現到一個很奇怪的現象,為什麼匯入的人員數量會那麼剛好的為1000人,憑著這幾年寫程式的直覺來猜測,這裡面一定有什麼問題。

原本的寫法

原本的寫法應該是參考JAVA官網 Advanced Topics for LDAP - Creation 上的寫法,如下:

// set properties for our connection and provider
Properties properties = new Properties();
properties.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.ldap.LdapCtxFactory");
properties.put(Context.PROVIDER_URL, "ldap://192.168.150.129:389");
properties.put(Context.REFERRAL, "ignore");
properties.put(Context.SECURITY_PRINCIPAL,
        "cn=Manager,dc=maxkit,dc=com,dc=tw");
properties.put(Context.SECURITY_CREDENTIALS, "secret");

InitialDirContext context;
context = new InitialDirContext(properties);

透過JNDI去,來達成對LDAP Server的連線,上面的程式執行起來是正常的,也能夠正確的連上LDAP Server。

人員沒有匯入問題所在

那既然上面的code是正常的,那怎麼會出現人員沒匯入的狀況呢?

客戶用的是微軟的Active Directory,而在客戶透過AD Client軟體查詢給我們看時,我發現到他的軟體一次也是回一千筆資料,而我自己的程式在匯入時,也只處理剛好一千筆的資料,綜合上述幾點看來,有足夠的理由讓我懷疑Active Directory是不是會對查詢進行每次只回傳1千筆資料的分頁處理。

找了一些網站,沒找到官方的正式資料,倒是同事有找到類似的文章,只是文章內是教說如何加大Active Directory每次查詢回來的資料筆數(Increasing the number of objects returned in a single LDAP query),根據網路上查詢到的資料來推斷,就是這原因造成人員沒有成功匯入的!

解決方案

既然知道問題在於沒有做分頁,那就將之分頁,問題不就解決了?

因此上網查了一些JAVA處理LDAP分頁的範例,找到了幾篇好懂的解決方案,如 JNDI, Active Directory, Paging and Range Retrieval,不過也因此發現到,要用分頁的話,原本的寫法是不行的,原因在於原本寫法用的InitialDirContext這類別,沒有提供任何可以使用分頁的方法,因此根據找到的文章,改成使用InitialLdapContext來處理,就能進行分頁處理了,範例如下:

// LDAP連線相關設定
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_PRINCIPAL, "cn=Manager,dc=maxkit,dc=com,dc=tw");
env.put(Context.SECURITY_CREDENTIALS, "secret");
env.put(Context.REFERRAL, "ignore");
env.put(Context.PROVIDER_URL, LDAP_URL);

LdapContext ctx = new InitialLdapContext(env, null);

// 設定分頁相關資訊
int pageSize = 1000; //設定LDAP每次分頁所取的資料筆數
byte[] cookie = null;
ctx.setRequestControls(new Control[]{new PagedResultsControl(
    pageSize, Control.CRITICAL)});

do {
    // 設定 LDAP 人員查詢條件
    String searchFilter = "(objectClass=organizationalPerson)";
    SearchControls searchCtls = new SearchControls();
    searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    // 進行 LDAP 資料查詢與資料處理
    NamingEnumeration<SearchResult> results = ctx.search(
            "dc=maxkit,dc=com,dc=tw", searchFilter, searchCtls);
    while (results != null && results.hasMore()) {
        // ...
        // 這邊執行 LDAP 查出來的人員相關處理邏輯
        // ...
    }       

    //==================================================
    // 換頁處理開始
    //==================================================

    // 此分頁資料處理完畢,底下先取出cookie,
    // 如果cookie不為null,則表示還有下一頁的資料
    Control[] controls = ctx.getResponseControls();
    if (controls != null) {
        for (int i = 0; i < controls.length; i++) {
            if (controls[i] instanceof PagedResultsResponseControl) {
                PagedResultsResponseControl prrc = 
                    (PagedResultsResponseControl) controls[i];
                cookie = prrc.getCookie();
            }
        }
    }

    // 將cookie資訊提供給InitialLdapContext,讓它在接下來的查詢中進行換頁
    ctx.setRequestControls(new Control[]{new PagedResultsControl(
            pageSize, cookie, Control.CRITICAL)});

    //==================================================
    // 換頁處理結束
    //==================================================
} while (cookie != null);

ctx.close();

上面的程式需要注意到的是:

  1. 必須用InitialLdapContext才能進行操作分頁。
  2. 每次分頁取完時,要去取出cookie,並將之設定給InitialLdapContext,以讓他換頁查詢。

根據上面的程式,就能達成LDAP換頁查詢的需求。你可以透過一些變數記錄來觀察此段程式是否有確實的做換頁的動作,這邊為了精簡程式,因此就省略了。

2014年6月9日

erlang - cowboy

Erlang Web Modules

Erlang 的 http 網頁框架最常見的是 yaws 與 Mochiweb,但除了這兩個框架,還有很多 http 函式庫,Erlang的Web庫和框架 簡單比較了 erlang 所有的 web library: yaws, Mochiweb, Misultin, Cowboy, httpd, SimpleBridge, webmachine, Nitrogen, Zotonic, Chicago Boss。

這篇文章 An interview with #erlang cowboy Loïc Hoguin (@lhoguin)
有一段跟 Cowboy 作者 Loïc Hoguin 的訪談,內容討論到 Cowboy 的優勢跟實作的理念。

從一些文章針對 Cowboy 的討論,一致認為 Cowboy 的實作方式,相較於其他的函式庫,有著一些特別的優勢,接下來就說明這些優點。

Cowboy 的優勢

Cowboy 跟其他 erlang web projects 例如 Misultin, Webmachine, Yaws 有兩個最大的差異,使用了 binaries 而不是 lists,另外一個重點是利用 Ranch 實作的 generic acceptor pool。

Cowboy 設計用來支援 99% 的 use case 而不管剩下的 1%,作者會根據新功能需求的內容,判斷是不是特殊需求,並決定要不要實作,因為使用者可以自己取得 source code 實做那些特殊的功能需求。

Cowboy 遵循了 Erlang clean code 的原則,包含了數百個測試程式並完整相容於 Dialyzer,不使用 process dictionaries 與 parametrized modules,作者認為多寫一些程式碼,有助於提高可閱讀性。Cowboy 為每個 connection 使用一個 process而不是兩個,而且用 binaries 取代 list 實作,因此記憶體的消耗量低。

Cowboy 目前支援 HTTP/1.0, HTTP/1.1, SPDY, Websocket (all implemented drafts + standard) 與 Webmachine-based REST。

user guide, manual

研究 Cowboy 最大的問題就是,還沒有很完整的使用教學手冊,就連 User Guide 也還沒有寫完,官方網站 建議大家閱讀 cowboy user guidecowboy function reference 與原始程式碼中的 examples。

User Guide 首先就說明,所有 HTTP 標準的 method name 都是 case sensitive,而且都是 uppercase,另外 HTTP header name 是 case insensistive,所以 Cowboy 設定 header name 為 lowercase。

Cowboy 目前除了還不支援 HTTP/2.0 之外,支援了 HTTP/1.0, HTTP/1.1, REST, XmlHttpRequest, Long-polling, HTML5, EventSource, Websocket, SPDY。

解釋一下不常見的 EventSource,這也可稱為 Server-Sent Events,這可讓 server push 資料到 HTML5 applications,這是由 server 到 client 的單向通訊channel。EventSource只支援 UTF-8 encoded text data,不能發送 binary data,通常我們會使用能提供雙向通訊的 Websocket。

SPDY 是減少網頁載入時間的協定,他會對每一個server都打開一條單一的連線,保持連線並處理所有的 requests,同時會壓縮 HTTP headers以減少 request 的資料量,SPDY相容於 HTTP/1.1。

編譯 Cowboy

編譯 cowboy 需要一些工具及搭配的 libraries: make, erlang.mk, relx, ranch, cowlib

erlang.mk 是 erlang application 的 Makefile
relx 是 Erlang/OTP release 工具
ranch 是 Socket acceptor pool for TCP protocols
cowlib 是處理 Web protocols 的 library

首先下載 Cowboy 0.9 版,解壓縮後,直接 make 會一直出現 error,問題出在 wget https 網站。修改 erlang.mk 裡面的兩個 wget,在後面加上 --no-check-certificate 參數,就可以完成編譯。

define get_pkg_file
    wget --no-check-certificate -O $(PKG_FILE) $(PKG_FILE_URL) || rm $(PKG_FILE)
endef

.....

define get_relx
    wget --no-check-certificate -O $(RELX) $(RELX_URL) || rm $(RELX)
    chmod +x $(RELX)
endef

Hello World

首先製作原始程式、 app 的設定與 Makefile

hello_erlang/
    src/
        hello_erlang.app.src
        hello_erlang_app.erl
        hello_erlang_sup.erl
        hello_handler.erl
    erlang.mk
    Makefile
    relx.config

這是 hello_erlang 的 application 設定

%% hello_erlang.app.src
{application, hello_world, [
    {description, "Cowboy Hello World example."},
    {vsn, "1"},
    % 目前保留為空白,但編譯時,會自動以 src 裡面編譯的所有 modules 的 list 取代
    {modules, []},
    % 通常只需要註冊 application 裡面的 top-level supervisor
    {registered, [hello_world_sup]},
    % 列出執行時所有需要的 applications
    {applications, [
        kernel,
        stdlib,
        cowboy
    ]},
    % 啟動 applcation 的 callback module
    {mod, {hello_world_app, []}},
    {env, []}
]}.

application 的 callback module,必須有 start/2 跟 stop/1 function

%% hello_world_app.erl
%% @private
-module(hello_world_app).
-behaviour(application).

-export([start/2]).
-export([stop/1]).

start(_Type, _Args) ->
    Dispatch = cowboy_router:compile([
        {'_', [
            {"/", toppage_handler, []}
        ]}
    ]),
    {ok, _} = cowboy:start_http(http, 100, [{port, 8000}], [
        {env, [{dispatch, Dispatch}]}
    ]),
    hello_world_sup:start_link().

stop(_State) ->
    ok.

application 的監督者

% hello_world_sup.erl
%% @private
-module(hello_world_sup).
-behaviour(supervisor).

%% API.
-export([start_link/0]).

%% supervisor.
-export([init/1]).

%% API.

-spec start_link() -> {ok, pid()}.
start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

%% supervisor.

init([]) ->
    Procs = [],
    % 重新啟動的策略為 one_for_one, 在 10 秒內重新啟動工作者超過 10 次,此監督者就會終止所有工作行程
    {ok, {{one_for_one, 10, 10}, Procs}}.

這是 Cowboy 最基本的 HTTP handler,需要實作 init/3, handle/2 and terminate/3 三個 callback functions,細節可參閱 cowboy_http_handler 文件。

%% toppage_handler.erl
%% @doc Hello world handler.
-module(toppage_handler).

-export([init/3]).
-export([handle/2]).
-export([terminate/3]).

% init 會初始化 the state for this request
% Req 是 cowboy_req:req()
init(_Type, Req, []) ->
    {ok, Req, undefined}.

% 處理 request 並以 cowboy_req:reply 回傳 HTTP response
% header 與 content 資料內容都必須要使用 binaries
handle(Req, State) ->
    {ok, Req2} = cowboy_req:reply(200, [
        {<<"content-type">>, <<"text/plain">>}
    ], <<"Hello world!">>, Req),
    {ok, Req2, State}.

terminate(_Reason, _Req, _State) ->
    ok.

編譯 hello world 之前,要先取得 erlang.mk

wget --no-check-certificate https://raw.github.com/extend/erlang.mk/master/erlang.mk

然後編輯 Makefile,內容如下

PROJECT = hello_erlang

DEPS = cowboy
dep_cowboy = pkg://cowboy master

include erlang.mk

另外為了產生 application release,我們要編輯一個 relx.config 檔案,內容如下。第一行的內容,代表會產生一個名稱為 hello_world_example 且版本號碼為 1 的執行 script,application 模組名稱為 hello_world

{release, {hello_world_example, "1"}, [hello_world]}.
{extended_start_script, true}.

接下來執行 make 就可以編譯 hello_world,erlang.mk 將會自動下載 relx,並建立 release package。

make
./_rel/bin/hello_world_example console

接下來就可以在 browser 上,以 http://localhost:8000/ 看到 hello world 網頁,也可以用 curl 測試

[root@koko cowboy-0.9.0]# curl -i http://localhost:8000/
HTTP/1.1 200 OK
connection: keep-alive
server: Cowboy
date: Wed, 09 Apr 2014 08:58:03 GMT
content-length: 12
content-type: text/plain

Hello world!

HTTP

當 http client 發送 request 到 server 直到產生 response 為止,必須要經過一些步驟:讀取 request -> 取得 resource -> 準備 response 資料 -> 同時可寫入 log,整個過程可由下圖表示。深綠色的 onrequest, handler, onresponse 都是可以介入,加入我們自己的程式碼的地方。

request 的處理會因為 protocol 的不同而有差異,HTTP/1.0 每一次連線都只能處理一個 request,因此 Cowboy 會在發送 repsonse 之後立刻關閉連線。HTTP/1.1 允許保持連線狀態 (keep-alive),SPDY 允許在一個連線中,非同步地同時發送多個 requests。

HTTP/1.1 keep-alive

server 可用以下程式碼,發送 connection:close 的 header,並強制將連線關閉。

{ok, Req2} = cowboy_req:reply(200, [
    {<<"connection">>, <<"close">>},
], <<"Closing the socket in 3.. 2.. 1..">>, Req).

Cowboy 預設只會在每一個連線中,接受最多 100 個 requests,這個數量可以透過 max_keepalive 的設定來修改。Cowboy 是利用 reuse 所有 request 的 process 來實作 keep_alive,這可節省消耗記憶體。

cowboy:start_http(my_http_listener, 100, [{port, 8080}], [
        {env, [{dispatch, Dispatch}]},
        {max_keepalive, 5}
]).

client 通常在發送 request 之後,會等待 response ,接下來才會繼續做其他事情,但是 client 也可以在還沒收到 response 的時候,就一直發送 request,而 server 也會依序處理這些 request 並用同樣的順序回傳 response。這個機制稱為 pipelining,這可以有效減少 latency,通常 browser 會用這個方式取得 static files。

SPDY

request 與 response 都是非同步 asynchronous 的,因為處理每個 request 所要花的時間長短不同,server 跟 client 都可以不按照順序,發送 request 與 response。Cowboy 會為每一個 request 都產生獨立的 process,而且這些 processes 都由另一個 process 管理,並處理 connection。

Routing

Routing 就是將 網址 mapping 對應到 erlang modules,用以處理相關 requests。網址對應會先比對 Host,然後再比對 path。

Routing 功能相關的資料結構如下

Routes = [Host1, Host2, ... HostN].

Host1 = {HostMatch, PathsList}.
Host2 = {HostMatch, Constraints, PathsList}.

PathsList = [Path1, Path2, ... PathN].

Path1 = {PathMatch, Handler, Opts}.
Path2 = {PathMatch, Constraints, Handler, Opts}.

HostMatch 與 PathMatch 可以用 string() 或是 binary() 資料型別。

%% 最簡單的 Host 與 Path 就是完整的路徑。
PathMatch1 = "/".
PathMatch2 = "/path/to/resource". 
HostMatch1 = "cowboy.example.org".

%% PathMatch前面一定要以 / 開頭,最後面的 / 則可有可無。
PathMatch2 = "/path/to/resource".
PathMatch3 = "/path/to/resource/".

%% HostMatch最前面與最後面的 . 都會被忽略,以下這三種 HostMatch 都是一樣的。
HostMatch1 = "cowboy.example.org".
HostMatch2 = "cowboy.example.org.".
HostMatch3 = ".cowboy.example.org".

: 是利用來做 Path 與 Host 的 segment。
以下範例會產生兩個 bindings: subdomain, name。

PathMatch = "/hats/:name/prices".
HostMatch = ":subdomain.example.org".
%% http://test.example.org/hats/wild_cowboy_legendary/prices
%% 結果會是 subdomain 為 test, name 為 wild_cowboy_legendary
%% bindings 的型別為 atom
%% 最後可以用 cowboy_req:binding/{2,3} 取得

_ 跟 erlang 一樣,是 don't care 的符號。

HostMatch = "ninenines.:_".
%% 符合所有以 ninenines. 開頭的 domain names

[] 代表 optional segments

PathMatch = "/hats/[page/:number]".
HostMatch = "[www.]ninenines.eu".

%% 也可以有巢狀的 []
PathMatch = "/hats/[page/[:number]]".

[...] 代表要取得剩下來的所有資料,可放在最前面或最後面

PathMatch = "/hats/[...]".
HostMatch = "[...]ninenines.eu".

如果 :name 出現了兩次,則只會在兩個 :name 的值都一樣時,才會配對成功。如果 :user 同時出現在 HostMatch 與 PathMatch,也是兩個地方都要一樣才會配對成功。

PathMatch = "/hats/:name/:name".
PathMatch = "/hats/:name/[:name]".

PathMatch = "/:user/[...]".
HostMatch = ":user.github.com".

要配對所有 Host 與 Path 就使用 _ 。

PathMatch = '_'.
HostMatch = '_'.

Constraints

在 Matching 結束後,可使用 Constraints 附加測試 result bindings,只有測試通過,Matching 才會完整成功。

Constraints 有兩種測試方式,

{Name, int}
{Name, function, fun ((Value) -> true | {true, NewValue} | false)}

Compilation

cowboy_router:compile/1 將 HostMatching, PathMatching, Constraints 組合起來,有效編譯成 cowboy 的對應表,以便快速找到對應的 Handler。

Dispatch = cowboy_router:compile([
    %% {HostMatch, list({PathMatch, Handler, Opts})}
    {'_', [{'_', my_handler, []}]}
]),
%% Name, NbAcceptors, TransOpts, ProtoOpts
cowboy:start_http(my_http_listener, 100,
    [{port, 8080}],
    [{env, [{dispatch, Dispatch}]}]
).

Live update

cowboy:set_env/3 用來即時更新 routing 使用的 dispatch list

cowboy:set_env(my_http_listener, dispatch,
    cowboy_router:compile(Dispatch)).

Handling plain HTTP requests

Handler 必須要實作三個 callback functions: init/3, handle/2, terminate/3

init/3

最簡單的 init ,就單純 return ok

init(_Type, Req, _Opts) ->
    {ok, Req, no_state}.

限制只能用 ssl 連線,如果用 TCP,就會 crash

init({ssl, _}, Req, _Opts) ->
    {ok, Req, no_state}.

這會檢查 Opts 裡面有沒有 lang option,沒有的話,會直接讓這個 init 設定 crash

init(_Type, Req, Opts) ->
    {_, _Lang} = lists:keyfind(lang, 1, Opts),
    {ok, Req, no_state}.

這會檢查 Opts 裡面有沒有 lang option,沒有的話,就傳回 HTTP response 500,並用 {shutdown, Req2, no_state} 來停止 init。

init(_Type, Req, Opts) ->
    case lists:keyfind(lang, 1, Opts) of
        false ->
            {ok, Req2} = cowboy_req:reply(500, [
                {<<"content-type">>, <<"text/plain">>}
            ], "Missing option 'lang'.", Req),
            {shutdown, Req2, no_state};
        _ ->
            {ok, Req, no_state}
    end.

當我們確認有 lang,就用 state record 把資料傳送到 handler 繼續處理。

-record(state, {
    lang :: en | fr
    %% More fields here.
}).

init(_Type, Req, Opts) ->
    {_, Lang} = lists:keyfind(lang, 1, Opts),
    {ok, Req, #state{lang=Lang}}.

handle/2

不做任何處理的 handle

handle(Req, State) ->
    {ok, Req, State}.

通常得要取得 request 的資訊,然後發送 http response

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

terminate/3

如果有使用 process dictionary, timers, monitors 或是 receiving messages,可使用 terminate 將資源清空

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

Req object

所有 cowboy_req 的 functions 都會更新 request,並將它回傳,我們必須在 handler 中隨時保持更新後的 Req 變數。因為 Req object 容許存取 immutable and mutable state,這表示呼叫某個 function 兩次可能會產生不同的結果,有些 function 會 cache immutable state,因此第二次呼叫處理的速度可能會比較快。

cowboy_req 的 functions 可分為四類

  1. access functions: 會回傳 {Value, Req}
    binding/{2,3}, bindings/1, body_length/1, cookie/{2,3}, cookies/1, header/{2,3}, headers/1, host/1, host_info/1, host_url/1, meta/{2,3}, method/1, path/1, path_info/1, peer/1, port/1, qs/1, qs_val/{2,3}, qs_vals/1, url/1, version/1

  2. question functions: 會回傳 boolean()
    has_body/1, has_resp_body/1, has_resp_header/2

  3. 處理 socket 或是一些可能會失敗的 operations: 會回傳 {Result, Req}, {Result, Value, Req} 或 {error, atom()},還有獨立的 chunk/2 會回傳 ok
    body/{1,2}, body_qs/{1,2}, chunked_reply/{2,3}, init_stream/4, parse_header/{2,3}, reply/{2,3,4}, skip_body/1, stream_body/{1,2}

  4. 會修改 Req object 的 functions: 會回傳新的 Req。
    compact/1, delete_resp_header/2, set_meta/3, set_resp_body/2, set_resp_body_fun/{2,3}, set_resp_cookie/4, set_resp_header/3

Request

標準的 HTTP methods 有 GET, HEAD, OPTIONS, PATCH, POST, PUT, DELETE,這些都是大寫。

    {Method, Req2} = cowboy_req:method(Req).
    case Method of
        <<"POST">> ->
            Body = <<"<h1>This is a response for POST</h1>">>
            {ok, Req3} = cowboy_req:reply(200, [], Body, Req3),
            {ok, Req3, State};
        <<"GET">> ->
            Body = <<"<h1>This is a response for GET</h1>">>
            {ok, Req3} = cowboy_req:reply(200, [], Body, Req3),
            {ok, Req3, State};
        _ ->
            Body = <<"<h1>This is a response for other methods</h1>">>
            {ok, Req3} = cowboy_req:reply(200, [], Body, Req3),
            {ok, Req3, State}
    end.

從 URL 取出 host, port, path 的資訊,另外是取得 Req 的 HTTP version

{Host, Req2} = cowboy_req:host(Req),
{Port, Req3} = cowboy_req:port(Req2),
{Path, Req4} = cowboy_req:path(Req3).

{Version, Req2} = cowboy_req:version(Req).

Bindings

在 Routing 處理過 Request 之後,就會產生 Bindings

%% 取得 my_binding 的值, 如果不存在就會回傳 undefined
{Binding, Req2} = cowboy_req:binding(my_binding, Req).

%% 取得 my_binding 的值, 如果不存在就會回傳 預設值42
{Binding, Req2} = cowboy_req:binding(my_binding, Req, 42).

%% 取得所有 Bindings
{AllBindings, Req2} = cowboy_req:bindings(Req).

%% 在最前面使用 ... ,可用 host_info 取得結果
{HostInfo, Req2} = cowboy_req:host_info(Req).

%% 在最後面使用 ... ,可用 path_info 取得結果
{PathInfo, Req2} = cowboy_req:path_info(Req).

Query string

%% 取得 request query 的字串, 例如 lang=en&key=test
{Qs, Req2} = cowboy_req:qs(Req).

%% 取得 lang 的值
{QsVal, Req2} = cowboy_req:qs_val(<<"lang">>, Req).

%% 取得 lang 的值,沒有就直接回傳預設值 en
{QsVal, Req2} = cowboy_req:qs_val(<<"lang">>, Req, <<"en">>).

%% 取得所有 query string values
{AllValues, Req2} = cowboy_req:qs_vals(Req).

Request URL

%% 重建完整的網址
{URL, Req2} = cowboy_req:url(Req).

%% 去掉 path 與 query string 的網址
{BaseURL, Req2} = cowboy_req:host_url(Req).

Headers

%% 取得 content-type 的 header value
{HeaderVal, Req2} = cowboy_req:header(<<"content-type">>, Req).

%% 取得 content-type 的 header value,預設值為 text/plain
{HeaderVal, Req2} = cowboy_req:header(<<"content-type">>, Req, <<"text/plain">>).

%% 取得所有 headers
{AllHeaders, Req2} = cowboy_req:headers(Req).

%% 將 content-type 的 value 轉換為 cowboy 解析過的 value
{ok, ParsedVal, Req2} = cowboy_req:parse_header(<<"content-type">>, Req).

%% 同上一個 function 的功能,但沒有 content-type 時,會回傳預設值
{ok, ParsedVal, Req2} = cowboy_req:parse_header(<<"content-type">>, Req, {<<"text">>, <<"plain">>, []}).

%% 同上一個 function 的功能,但沒有 content-type 時,會回傳undefined
{undefined, HeaderVal, Req2}
    = cowboy_req:parse_header(<<"unicorn-header">>, Req).

%% parsing 失敗會回傳 {error, Reason}

Meta

cowboy 會自動在 request 中加入一些 meta information

%% 取得 websocket_version 的值
{MetaVal, Req2} = cowboy_req:meta(websocket_version, Req).

%% 取得 websocket_version 的值,沒有的話,就回傳預設值
{MetaVal, Req2} = cowboy_req:meta(websocket_version, Req, 13).

%% 設定新的 meta values,名稱一定要是 atom()
Req2 = cowboy_req:set_meta(the_answer, 42, Req).

Peer

%% 取得 client 的 IP 與 Port
{{IP, Port}, Req2} = cowboy_req:peer(Req).

Reducing the memory

如果不需要讀取 Request 相關資訊後,就可以呼叫 compact/1 把資料清除,釋放記憶體,這個功能可用在 long-polling 或 websocket 中。

Req2 = cowboy_req:compact(Req).

Reading the request body

因為 request body 的大小不固定,所有讀取 body 的 functions 都只能使用一次,而且 cowboy 不會 cache 這些結果。

Check for request body

%% 檢查是否有 request body
cowboy_req:has_body(Req).

%% 取得 request body 的長度
{Length, Req2} = cowboy_req:body_length(Req).

如果有 request body 但是 body_length 卻是 undefined,則代表這是 chunked transfer-encoding,可以用 stream functions 讀取資料。

Reading the body

如果有request header 裡面有 content-length,就可以直接讀取整個 request body。 如果沒有 content-length,就會得到 {error, chunked}。

{ok, Body, Req2} = cowboy_req:body(Req).

cowboy 預設會拒絕超過 8MB 的 body size,以避免受到攻擊。可以用 body function 覆寫。

{ok, Body, Req2} = cowboy_req:body(100000000, Req).

%% 不限制
{ok, Body, Req2} = cowboy_req:body(infinity, Req).

Reading a body sent from an HTML form

如果 request 是 application/x-www-form-urlencoded content-type,可以直接取得 key/value pairs

{ok, KeyValues, Req2} = cowboy_req:body_qs(Req).

%% 從 list 取得 lang 的值
%% 不要直接用 pattern matching 寫,因為 list 沒有固定的順序
{_, Lang} = lists:keyfind(lang, 1, KeyValues).

%% 預設只能處裡 16KB 的 body,可用此 function 覆寫這個限制
{ok, KeyValues, Req2} = cowboy_req:body_qs(500000, Req).

Streaming the body

stream the request body by chunks

{ok, Chunk, Req2} = cowboy_req:stream_body(Req).

%% 預設每個 chunk 的大小上限為 1MB
%% 也可以覆寫這個限制
{ok, Chunk, Req2} = cowboy_req:stream_body(500000, Req).

如果 body 已經讀完了,接下來所有function call 都會回傳 {done, Req2}。

這個resursive function範例會讀取整個 request body,持續處理所有 chunks,並列印到 console 上。

body_to_console(Req) ->
    case cowboy_req:stream_body(Req) of
        {ok, Chunk, Req2} ->
            io:format("~s", [Chunk]),
            body_to_console(Req2);
        {done, Req2} ->
            Req2
    end.

cowboy 預設會根據 transfer-encoding decode 所有 chunks 資料,預設不會 decode 任何 content-encoding。在啟動 stream 之前,要先呼叫 init_stream設定transfer_decode與content-encoding的callback function。

{ok, Req2} = cowboy_req:init_stream(fun transfer_decode/2,
    TransferStartState, fun content_decode/1, Req).

Skipping the body

{ok, Req2} = cowboy_req:skip_body(Req).

Sending a response

只能發送一個response,如果再發送一次,就會造成 crash。reponse 可以一次送完,或是 streamed by 任意長度的 chunks。

Reply

%% 發送 reponse,cowboy 會自動把 header 補齊
{ok, Req2} = cowboy_req:reply(200, Req).

%% 發送 response,並增加自訂的 header
{ok, Req2} = cowboy_req:reply(303, [
    {<<"location">>, <<"http://ninenines.eu">>}
], Req).

%% 可覆寫 response header 的資訊
{ok, Req2} = cowboy_req:reply(200, [
    {<<"server">>, <<"yaws">>}
], Req).

%% 發送 response body 時,要設定 content-type,cowboy會自動設定 content-length
{ok, Req2} = cowboy_req:reply(200, [
    {<<"content-type">>, <<"text/plain">>
], "Hello world!", Req).

{ok, Req2} = cowboy_req:reply(200, [
    {<<"content-type">>, <<"text/html">>}
], "<html><head>Hello world!</head><body><p>Hats off!</p></body></html>", Req).

Chunked reply

%% 將 reponse 分成任意長度的 chunked data,前面一定要 match ok,否則就代表 cowboy_req:chunk 發生錯誤
{ok, Req2} = cowboy_req:chunked_reply(200, Req),
ok = cowboy_req:chunk("Hello...", Req2),
ok = cowboy_req:chunk("chunked...", Req2),
ok = cowboy_req:chunk("world!!", Req2).

%% 雖然可以發送沒有 content-type 的 response,但還是建議產生 chunked_reply 時,要指定 content-type
{ok, Req2} = cowboy_req:chunked_reply(200, [
    {<<"content-type">>, <<"text/html">>}
], Req),
ok = cowboy_req:chunk("<html><head>Hello world!</head>", Req2),
ok = cowboy_req:chunk("<body><p>Hats off!</p></body></html>", Req2).

Preset response headers

%% 可覆寫 cowboy 預設的 response header
Req2 = cowboy_req:set_resp_header(<<"allow">>, "GET", Req).

%% 測試是不是已經有設定某個 response header,這裡只會測試自訂的 response header,不會測試 cowboy 增加到 reply 的 headers,會回傳 true/false
cowboy_req:has_resp_header(<<"allow">>, Req).

%% 刪除 preset response header
Req2 = cowboy_req:delete_resp_header(<<"allow">>, Req).

Preset response body

%% 設定 preset reaponse body,如果是要發送 chunked reply 或是有確切 body內容的 reply,就會這個預先設定的 body。
Req2 = cowboy_req:set_resp_body("Hello world!", Req).

%% 有三種方式,可以在發送 response body 前,呼叫一個 fun callback
%% 1. 如果知道 body length
F = fun (Socket, Transport) ->
    Transport:send(Socket, "Hello world!")
end,
Req2 = cowboy_req:set_resp_body_fun(12, F, Req).

%% 2. 不知道 body length,就改用 chunked response body fun
F = fun (SendChunk) ->
    Body = lists:duplicate(random:uniform(1024, $a)),
    SendChunk(Body)
end,
Req2 = cowboy_req:set_resp_body_fun(chunked, F, Req).

%% 3. 也可在不知道長度的狀況下,直接向 socket 發送資料,cowboy 會自動根據 protocol,決定要不要主動關閉 connection
F = fun (Socket, Transport) ->
    Body = lists:duplicate(random:uniform(1024, $a)),
    Transport:send(Socket, Body)
end,
Req2 = cowboy_req:set_resp_body_fun(F, Req).

Sending files

可在不讀取檔案的條件下,直接從 disk 回傳檔案內容給 client,cowboy 是直接由 kernel 透過 syscall 把檔案發送到 socket,建議最好要先設定 file size。

F = fun (Socket, Transport) ->
    Transport:sendfile(Socket, "priv/styles.css")
end,
Req2 = cowboy_req:set_resp_body_fun(FileSize, F, Req).

Reference

cowboy user guide