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