Looking at TCP and HTTP, using Elixir (part 2)
HTTP
HTTP (the Hypertext Transfer Protocol) is just an agreed upon format for messages that are sent to and from a TCP server. Although I’m sure the actual definition of HTTP is more complicated than that.
Supporting HTTP
I’ve made some minor adjustments to our Echo server. I set up another supervisor, which allows us to spawn processes that will handle each TCP connection. This gives our server the ability to serve multiple clients at the same time. Here’s where we’re at:
# lib/echo.ex
defmodule Echo do
use Application
def start(_type, _args) do
import Supervisor.Spec
children = [
supervisor(Task.Supervisor, [[name: Echo.TaskSupervisor]]),
worker(Task, [Echo.Server, :start, [12321]])
]
opts = [strategy: :one_for_one, name: Echo.Supervisor]
Supervisor.start_link(children, opts)
end
end
# lib/echo/server.ex
defmodule Echo.Server do
@options [:binary, packet: :line, active: false, reuseaddr: true]
def start(port) do
{:ok, server} = :gen_tcp.listen(port, @options)
loop_acceptor(server)
end
defp loop_acceptor(server) do
{:ok, client} = :gen_tcp.accept(server)
{:ok, pid} = Task.Supervisor.start_child(Echo.TaskSupervisor, fn ->
read_message(client)
end)
:gen_tcp.controlling_process(client, pid)
loop_acceptor(server)
end
defp read_message(client) do
case :gen_tcp.recv(client, 0) do
{:ok, message} ->
message |> send_message(client)
read_message(client)
{:error, _} ->
:gen_tcp.close(client)
end
end
defp send_message(message, client) do
:gen_tcp.send(client, message)
end
end
Our tests still pass, and if you try out Echo in your terminal, you can now
connect multiple clients to it via telnet
and you’ll see that it will handle
them all perfectly fine.
OK great, now let’s take a look at something interesting.
Keep your Echo server running, open up another terminal and type:
$ curl http://localhost:12321
You should see something like the following:
$ curl http://localhost:12321
GET / HTTP/1.1
User-Agent: curl/7.38.0
Host: localhost:12321
Accept: */*
Using curl, we just simulated a browser request. And because our server is just echoing everything back at us, we just saw exactly what curl sent to our TCP server!
There’s a small problem in that it will currently just hang there, because a browser (and curl) expect the TCP connection to close once the response has been sent. You can press CTRL-C to exit it for now.
We can make it even cooler by making the request from our browser. But we need to make one small tweak to our Echo server in order to do this.
Once all the lines come in from the browser or curl, they will send a line that just contains a carriage return. When this line is read by our TCP server, we want to close the connection. Then our browser will display everything that has been transmitted back to it.
We can first add a test for the expected behavior:
# test/echo_test.exs
test "sending a carriage return terminates the request", %{socket: socket} do
:ok = :gen_tcp.send(socket, "Hello, world!\n")
:ok = :gen_tcp.send(socket, "Hello, world!\n")
:ok = :gen_tcp.send(socket, "\r\n")
:gen_tcp.recv(socket, 0) # Returns the first Hello, world!
:gen_tcp.recv(socket, 0) # Returns the second Hello, world!
case :gen_tcp.recv(socket, 0) do # Socket should be closed
{:error, response} ->
assert response == :closed
end
end
Then make the necessary changes to our server:
# lib/echo/server.ex
defp read_message(client) do
case :gen_tcp.recv(client, 0) do
{:ok, "\r\n"} ->
:gen_tcp.close(client)
{:ok, message} ->
message |> send_message(client)
read_message(client)
{:error, _} ->
:gen_tcp.close(client)
end
end
Now go to http://localhost:12321 in your browser and take a look! You will see the exact request your browser made (including all headers it sent), printed out in your browser window.
Concluding
Now that you’ve seen what HTTP requests look like, how to reply to them, you can pretty much do anything at this point. This is far from a complete HTTP server though. Some things to try:
- Send something other than an echo back to the browser (maybe a file)
- Change the content sent back based on the request path (the first line that gets sent in the request contains the path)
- Send a different HTTP status code back to the browser
- Create a Router to handle your requests