2014/06/09

erlang - cowboy

Erlang Web Modules

Erlang 的 http 網頁框架最常見的是 yaws 與 Mochiweb,但除了這兩個框架,還有很多 http 函式庫,Erlang的Web庫和框架 簡單比較了 erlang 所有的 web library: yaws, Mochiweb, Misultin, Cowboy, httpd, SimpleBridge, webmachine, Nitrogen, Zotonic, Chicago Boss。

這篇文章 An interview with #erlang cowboy Loïc Hoguin (@lhoguin)
有一段跟 Cowboy 作者 Loïc Hoguin 的訪談,內容討論到 Cowboy 的優勢跟實作的理念。

從一些文章針對 Cowboy 的討論,一致認為 Cowboy 的實作方式,相較於其他的函式庫,有著一些特別的優勢,接下來就說明這些優點。

Cowboy 的優勢

Cowboy 跟其他 erlang web projects 例如 Misultin, Webmachine, Yaws 有兩個最大的差異,使用了 binaries 而不是 lists,另外一個重點是利用 Ranch 實作的 generic acceptor pool。

Cowboy 設計用來支援 99% 的 use case 而不管剩下的 1%,作者會根據新功能需求的內容,判斷是不是特殊需求,並決定要不要實作,因為使用者可以自己取得 source code 實做那些特殊的功能需求。

Cowboy 遵循了 Erlang clean code 的原則,包含了數百個測試程式並完整相容於 Dialyzer,不使用 process dictionaries 與 parametrized modules,作者認為多寫一些程式碼,有助於提高可閱讀性。Cowboy 為每個 connection 使用一個 process而不是兩個,而且用 binaries 取代 list 實作,因此記憶體的消耗量低。

Cowboy 目前支援 HTTP/1.0, HTTP/1.1, SPDY, Websocket (all implemented drafts + standard) 與 Webmachine-based REST。

user guide, manual

研究 Cowboy 最大的問題就是,還沒有很完整的使用教學手冊,就連 User Guide 也還沒有寫完,官方網站 建議大家閱讀 cowboy user guidecowboy function reference 與原始程式碼中的 examples。

User Guide 首先就說明,所有 HTTP 標準的 method name 都是 case sensitive,而且都是 uppercase,另外 HTTP header name 是 case insensistive,所以 Cowboy 設定 header name 為 lowercase。

Cowboy 目前除了還不支援 HTTP/2.0 之外,支援了 HTTP/1.0, HTTP/1.1, REST, XmlHttpRequest, Long-polling, HTML5, EventSource, Websocket, SPDY。

解釋一下不常見的 EventSource,這也可稱為 Server-Sent Events,這可讓 server push 資料到 HTML5 applications,這是由 server 到 client 的單向通訊channel。EventSource只支援 UTF-8 encoded text data,不能發送 binary data,通常我們會使用能提供雙向通訊的 Websocket。

SPDY 是減少網頁載入時間的協定,他會對每一個server都打開一條單一的連線,保持連線並處理所有的 requests,同時會壓縮 HTTP headers以減少 request 的資料量,SPDY相容於 HTTP/1.1。

編譯 Cowboy

編譯 cowboy 需要一些工具及搭配的 libraries: make, erlang.mk, relx, ranch, cowlib

erlang.mk 是 erlang application 的 Makefile
relx 是 Erlang/OTP release 工具
ranch 是 Socket acceptor pool for TCP protocols
cowlib 是處理 Web protocols 的 library

首先下載 Cowboy 0.9 版,解壓縮後,直接 make 會一直出現 error,問題出在 wget https 網站。修改 erlang.mk 裡面的兩個 wget,在後面加上 --no-check-certificate 參數,就可以完成編譯。

define get_pkg_file
    wget --no-check-certificate -O $(PKG_FILE) $(PKG_FILE_URL) || rm $(PKG_FILE)
endef

.....

define get_relx
    wget --no-check-certificate -O $(RELX) $(RELX_URL) || rm $(RELX)
    chmod +x $(RELX)
endef

Hello World

首先製作原始程式、 app 的設定與 Makefile

hello_erlang/
    src/
        hello_erlang.app.src
        hello_erlang_app.erl
        hello_erlang_sup.erl
        hello_handler.erl
    erlang.mk
    Makefile
    relx.config

這是 hello_erlang 的 application 設定

%% hello_erlang.app.src
{application, hello_world, [
    {description, "Cowboy Hello World example."},
    {vsn, "1"},
    % 目前保留為空白,但編譯時,會自動以 src 裡面編譯的所有 modules 的 list 取代
    {modules, []},
    % 通常只需要註冊 application 裡面的 top-level supervisor
    {registered, [hello_world_sup]},
    % 列出執行時所有需要的 applications
    {applications, [
        kernel,
        stdlib,
        cowboy
    ]},
    % 啟動 applcation 的 callback module
    {mod, {hello_world_app, []}},
    {env, []}
]}.

application 的 callback module,必須有 start/2 跟 stop/1 function

%% hello_world_app.erl
%% @private
-module(hello_world_app).
-behaviour(application).

-export([start/2]).
-export([stop/1]).

start(_Type, _Args) ->
    Dispatch = cowboy_router:compile([
        {'_', [
            {"/", toppage_handler, []}
        ]}
    ]),
    {ok, _} = cowboy:start_http(http, 100, [{port, 8000}], [
        {env, [{dispatch, Dispatch}]}
    ]),
    hello_world_sup:start_link().

stop(_State) ->
    ok.

application 的監督者

% hello_world_sup.erl
%% @private
-module(hello_world_sup).
-behaviour(supervisor).

%% API.
-export([start_link/0]).

%% supervisor.
-export([init/1]).

%% API.

-spec start_link() -> {ok, pid()}.
start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

%% supervisor.

init([]) ->
    Procs = [],
    % 重新啟動的策略為 one_for_one, 在 10 秒內重新啟動工作者超過 10 次,此監督者就會終止所有工作行程
    {ok, {{one_for_one, 10, 10}, Procs}}.

這是 Cowboy 最基本的 HTTP handler,需要實作 init/3, handle/2 and terminate/3 三個 callback functions,細節可參閱 cowboy_http_handler 文件。

%% toppage_handler.erl
%% @doc Hello world handler.
-module(toppage_handler).

-export([init/3]).
-export([handle/2]).
-export([terminate/3]).

% init 會初始化 the state for this request
% Req 是 cowboy_req:req()
init(_Type, Req, []) ->
    {ok, Req, undefined}.

% 處理 request 並以 cowboy_req:reply 回傳 HTTP response
% header 與 content 資料內容都必須要使用 binaries
handle(Req, State) ->
    {ok, Req2} = cowboy_req:reply(200, [
        {<<"content-type">>, <<"text/plain">>}
    ], <<"Hello world!">>, Req),
    {ok, Req2, State}.

terminate(_Reason, _Req, _State) ->
    ok.

編譯 hello world 之前,要先取得 erlang.mk

wget --no-check-certificate https://raw.github.com/extend/erlang.mk/master/erlang.mk

然後編輯 Makefile,內容如下

PROJECT = hello_erlang

DEPS = cowboy
dep_cowboy = pkg://cowboy master

include erlang.mk

另外為了產生 application release,我們要編輯一個 relx.config 檔案,內容如下。第一行的內容,代表會產生一個名稱為 hello_world_example 且版本號碼為 1 的執行 script,application 模組名稱為 hello_world

{release, {hello_world_example, "1"}, [hello_world]}.
{extended_start_script, true}.

接下來執行 make 就可以編譯 hello_world,erlang.mk 將會自動下載 relx,並建立 release package。

make
./_rel/bin/hello_world_example console

接下來就可以在 browser 上,以 http://localhost:8000/ 看到 hello world 網頁,也可以用 curl 測試

[root@koko cowboy-0.9.0]# curl -i http://localhost:8000/
HTTP/1.1 200 OK
connection: keep-alive
server: Cowboy
date: Wed, 09 Apr 2014 08:58:03 GMT
content-length: 12
content-type: text/plain

Hello world!

HTTP

當 http client 發送 request 到 server 直到產生 response 為止,必須要經過一些步驟:讀取 request -> 取得 resource -> 準備 response 資料 -> 同時可寫入 log,整個過程可由下圖表示。深綠色的 onrequest, handler, onresponse 都是可以介入,加入我們自己的程式碼的地方。

request 的處理會因為 protocol 的不同而有差異,HTTP/1.0 每一次連線都只能處理一個 request,因此 Cowboy 會在發送 repsonse 之後立刻關閉連線。HTTP/1.1 允許保持連線狀態 (keep-alive),SPDY 允許在一個連線中,非同步地同時發送多個 requests。

HTTP/1.1 keep-alive

server 可用以下程式碼,發送 connection:close 的 header,並強制將連線關閉。

{ok, Req2} = cowboy_req:reply(200, [
    {<<"connection">>, <<"close">>},
], <<"Closing the socket in 3.. 2.. 1..">>, Req).

Cowboy 預設只會在每一個連線中,接受最多 100 個 requests,這個數量可以透過 max_keepalive 的設定來修改。Cowboy 是利用 reuse 所有 request 的 process 來實作 keep_alive,這可節省消耗記憶體。

cowboy:start_http(my_http_listener, 100, [{port, 8080}], [
        {env, [{dispatch, Dispatch}]},
        {max_keepalive, 5}
]).

client 通常在發送 request 之後,會等待 response ,接下來才會繼續做其他事情,但是 client 也可以在還沒收到 response 的時候,就一直發送 request,而 server 也會依序處理這些 request 並用同樣的順序回傳 response。這個機制稱為 pipelining,這可以有效減少 latency,通常 browser 會用這個方式取得 static files。

SPDY

request 與 response 都是非同步 asynchronous 的,因為處理每個 request 所要花的時間長短不同,server 跟 client 都可以不按照順序,發送 request 與 response。Cowboy 會為每一個 request 都產生獨立的 process,而且這些 processes 都由另一個 process 管理,並處理 connection。

Routing

Routing 就是將 網址 mapping 對應到 erlang modules,用以處理相關 requests。網址對應會先比對 Host,然後再比對 path。

Routing 功能相關的資料結構如下

Routes = [Host1, Host2, ... HostN].

Host1 = {HostMatch, PathsList}.
Host2 = {HostMatch, Constraints, PathsList}.

PathsList = [Path1, Path2, ... PathN].

Path1 = {PathMatch, Handler, Opts}.
Path2 = {PathMatch, Constraints, Handler, Opts}.

HostMatch 與 PathMatch 可以用 string() 或是 binary() 資料型別。

%% 最簡單的 Host 與 Path 就是完整的路徑。
PathMatch1 = "/".
PathMatch2 = "/path/to/resource". 
HostMatch1 = "cowboy.example.org".

%% PathMatch前面一定要以 / 開頭,最後面的 / 則可有可無。
PathMatch2 = "/path/to/resource".
PathMatch3 = "/path/to/resource/".

%% HostMatch最前面與最後面的 . 都會被忽略,以下這三種 HostMatch 都是一樣的。
HostMatch1 = "cowboy.example.org".
HostMatch2 = "cowboy.example.org.".
HostMatch3 = ".cowboy.example.org".

: 是利用來做 Path 與 Host 的 segment。
以下範例會產生兩個 bindings: subdomain, name。

PathMatch = "/hats/:name/prices".
HostMatch = ":subdomain.example.org".
%% http://test.example.org/hats/wild_cowboy_legendary/prices
%% 結果會是 subdomain 為 test, name 為 wild_cowboy_legendary
%% bindings 的型別為 atom
%% 最後可以用 cowboy_req:binding/{2,3} 取得

_ 跟 erlang 一樣,是 don't care 的符號。

HostMatch = "ninenines.:_".
%% 符合所有以 ninenines. 開頭的 domain names

[] 代表 optional segments

PathMatch = "/hats/[page/:number]".
HostMatch = "[www.]ninenines.eu".

%% 也可以有巢狀的 []
PathMatch = "/hats/[page/[:number]]".

[...] 代表要取得剩下來的所有資料,可放在最前面或最後面

PathMatch = "/hats/[...]".
HostMatch = "[...]ninenines.eu".

如果 :name 出現了兩次,則只會在兩個 :name 的值都一樣時,才會配對成功。如果 :user 同時出現在 HostMatch 與 PathMatch,也是兩個地方都要一樣才會配對成功。

PathMatch = "/hats/:name/:name".
PathMatch = "/hats/:name/[:name]".

PathMatch = "/:user/[...]".
HostMatch = ":user.github.com".

要配對所有 Host 與 Path 就使用 _ 。

PathMatch = '_'.
HostMatch = '_'.

Constraints

在 Matching 結束後,可使用 Constraints 附加測試 result bindings,只有測試通過,Matching 才會完整成功。

Constraints 有兩種測試方式,

{Name, int}
{Name, function, fun ((Value) -> true | {true, NewValue} | false)}

Compilation

cowboy_router:compile/1 將 HostMatching, PathMatching, Constraints 組合起來,有效編譯成 cowboy 的對應表,以便快速找到對應的 Handler。

Dispatch = cowboy_router:compile([
    %% {HostMatch, list({PathMatch, Handler, Opts})}
    {'_', [{'_', my_handler, []}]}
]),
%% Name, NbAcceptors, TransOpts, ProtoOpts
cowboy:start_http(my_http_listener, 100,
    [{port, 8080}],
    [{env, [{dispatch, Dispatch}]}]
).

Live update

cowboy:set_env/3 用來即時更新 routing 使用的 dispatch list

cowboy:set_env(my_http_listener, dispatch,
    cowboy_router:compile(Dispatch)).

Handling plain HTTP requests

Handler 必須要實作三個 callback functions: init/3, handle/2, terminate/3

init/3

最簡單的 init ,就單純 return ok

init(_Type, Req, _Opts) ->
    {ok, Req, no_state}.

限制只能用 ssl 連線,如果用 TCP,就會 crash

init({ssl, _}, Req, _Opts) ->
    {ok, Req, no_state}.

這會檢查 Opts 裡面有沒有 lang option,沒有的話,會直接讓這個 init 設定 crash

init(_Type, Req, Opts) ->
    {_, _Lang} = lists:keyfind(lang, 1, Opts),
    {ok, Req, no_state}.

這會檢查 Opts 裡面有沒有 lang option,沒有的話,就傳回 HTTP response 500,並用 {shutdown, Req2, no_state} 來停止 init。

init(_Type, Req, Opts) ->
    case lists:keyfind(lang, 1, Opts) of
        false ->
            {ok, Req2} = cowboy_req:reply(500, [
                {<<"content-type">>, <<"text/plain">>}
            ], "Missing option 'lang'.", Req),
            {shutdown, Req2, no_state};
        _ ->
            {ok, Req, no_state}
    end.

當我們確認有 lang,就用 state record 把資料傳送到 handler 繼續處理。

-record(state, {
    lang :: en | fr
    %% More fields here.
}).

init(_Type, Req, Opts) ->
    {_, Lang} = lists:keyfind(lang, 1, Opts),
    {ok, Req, #state{lang=Lang}}.

handle/2

不做任何處理的 handle

handle(Req, State) ->
    {ok, Req, State}.

通常得要取得 request 的資訊,然後發送 http response

handle(Req, State) ->
    {ok, Req2} = cowboy_req:reply(200, [
        {<<"content-type">>, <<"text/plain">>}
    ], <<"Hello World!">>, Req),
    {ok, Req2, State}.

terminate/3

如果有使用 process dictionary, timers, monitors 或是 receiving messages,可使用 terminate 將資源清空

terminate(_Reason, Req, State) ->
    ok.

Req object

所有 cowboy_req 的 functions 都會更新 request,並將它回傳,我們必須在 handler 中隨時保持更新後的 Req 變數。因為 Req object 容許存取 immutable and mutable state,這表示呼叫某個 function 兩次可能會產生不同的結果,有些 function 會 cache immutable state,因此第二次呼叫處理的速度可能會比較快。

cowboy_req 的 functions 可分為四類

  1. access functions: 會回傳 {Value, Req}
    binding/{2,3}, bindings/1, body_length/1, cookie/{2,3}, cookies/1, header/{2,3}, headers/1, host/1, host_info/1, host_url/1, meta/{2,3}, method/1, path/1, path_info/1, peer/1, port/1, qs/1, qs_val/{2,3}, qs_vals/1, url/1, version/1

  2. question functions: 會回傳 boolean()
    has_body/1, has_resp_body/1, has_resp_header/2

  3. 處理 socket 或是一些可能會失敗的 operations: 會回傳 {Result, Req}, {Result, Value, Req} 或 {error, atom()},還有獨立的 chunk/2 會回傳 ok
    body/{1,2}, body_qs/{1,2}, chunked_reply/{2,3}, init_stream/4, parse_header/{2,3}, reply/{2,3,4}, skip_body/1, stream_body/{1,2}

  4. 會修改 Req object 的 functions: 會回傳新的 Req。
    compact/1, delete_resp_header/2, set_meta/3, set_resp_body/2, set_resp_body_fun/{2,3}, set_resp_cookie/4, set_resp_header/3

Request

標準的 HTTP methods 有 GET, HEAD, OPTIONS, PATCH, POST, PUT, DELETE,這些都是大寫。

    {Method, Req2} = cowboy_req:method(Req).
    case Method of
        <<"POST">> ->
            Body = <<"<h1>This is a response for POST</h1>">>
            {ok, Req3} = cowboy_req:reply(200, [], Body, Req3),
            {ok, Req3, State};
        <<"GET">> ->
            Body = <<"<h1>This is a response for GET</h1>">>
            {ok, Req3} = cowboy_req:reply(200, [], Body, Req3),
            {ok, Req3, State};
        _ ->
            Body = <<"<h1>This is a response for other methods</h1>">>
            {ok, Req3} = cowboy_req:reply(200, [], Body, Req3),
            {ok, Req3, State}
    end.

從 URL 取出 host, port, path 的資訊,另外是取得 Req 的 HTTP version

{Host, Req2} = cowboy_req:host(Req),
{Port, Req3} = cowboy_req:port(Req2),
{Path, Req4} = cowboy_req:path(Req3).

{Version, Req2} = cowboy_req:version(Req).

Bindings

在 Routing 處理過 Request 之後,就會產生 Bindings

%% 取得 my_binding 的值, 如果不存在就會回傳 undefined
{Binding, Req2} = cowboy_req:binding(my_binding, Req).

%% 取得 my_binding 的值, 如果不存在就會回傳 預設值42
{Binding, Req2} = cowboy_req:binding(my_binding, Req, 42).

%% 取得所有 Bindings
{AllBindings, Req2} = cowboy_req:bindings(Req).

%% 在最前面使用 ... ,可用 host_info 取得結果
{HostInfo, Req2} = cowboy_req:host_info(Req).

%% 在最後面使用 ... ,可用 path_info 取得結果
{PathInfo, Req2} = cowboy_req:path_info(Req).

Query string

%% 取得 request query 的字串, 例如 lang=en&key=test
{Qs, Req2} = cowboy_req:qs(Req).

%% 取得 lang 的值
{QsVal, Req2} = cowboy_req:qs_val(<<"lang">>, Req).

%% 取得 lang 的值,沒有就直接回傳預設值 en
{QsVal, Req2} = cowboy_req:qs_val(<<"lang">>, Req, <<"en">>).

%% 取得所有 query string values
{AllValues, Req2} = cowboy_req:qs_vals(Req).

Request URL

%% 重建完整的網址
{URL, Req2} = cowboy_req:url(Req).

%% 去掉 path 與 query string 的網址
{BaseURL, Req2} = cowboy_req:host_url(Req).

Headers

%% 取得 content-type 的 header value
{HeaderVal, Req2} = cowboy_req:header(<<"content-type">>, Req).

%% 取得 content-type 的 header value,預設值為 text/plain
{HeaderVal, Req2} = cowboy_req:header(<<"content-type">>, Req, <<"text/plain">>).

%% 取得所有 headers
{AllHeaders, Req2} = cowboy_req:headers(Req).

%% 將 content-type 的 value 轉換為 cowboy 解析過的 value
{ok, ParsedVal, Req2} = cowboy_req:parse_header(<<"content-type">>, Req).

%% 同上一個 function 的功能,但沒有 content-type 時,會回傳預設值
{ok, ParsedVal, Req2} = cowboy_req:parse_header(<<"content-type">>, Req, {<<"text">>, <<"plain">>, []}).

%% 同上一個 function 的功能,但沒有 content-type 時,會回傳undefined
{undefined, HeaderVal, Req2}
    = cowboy_req:parse_header(<<"unicorn-header">>, Req).

%% parsing 失敗會回傳 {error, Reason}

Meta

cowboy 會自動在 request 中加入一些 meta information

%% 取得 websocket_version 的值
{MetaVal, Req2} = cowboy_req:meta(websocket_version, Req).

%% 取得 websocket_version 的值,沒有的話,就回傳預設值
{MetaVal, Req2} = cowboy_req:meta(websocket_version, Req, 13).

%% 設定新的 meta values,名稱一定要是 atom()
Req2 = cowboy_req:set_meta(the_answer, 42, Req).

Peer

%% 取得 client 的 IP 與 Port
{{IP, Port}, Req2} = cowboy_req:peer(Req).

Reducing the memory

如果不需要讀取 Request 相關資訊後,就可以呼叫 compact/1 把資料清除,釋放記憶體,這個功能可用在 long-polling 或 websocket 中。

Req2 = cowboy_req:compact(Req).

Reading the request body

因為 request body 的大小不固定,所有讀取 body 的 functions 都只能使用一次,而且 cowboy 不會 cache 這些結果。

Check for request body

%% 檢查是否有 request body
cowboy_req:has_body(Req).

%% 取得 request body 的長度
{Length, Req2} = cowboy_req:body_length(Req).

如果有 request body 但是 body_length 卻是 undefined,則代表這是 chunked transfer-encoding,可以用 stream functions 讀取資料。

Reading the body

如果有request header 裡面有 content-length,就可以直接讀取整個 request body。 如果沒有 content-length,就會得到 {error, chunked}。

{ok, Body, Req2} = cowboy_req:body(Req).

cowboy 預設會拒絕超過 8MB 的 body size,以避免受到攻擊。可以用 body function 覆寫。

{ok, Body, Req2} = cowboy_req:body(100000000, Req).

%% 不限制
{ok, Body, Req2} = cowboy_req:body(infinity, Req).

Reading a body sent from an HTML form

如果 request 是 application/x-www-form-urlencoded content-type,可以直接取得 key/value pairs

{ok, KeyValues, Req2} = cowboy_req:body_qs(Req).

%% 從 list 取得 lang 的值
%% 不要直接用 pattern matching 寫,因為 list 沒有固定的順序
{_, Lang} = lists:keyfind(lang, 1, KeyValues).

%% 預設只能處裡 16KB 的 body,可用此 function 覆寫這個限制
{ok, KeyValues, Req2} = cowboy_req:body_qs(500000, Req).

Streaming the body

stream the request body by chunks

{ok, Chunk, Req2} = cowboy_req:stream_body(Req).

%% 預設每個 chunk 的大小上限為 1MB
%% 也可以覆寫這個限制
{ok, Chunk, Req2} = cowboy_req:stream_body(500000, Req).

如果 body 已經讀完了,接下來所有function call 都會回傳 {done, Req2}。

這個resursive function範例會讀取整個 request body,持續處理所有 chunks,並列印到 console 上。

body_to_console(Req) ->
    case cowboy_req:stream_body(Req) of
        {ok, Chunk, Req2} ->
            io:format("~s", [Chunk]),
            body_to_console(Req2);
        {done, Req2} ->
            Req2
    end.

cowboy 預設會根據 transfer-encoding decode 所有 chunks 資料,預設不會 decode 任何 content-encoding。在啟動 stream 之前,要先呼叫 init_stream設定transfer_decode與content-encoding的callback function。

{ok, Req2} = cowboy_req:init_stream(fun transfer_decode/2,
    TransferStartState, fun content_decode/1, Req).

Skipping the body

{ok, Req2} = cowboy_req:skip_body(Req).

Sending a response

只能發送一個response,如果再發送一次,就會造成 crash。reponse 可以一次送完,或是 streamed by 任意長度的 chunks。

Reply

%% 發送 reponse,cowboy 會自動把 header 補齊
{ok, Req2} = cowboy_req:reply(200, Req).

%% 發送 response,並增加自訂的 header
{ok, Req2} = cowboy_req:reply(303, [
    {<<"location">>, <<"http://ninenines.eu">>}
], Req).

%% 可覆寫 response header 的資訊
{ok, Req2} = cowboy_req:reply(200, [
    {<<"server">>, <<"yaws">>}
], Req).

%% 發送 response body 時,要設定 content-type,cowboy會自動設定 content-length
{ok, Req2} = cowboy_req:reply(200, [
    {<<"content-type">>, <<"text/plain">>
], "Hello world!", Req).

{ok, Req2} = cowboy_req:reply(200, [
    {<<"content-type">>, <<"text/html">>}
], "<html><head>Hello world!</head><body><p>Hats off!</p></body></html>", Req).

Chunked reply

%% 將 reponse 分成任意長度的 chunked data,前面一定要 match ok,否則就代表 cowboy_req:chunk 發生錯誤
{ok, Req2} = cowboy_req:chunked_reply(200, Req),
ok = cowboy_req:chunk("Hello...", Req2),
ok = cowboy_req:chunk("chunked...", Req2),
ok = cowboy_req:chunk("world!!", Req2).

%% 雖然可以發送沒有 content-type 的 response,但還是建議產生 chunked_reply 時,要指定 content-type
{ok, Req2} = cowboy_req:chunked_reply(200, [
    {<<"content-type">>, <<"text/html">>}
], Req),
ok = cowboy_req:chunk("<html><head>Hello world!</head>", Req2),
ok = cowboy_req:chunk("<body><p>Hats off!</p></body></html>", Req2).

Preset response headers

%% 可覆寫 cowboy 預設的 response header
Req2 = cowboy_req:set_resp_header(<<"allow">>, "GET", Req).

%% 測試是不是已經有設定某個 response header,這裡只會測試自訂的 response header,不會測試 cowboy 增加到 reply 的 headers,會回傳 true/false
cowboy_req:has_resp_header(<<"allow">>, Req).

%% 刪除 preset response header
Req2 = cowboy_req:delete_resp_header(<<"allow">>, Req).

Preset response body

%% 設定 preset reaponse body,如果是要發送 chunked reply 或是有確切 body內容的 reply,就會這個預先設定的 body。
Req2 = cowboy_req:set_resp_body("Hello world!", Req).

%% 有三種方式,可以在發送 response body 前,呼叫一個 fun callback
%% 1. 如果知道 body length
F = fun (Socket, Transport) ->
    Transport:send(Socket, "Hello world!")
end,
Req2 = cowboy_req:set_resp_body_fun(12, F, Req).

%% 2. 不知道 body length,就改用 chunked response body fun
F = fun (SendChunk) ->
    Body = lists:duplicate(random:uniform(1024, $a)),
    SendChunk(Body)
end,
Req2 = cowboy_req:set_resp_body_fun(chunked, F, Req).

%% 3. 也可在不知道長度的狀況下,直接向 socket 發送資料,cowboy 會自動根據 protocol,決定要不要主動關閉 connection
F = fun (Socket, Transport) ->
    Body = lists:duplicate(random:uniform(1024, $a)),
    Transport:send(Socket, Body)
end,
Req2 = cowboy_req:set_resp_body_fun(F, Req).

Sending files

可在不讀取檔案的條件下,直接從 disk 回傳檔案內容給 client,cowboy 是直接由 kernel 透過 syscall 把檔案發送到 socket,建議最好要先設定 file size。

F = fun (Socket, Transport) ->
    Transport:sendfile(Socket, "priv/styles.css")
end,
Req2 = cowboy_req:set_resp_body_fun(FileSize, F, Req).

Reference

cowboy user guide

沒有留言:

張貼留言