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並發連接目標