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 流程圖,可以先下載後,再拖拉到瀏覽器中觀看,這四個流程圖分別說明了以下的流程。
- Beginning part, up to resource_exists
- From resource_exists, for HEAD and GET requests
- From resource_exists, for POST/PATCH/PUT requests
- 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
- {ok, Req, Env} : 會繼續執行下一個 middleware
- {suspend, Module, Function, Args} : to hibernate,繼續執行下一個 MFA
- {halt, Req} : 停止處理這個 request,繼續下一個 request
- {error, StatusCode, Req} : 回應 error 並 close the socket
Configuration
Env 裡面保留了兩個值
- listener
包含 name of the listener - 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
沒有留言:
張貼留言