Gen_tcp accept socket across processes gives `:einval`

Hi everyone,

I’m implementing something that needs to read a TCP stream for the first time and trying to get my head around gen_tcp. I start a GenServer to handle the connection and want to be able to make other calls to it so I thought spawning the blocking :gen_tcp.accept call would be efficient but it seems the socket doesn’t survive being transferred between processes. Does anyone know why? Or if I’m making an elementary mistake here?

Here’s a minimal script that demonstrates the problem:

# testtcp.exs
{:ok, socket} = :gen_tcp.listen(0, [])
{:ok, port} = :inet.port(socket) |> dbg()
current = self()
# Spawn a process just for accepting connections and send the new connection back to use when it
# happens.
spawn(fn ->
  case :gen_tcp.accept(socket) do
    {:ok, conn} ->
      send(current, {:new_conn, conn} |> dbg())

# Spawn to connect to the socket.
spawn(fn ->
  dbg(:gen_tcp.connect('localhost', port, [], :infinity))

receive do
  {:new_conn, conn} ->
    dbg({:received_conn, conn})

Which outputs:

[tcptest.exs:3: (file)]
:inet.port(socket) #=> {:ok, 40015}

[tcptest.exs:8: (file)]
:inet.sockname(conn) #=> {:ok, {{127, 0, 0, 1}, 40015}}

[tcptest.exs:14: (file)]
:gen_tcp.connect('localhost', port, [], :infinity) #=> {:ok, #Port<0.7>}

[tcptest.exs:9: (file)]
{:new_conn, conn} #=> {:new_conn, #Port<0.8>}

[tcptest.exs:19: (file)]
{:received_conn, conn} #=> {:received_conn, #Port<0.8>}

[tcptest.exs:20: (file)]
:inet.sockname(conn) #=> {:error, :einval}

(there’s no way to add line numbers to code blocks here right?)

You’ll notice the dbg output is slightly out of order, but it shows the change between calling :inet.sockname in the same process that accepted the socket (valid) and in the receive loop ({:error, :einval}).

Thanks for any help!

Indeed, the data for a socket has a meaning which is local (to a process) and so sending it to another process in a message is useless. If you want a socket to be “sent” to another process, it has to be done when spawning (as you do in the first spawn of your example).

Thanks for the answer! May I ask how you know that? Is it an innate trait of the BEAM or perhaps documented somewhere I didn’t look? Just want to understand these kinds of limitations as I thought everything could be passed between processes. Though I realise that between nodes some things won’t work.

Good question :slight_smile: I “know” it mostly by analogy with Unix processes, where sockets are just indexes to a local-to-the-process table.

1 Like

There’s the concept of a “controlling process” in gen_tcp that makes sending socket references between processes a little tricky.


The issue you’re encountering is simpler: a common cause of sockname returning EINVAL is if the socket has already shut down.

Here’s my read of how things happen in your code:

  • main process calls :gen_tcp.listen
  • first spawned process calls :gen_tcp.accept and blocks
  • second spawned process calls :gen_tcp.connect and blocks
  • first spawned process wakes up, prints out sockname, and sends conn to the main process
  • Then there’s a race between the three processes:
    • the second spawned process is now connected, gen_tcp.connect unblocks and the SECOND PROCESS EXITS. One end of the TCP connection starts shutting down
    • the FIRST PROCESS EXITS, taking the other end of the TCP connection down
    • the main process tries to call sockname on conn, which is shut down or shutting down

A quick way to check for conditions like this is to add Process.sleep(10000) (or your favorite large number) at the end of each function passed to spawn, which forces the processes to stay alive longer. Doing that in your example results in the code working.

Ah ha, great explanation of what’s happening, thank you! :bowing_man:

For completeness, it looks like one way to get this to work on the accepter side is to use :gen_tcp.controlling_process/2 before send/2 with the value of the destination process.