cowboy source code 裡面有一個 examples 目錄,列出多個範例程式,接下來我們藉由閱讀程式碼的方式,了解如何使用 cowboy。
hello_world
主要寫了四支程式
hello_erlang.app.src
application 設定hello_erlang_sup.erl
application 的監督者 supervisorhello_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().
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
ssl_hello_world.app.src
application 設定ssl_hello_world_sup_erl
supervisorssl_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().
toppage_handler.erl
跟 hello world 的 toppage_handler.erl 完全一樣。
測試時必須要連結 https://localhost:8443/ 這個網址。
chunked_hello_world
chunked data transfer with two one-second delays
chunked_hello_world.app.src
application 設定chunked_hello_world_sup.erl
supervisorchunked_hello_world_app.erl
跟 hello world 的 hello_erlang_app.erl 完全一樣。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 資料內容。
rest_hello_world.app.src
application 設定rest_hello_world_sup.erl
supervisorrest_hello_world_app.erl
application 的 callback module,跟 hello world 的 hello_erlang_app.erl 完全一樣。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
- html
curl -i http://localhost:8080
- json
curl -i -H "Accept: application/json" http://localhost:8080
- text
curl -i -H "Accept: text/plain" http://localhost:8080
static world
static file handler
staitc_world.app.src
application 設定static_world_sup.erl
supervisorstatic_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
web_server.app.src
application 設定web_server_sup.erl
supervisorweb_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().
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"].
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/。
另外,這個程式實際上還有些問題,例如:
http://localhost:8080// 當網址後面多了一個 / 的時候,網頁上會列印出機器根目錄的目錄及檔案。
在 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
echo_get.app.src
application 設定echo_get_sup.erl
supervisorecho_get_app.erl
跟 hello world 的 hello_erlang_app.erl 完全一樣。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
- echo_post.app.src
- echo_post_sup.erl
- echo_post_app.erl
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
- cookie.app.src
- cookie_sup.erl
- cookie_app.erl
- toppage_handler.erl
以 cowboy_req:set_resp_cookie 設定 cookiehandle(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
- websocket.app.src
- websocket_sup.erl
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"}} ]} ]),
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
- error_hook.app.src
- error_hook_sup.erl
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.
沒有留言:
張貼留言