As I was going over my HTTP/2 in Erlang talk for StrangeLoop, Sean Cribbs showed me a way to listen asynchronously for a socket. It's a pretty clean method, but I had to do some digging in order to get it working, because it uses the prim_inet module, which isn't very well documented. I think that might be because we're not supposed to use it. Oops.

So, suppose you've already opened a socket with gen_tcp:listen. It'a actually very important that you use gen_tcp:listen and not ssl:listen. We'll upgrade to SSL later if that's important to you, and it should be.

{ok, Ref} = prim_inet:async_accept(ListenSocket, -1),

First of all, Ref isn't a reference(). Sorry about that. I actually have no idea what it is, but it'll be the same value that we bind to Ref again later. When's later? When a client tries to connect. When that happens you'll get a process message that looks like this:

{inet_async, ListenSocket, Ref, {ok, CliSocket}}

ListenSocket is the same one you passed in. Ref, like I mentioned, is the same value that was returned from prim_inet:async_accept/2. It's {ok, CliSocket} that we want to work with.

We're certainly not done. When you try to perform any gen_tcp functions on CliSocket, it all explodes. gen_tcp:accept and ssl:accept regester the port as a socket with the inet_db for you, but since we went around those, we're going to have to register it manually:

inet_db:register_socket(CliSocket, inet_tcp),

Great. Now all these gen_tcp options work! But, you'll need to do a little more if you want this to be a SSL connection.

{ok, AcceptSocket} = ssl:ssl_accept(CliSocket, SSLOptions),

Once this is done, you can perform ssl functions on the socket.

How I Used It

In chatterbox, each http2_connection FSM started like this:

  • Supervisor passes an open socket (started with either ssl:listen or gen_tcp:listen) to init/1
  • In order to not hang in the init/1 callback, add the Listener to the FSM state and transition to an accept state with a 0 timeout
  • use a case statement to call either gen_tcp:accept/1 or ssl:transport_accept/1, and hang waiting on a connection.

That's how it was before this commit when I moved over to the prim_inet solution. Now it works more like:

  • Supervisor still passes an open socket, but now it's always opened with gen_tcp:listen
  • init/1 calls prim_inet:async_accept/2
  • The half of my accept/2 callback was refactored into handle_info/2, because now it's reacting to a message from prim_inet, and not a 0 timeout.
  • The remaining half of my accpet/2 callback was renamed to handshake/2 to more accurately reflect it's role in the HTTP/2 spec.

This way, my FSM is no longer blocking on gen_tcp:accpet/1 or ssl:transport_accept/1, and can now receive messages. Now, I don't actually need to receive messages in this particular case, but it seems cleaner overall.