📜 ⬆️ ⬇️

Cowboy Web Server

Hello!
In this tutorial, I plan to show those who are not familiar with the Cowboy web server how to use it. For people who have experience with him, this tutorial will hardly be interesting, but for those who know about Cowboy only by hearsay - welcome!

What do we do:
  1. The simplest installation and server startup
  2. Routing Overview, Static Maintenance
  3. Templating with ErlyDTL (Django Template Language for Erlang)


For convenience, we need rebar, the installation is simple:
> git clone git://github.com/basho/rebar.git && cd rebar && ./bootstrap 

Now we have an executable file rebar in the directory - we copy (and better link) it somewhere in $ PATH. For example:
 > sudo ln -s `pwd`/rebar /usr/bin/rebar 


And here we go!
')

The simplest installation and server startup


To begin with, we will create a directory and a skeleton for our future application, rebar will help us with this. Go to somewhere where we will create the application and execute the following command:
 > mkdir webserver && cd webserver && rebar create-app appid=webserver 

The rebar create-app appid=webserver creates the skeleton of the simplest Erlang application and now our webserver directory should look like this:

The next thing we do is add a dependency on Cowboy, Sync, Mimetypes and Erlydtl. Cowboy is our web server, Sync is a utility that will allow us not to reboot our server with every change and will recompile modified modules itself when updating, Mimetypes is a library for determining whether the extension matches the mimetype (useful when we are dealing with static feedback), and Erlydtl - template engine. Create a configuration file for rebar called rebar.config:
Rebar.config content
 {deps, [ {cowboy, ".*", {git, "https://github.com/extend/cowboy.git", {branch, "master"}}}, {sync, ".*", {git, "git://github.com/rustyio/sync.git", {branch, "master"}}}, {mimetypes, ".*", {git, "git://github.com/spawngrid/mimetypes.git", {branch, "master"}}}, {erlydtl, ".*", {git, "git://github.com/evanmiller/erlydtl.git", {branch, "master"}}} ]}. 


Create a file src / webserver.erl, with the help of which we will just start and stop our server:
Src / webserver.erl content
 -module(webserver). %% API -export([ start/0, stop/0 ]). -define(APPS, [crypto, ranch, cowboy, webserver]). %% =================================================================== %% API functions %% =================================================================== start() -> ok = ensure_started(?APPS), ok = sync:go(). stop() -> sync:stop(), ok = stop_apps(lists:reverse(?APPS)). %% =================================================================== %% Internal functions %% =================================================================== ensure_started([]) -> ok; ensure_started([App | Apps]) -> case application:start(App) of ok -> ensure_started(Apps); {error, {already_started, App}} -> ensure_started(Apps) end. stop_apps([]) -> ok; stop_apps([App | Apps]) -> application:stop(App), stop_apps(Apps). 


Now the webserver: start () call will start the crypto, ranch, cowboy, webserver and auto-update applications using Sync in turn, and the webserver: stop will stop everything running in reverse order.
The cascade is ready, it's time to go to the Cowboy. Open webserver_app.erl and edit the function start / 2:
Webserver_app function: start / 2
 start(_StartType, _StartArgs) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", index_handler, []}, {'_', notfound_handler, []} ]} ]), Port = 8008, {ok, _} = cowboy:start_http(http_listener, 100, [{port, Port}], [{env, [{dispatch, Dispatch}]}] ), webserver_sup:start_link(). 


In the dispatching rules, we indicated that we will serve all requests except "/" that will be sent to the server using notfound_handler (we will give 404 error), and requests to "/" will be processed using index_handler. So you should create them:
Contents src / index_handler.erl
 -module(index_handler). -behaviour(cowboy_http_handler). %% Cowboy_http_handler callbacks -export([ init/3, handle/2, terminate/3 ]). init({tcp, http}, Req, _Opts) -> {ok, Req, undefined_state}. handle(Req, State) -> Body = <<"<h1>It works!</h1>">>, {ok, Req2} = cowboy_req:reply(200, [], Body, Req), {ok, Req2, State}. terminate(_Reason, _Req, _State) -> ok. 


Src / notfound_handler.erl content
 -module(notfound_handler). -behaviour(cowboy_http_handler). %% Cowboy_http_handler callbacks -export([ init/3, handle/2, terminate/3 ]). init({tcp, http}, Req, _Opts) -> {ok, Req, undefined_state}. handle(Req, State) -> Body = <<"<h1>404 Page Not Found</h1>">>, {ok, Req2} = cowboy_req:reply(404, [], Body, Req), {ok, Req2, State}. terminate(_Reason, _Req, _State) -> ok. 


That's all - we created a simplest web server that can handle requests to localhost : 8008 and localhost : 8008 / WHATEVER. Now it is necessary to compile and run the web server:
 > rebar get-deps > rebar compile > erl -pa ebin deps/*/ebin -s webserver 

rebar get-deps pull up dependencies from the config, rebar compile compile the code, and erl -pa ebin deps/*/ebin -s webserver will start the server itself. By the way, it's time to create a simple Makefile to facilitate the implementation of the above operations:
Makefile Content
 REBAR = `which rebar`

 all: deps compile

 deps:
	 @ ($ (REBAR) get-deps)

 compile: clean
	 @ ($ (REBAR) compile)

 clean:
	 @ ($ (REBAR) clean)

 run:
	 @ (erl -pa ebin deps / * / ebin -s webserver)

 .PHONY: all deps compile clean run


Now you can compile the project by calling make , and run by calling make run
After the server has been started, you can go first to localhost : 8008 and then to localhost : 8008 / whatever and make sure that the server is working as expected by sending “It works” to the first request and “404 Page Not Found” to the second

Routing Overview, Static Maintenance


Routing in Cowboy is not to say that the most convenient, but quite tolerable - basic features like passing parameters to the URL and validating these parameters are available. So far we have only two routes in the dispatch rules:
 {"/", index_handler, []}, {'_', notfound_handler, []} 

Which is inside of another, defining for which host we will use nested. You can read more about this and routing in general here: github.com/extend/cowboy/blob/master/guide/routing.md and here I’ll only clarify that the atom '_' means that the route will match requests to absolutely all addresses , notfound_handler is the name of the module that will process zatachenny requests, and [] is a list of additional. parameters passed to the module
We will keep statics in the priv directory in the priv / css priv / js, priv / img subdirectories and match it with the following rules:
 /css/WHATEVER -> /priv/css/WHATEVER /js/WHATEVER -> /priv/js/WHATEVER /img/WHATEVER -> priv/img/WHATEVER 

To do this, add 3 routes respectively:
 Dispatch = cowboy_router:compile([ {'_', [ {"/css/[...]", cowboy_static, [ {directory, {priv_dir, webserver, [<<"css">>]}}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}} ]}, {"/js/[...]", cowboy_static, [ {directory, {priv_dir, webserver, [<<"js">>]}}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}} ]}, {"/img/[...]", cowboy_static, [ {directory, {priv_dir, webserver, [<<"img">>]}}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}} ]}, {"/", index_handler, []}, {'_', notfound_handler, []} ]} ]). 

the mimetypes: path_to_mimes / 2 function is responsible for returning the correct mimetype by file extension.
It is easy to see that the previous 3 routes almost completely copy each other with minor exceptions, let's take out the generation of the static route into the function and replace the routes with it:
 Static = fun(Filetype) -> {lists:append(["/", Filetype, "/[...]"]), cowboy_static, [ {directory, {priv_dir, webserver, [list_to_binary(Filetype)]}}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}} ]} end, Dispatch = cowboy_router:compile([ {'_', [ Static("css"), Static("js"), Static("img"), {"/", index_handler, []}, {'_', notfound_handler, []} ]} ]). 

Now, in order for new dispatch rules to take effect, we need to either restart the server or use the cowboy function: set_env / 3
The first is unsportsmanlike, and you’re going to reboot the server for every sneeze in the routing rules, so we’ll add a function to update the routing in our webserver file so that you can call the webserver: update_routing () in the console. And, so that the webserver: update_routing / 0 function knows about new routes, we will put their definition into a separate function. As a result, the webserver_app.erl file will look as follows:
Src / webserver_app.erl content
 -module(webserver_app). -behaviour(application). %% Application callbacks -export([ start/2, stop/1 ]). %% API -export([dispatch_rules/0]). %% =================================================================== %% API functions %% =================================================================== dispatch_rules() -> Static = fun(Filetype) -> {lists:append(["/", Filetype, "/[...]"]), cowboy_static, [ {directory, {priv_dir, webserver, [list_to_binary(Filetype)]}}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}} ]} end, cowboy_router:compile([ {'_', [ Static("css"), Static("js"), Static("img"), {"/", index_handler, []}, {'_', notfound_handler, []} ]} ]). %% =================================================================== %% Application callbacks %% =================================================================== start(_StartType, _StartArgs) -> Dispatch = dispatch_rules(), Port = 8008, {ok, _} = cowboy:start_http(http_listener, 100, [{port, Port}], [{env, [{dispatch, Dispatch}]}] ), webserver_sup:start_link(). stop(_State) -> ok. 


Now add the update_routing function to the webserver.erl module:
Webserver function: update_routes / 0
 update_routes() -> Routes = webserver_app:dispatch_rules(), cowboy:set_env(http_listener, dispatch, Routes). 


And do not forget to add a function to the -export () attribute, after which it will look like this:
 %% API -export([ start/0, stop/0, update_routes/0 ]). 

execute in the webserver:update_routes(). console webserver:update_routes(). create directories for statics
 > mkdir priv && cd priv && mkdir css js img 

and put there any relevant files, after which you can verify that they are being sent, as expected, to the address localhost : 8008 / PATH / FILE

Templating with ErlyDTL (Django Template Language for Erlang)


Evan Miller, author of the notorious Chicago Boss web framework under Erlang, ported Django Template Language (https://docs.djangoproject.com/en/dev/topics/templates/) to Erlang and it turned out, quite frankly, pretty cool. Actually, I would recommend this template engine to use in your future projects - I haven’t seen any alternatives yet.
Create a new webserver / tpl directory and save there three templates:

Content tpl / layout.dtl
 <!DOCTYPE html> <html> <head> <title>Webserver</title> </head> <body> {% block content %}{% endblock %} </body> </html> 


Content tpl / index.dtl
 {% extends "layout.dtl" %} {% block content %} <h1>Hello, {{ username | default : "stranger" }}!</h1> {% endblock %} 


Content tpl / 404.dtl
 {% extends "layout.dtl" %} {% block content %} <h1>URL <span style="color:red;">{{ url }}</span> does not exists.</h1> {% endblock %} 



To use templates, they need to be compiled. This is done with erlydtl: compile / 3 as follows:
 ok = erlydtl:compile("tpl/layout.dtl", "layout_tpl", []), ok = erlydtl:compile("tpl/index.dtl", "index_tpl", []), ok = erlydtl:compile("tpl/404.dtl", "404_tpl", []). 

The final argument is a list of options for compiling a template, which you can read more about here: github.com/evanmiller/erlydtl
In order not to compile all templates with your hands each time you change, we will create functions in the webserver module that will be recompiled:
Functions for recompiling templates
 c_tpl() -> c_tpl([]). c_tpl(Opts) -> c_tpl(filelib:wildcard("tpl/*.dtl"), Opts). c_tpl([], _Opts) -> ok; c_tpl([File | Files], Opts) -> ok = erlydtl:compile(File, re:replace(filename:basename(File), ".dtl", "_tpl", [global, {return, list}]), Opts), c_tpl(Files, Opts). 


and export them:
 %% API -export([ start/0, stop/0, update_routes/0, c_tpl/0, c_tpl/1, c_tpl/2 ]). 

c_tpl / 0 will recompile all the templates from the tpl directory with no options, c_tpl / 1 will do the same, only with the specified options, and c_tpl / 2 will recompile the specified files with the specified options. Let's compile all the templates by running the webserver:c_tpl(). Erlang console webserver:c_tpl().
We will also update our rebar.config so that when compiling it also compiles the templates (thanks for the egobrain hint):
Updated rebar.config
 {plugins,[rebar_erlydtl_compiler]}. {deps, [ {cowboy, ".*", {git, "https://github.com/extend/cowboy.git", {branch, "master"}}}, {sync, ".*", {git, "git://github.com/rustyio/sync.git", {branch, "master"}}}, {mimetypes, ".*", {git, "git://github.com/spawngrid/mimetypes.git", {branch, "master"}}}, {erlydtl, ".*", {git, "git://github.com/evanmiller/erlydtl.git", {branch, "master"}}} ]}. {erlydtl_opts,[ {compiler_options, [debug_info]}, [ {doc_root, "tpl"}, {out_dir, "ebin"}, {source_ext, ".dtl"}, {module_ext, "_tpl"} ] ]}. 


Unfortunately, it did not work out for me by simple means to force Sync to pick up the changes in the templates, I’ll look into its code a bit later, so I still left the functions for recompilation in the module.

Now we edit our handlers so that they give the answer the compiled templates, and also transfer the necessary variables to the templates:

Contents src / index_handler.erl
 -module(index_handler). -behaviour(cowboy_http_handler). %% Cowboy_http_handler callbacks -export([ init/3, handle/2, terminate/3 ]). init({tcp, http}, Req, _Opts) -> {ok, Req, undefined_state}. handle(Req, State) -> {Username, Req2} = cowboy_req:qs_val(<<"username">>, Req, "stranger"), {ok, HTML} = index_tpl:render([{username, Username}]), {ok, Req3} = cowboy_req:reply(200, [], HTML, Req2), {ok, Req3, State}. terminate(_Reason, _Req, _State) -> ok. 


Src / notfound_handler.erl content
 -module(notfound_handler). -behaviour(cowboy_http_handler). %% Cowboy_http_handler callbacks -export([ init/3, handle/2, terminate/3 ]). init({tcp, http}, Req, _Opts) -> {ok, Req, undefined_state}. handle(Req, State) -> {URL, Req2} = cowboy_req:url(Req), {ok, HTML} = '404_tpl':render([{url, URL}]), {ok, Req3} = cowboy_req:reply(404, [], HTML, Req2), {ok, Req3, State}. terminate(_Reason, _Req, _State) -> ok. 


That's all. Open localhost : 8008 /? Username = world or localhost : 8008 / qweqweasdasd and rejoice that everything works exactly as we expected.

The complete project code can be found here: github.com/chvanikoff/webserver

This is the end of my story, and in the next article I will talk about how to add multilingual support to our application written today. Questions, comments, comments are welcome;)

Source: https://habr.com/ru/post/173595/


All Articles