dnlgrv

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