cowboy source code 裡面有一個 examples 目錄,列出多個範例程式,接下來我們藉由閱讀程式碼的方式,了解如何使用 cowboy。
rest_basic_auth
- rest_basic_auth.app.src
- rest_basic_auth_sup.erl
- rest_basic_auth_app.erl
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 的資料。
- rest_pastebin.app.src
- rest_pastebin_sup.erl
- rest_pastebin_app.erl
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
- rest_stream_response.app.src
- rest_stream_response_sup.erl
rest_stream_response_app.erl
啟動時,以 ets 產生 1000 筆亂數資料
Table = ets:new(stream_tab, []), generate_rows(Table, 1000),
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
- compress_response.app.src
- compress_response_sup.erl
compress_response_app.erl
在啟動時,指定 {compress, true}{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [ {compress, true}, {env, [{dispatch, Dispatch}]} ]),
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 的範例。
- eventsource.app.src
- eventsource_sup.erl
- eventsource_app.erl
compile routing 時,指定 /eventsource 由 eventsource_handler 處理。Dispatch = cowboy_router:compile([ {'_', [ {"/eventsource", eventsource_handler, []}, {"/", cowboy_static, {priv_file, eventsource, "index.html"}} ]} ]),
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。
- markdown_middleware.app.src
- markdown_middleware_sup.erl
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]} ]),
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.
erlmarkdown.erl
這是用來處理 .md -> .html 的程式
測試時,瀏覽網頁 http://localhost:8080/video.html 。
沒有留言:
張貼留言