This post is a part of my upcoming book on Phoenix 1.3.
By the end of this post you will learn
Cowboy is a web server built in Erlang. It's a production ready server and not just a handy tool for development environment like webrick
. Currently Cowboy is the only supported Erlang web server by Phoenix. In this post, we shall see how to run a simple app using just Cowboy in your Elixir app. Understanding how Cowboy works and knowing the functionalities that it gives can form a strong foundation of understanding how Phoenix works and its internals.
Let's start with creating a new Elixir app that will print "Hello World!" when you visit http://localhost:8080
.
mix new my_elixir_web
Open up mix.exs
and add Cowboy as a dependency.
# mix.exs
defp deps do
[
{:cowboy, "~> 1.0.0"},
]
end
Now run mix do deps.get, compile
Once the downloading of dependencies and compilation is complete, run iex shell using mix.
iex -S mix
This starts iex
shell with all the compiled code of our application and its dependencies loaded. We don't have any code in our application yet but we do have mentioned :cowboy
as a dependency and we want it to be compiled and loaded in our iex shell.
Once in iex
shell, define the following module and run the code below. I admit that the code is quite cryptic but don't worry, I am going to explain it shortly. So go ahead and copy paste the following code in your iex
shell.
defmodule CowboyHandler do
def init(_type, req, _opts) do
{:ok, req, :nostate}
end
def handle(request, state) do
{ :ok, reply } = :cowboy_req.reply(
200, [{"content-type", "text/html"}], "<h1>Hello World!</h1>", request
)
{:ok, reply, state}
end
def terminate(_reason, _request, _state), do: :ok
end
dispatch_config = :cowboy_router.compile([
{ :_,
[
{:_, CowboyHandler, []},
]
}
])
:cowboy.start_http(:http, 100,[{:port, 8080}],[{ :env, [{:dispatch, dispatch_config}]}])
Now open up http://localhost:8080
and behold the glamorous text of every programmer.
To understand what we just did, we need to know more about Cowboy server. Cowboy is an Erlang web server similar to Nginx or Apache. However there are quite a few important differences.
A successful configuration of a Cowboy server to provide a response involves
This is the most cryptic part of all, but understanding it is not as difficult as it seems.
# Code that maps any path to CowboyHandler module.
dispatch_config = :cowboy_router.compile([
{ :_,
[
{:_, CowboyHandler, []},
]
}
])
Let's crack it from the center. {:_, CowboyHandler, []}
is Cowboy's way of saying for any given path, use the module CowboyHandler
as the handler and pass the value []
.
{:_, # --> matches any path. Cowboy uses :_ as wildcard
CowboyHandler, # --> Module that handles Cowboy request
[], # --> Arguments that go to the handler.
}
So each path in Cowboy is a tuple with three elements of the format {:path, handler, args}
. Since an application might need to handle multiple paths, Cowboy wants the paths information as a list.
# eg., for multiple paths
[
{"/", PageHandler, args},
{"/about", AboutPageHandler, args},
{"/contact", ContactPageHandler, args},
{:_, Error404Handler, args}
]
Our simple Cowboy application, we need only one wildcard path as we want to display a static message to all paths.
[
{:_, CowboyHandler, []},
]
Then to understand further, let's go a level above the paths in our dispatch_config.
{ :_,
[
{:_, CowboyHandler, []},
]
}
# let me rewrite it as
{ :_,
path_list # where path_list is a list of of paths as seen above.
}
This outer layer of tuple contains the host information. Again :_
is a wildcard but matching any host. So the host layer tuple is of the format {host, path_list}
. Since there can be multiple hosts per machine, we provide a list here again. Following example makes the entire routing structure clear.
Imagine we have two different domains sub1.example.com
and sub2.example2.com
pointed to the same machine. On sub1.example1.com
we need /
and /about
pages, and on sub2.example.com
we need /
and /contact
pages. A configuration for such a system will look this:
[{
"sub1.example.com",
[{ "/",
Sub1.HomePageHandler,
[]
},
{ "/about",
Sub1.AboutPageHandler,
[]
}
]
},
{
"sub2.example.com",
[{ "/",
Sub2.HomePageHandler,
[]
},
{ "/contact",
Sub2.ContactPageHandler,
[]
}
]
},
]
To compile our router, all that you need to do is call :cowboy_router.compile/1
with our list of routes. Compiling of routes enables Cowboy to match routes more efficiently.
routes_definition = [{host, path_list}]
compiled_router = :cowboy_router.compile(routes_definition)
The handler modules need to define the following three functions to be eligible to handle a Cowboy connection. In our CowboyHandler module above, we have defined the following three functions. Without any one of these three functions defined in your Handler module, Cowboy will not start.
def init({transport, protocol}, request, opts) do
{:ok, request, state}
end
def handle(request, state) do
{:ok, response, state}
end
def terminate(reason, request, state) do
:ok
end
The main function where the response goes out is the handle/2
and it needs to send out the response to Cowboy process by calling
:cowboy_req.reply/4
.
# template
:cowboy_req.reply(status_code, headers, body, request)
# example
:cowboy_req.reply(200, [{"content-type", "text/html"}], "<h1>Hello World</h1>", request
)
Nginx or Apache web servers are started by the host operating system. So when you deploy a PHP or Rails app, you application code has nothing to do with Nginx or Apache. You application does not start or stop Nginx server and you don't run an instance of Nginx for each of your application. If you have 10 PHP applications, you don't run 10 separate copies of Nginx for these 10 web applications.
With Cowboy, you web application need to start it as part of your application booting process and has to kill it when your application stops.
The below code that you have run already is responsible for starting Cowboy server.
:cowboy.start_http(:http, 100,[{:port, 8080}], [...omitted])
The code is starting 100 processes of Cowboy to handle multiple request and is listening at port 8080.
To start the Cowboy server with our compiled routes, we need more details apart from the compiled router.
Are you using http or https? How many Cowboy processes do you need to start? Which port does Cowboy listen for request?
:cowboy.start_http/4
function starts the Cowboy server with all required configuration.
:cowboy.start_http(ref_atom, pool_size, tcp_opts, cowboy_args)
ref_atom
- you can provide any atom. Ranch, a dependency of Cowboy makes use of this atom to name the process that it manages.pool_size
- how many Cowboy process do you need for your application to handle multiple concurrent requests?tcp_opts
- there are many tcp options that you can configure when you start the server. One such is configuring the port number on which your server listens for request. Since there are many options, the data type is a list of tuple with each tuple configuring one tcp option.cowboy_args
- these are data that are passed to Cowboy for managing a request. It contains the routing information.With this above knowledge, we can now configure our Cowboy server:
:cowboy.start_http(:http, 100,[{:port, 8080}],[{ :env, [{:dispatch, dispatch_config}]}])
To recap, our entire code looks like this to display the simple "Hello World!" message. This entire code has to be run on iex -S mix
shell.
# Our handler module
defmodule CowboyHandler do
def init(_type, req, _opts) do
{:ok, req, :nostate}
end
def handle(request, state) do
# Sending reply to the browser
{ :ok, reply } = :cowboy_req.reply(
200, [{"content-type", "text/html"}], "<h1>Hello World!</h1>", request
)
{:ok, reply, state}
end
def terminate(_reason, _request, _state), do: :ok
end
# Configuring and compiling router
dispatch_config = :cowboy_router.compile([
{ :_,
[
{:_, CowboyHandler, []},
]
}
])
# Starting the Cowboy server with our dispatch_config
:cowboy.start_http(:http, 100,[{:port, 8080}],[{ :env, [{:dispatch, dispatch_config}]}])
We could improve on this by having the code saved in a module file and starting the Cowboy server as part of our app starting process. I leave it as an exercise for you to handle it.
Can you build your app entirely on Cowboy using the method described above? If so what do you need?
Ok. Solutions exist but do you want to take that route? Unless you are venturing out to build another Phoenix framework or you are a masochist, using Phoenix library is the right choice to focus on getting things done and to keep your sanity level in check.
Here is a non-exhaustive list of what Phoenix provides in comparison with Cowboy?
That brings us to the end of a long post. Hope you enjoyed it. If you have any questions please feel free to comment below and I will do my best to answer them all.