Search code examples
socketstcpelixirphoenix-frameworkgen-tcp

Sending messages from a tcp server in Elixir to a tcp client within an open connection


I developed a TCP server in the Phoenixframwork by using an implementation of the Erlang :gen_tcp module.

I can start the server by calling :gen_tcp.listen(port) which then listens for new connections on this port.

One client is an automated picking system for pharmacies (basically an automated drug dispense robot).

So as a tcp client the robot is able to open a connection to my tcp server. The Server listens for new messages by the robot via the handle_info-callback method and is also able to respond to the client within this request (:gen_tcp.send).

The problem I am facing is that I have no idea how I would use this connection and send data back to the robot without a client request.

Since the robot is a tcp client (the company behind the robot says that there is currently no way that the robot could act as a server) there is no open port / robot server address I could send messages to. So I have to use the already established connection initialized by the client.

Setup

pharmacy_ui > pharmacy_api (Phoenix) > robot (vendor software)

Workflow:

  1. robot initializes a connection to api via tcp
  2. robot sends status information to api and gets a response
  3. at some point (see update 1), the api has to send a dispense request to the robot (by using the connection initialized in #1)

Step 1 and 2 work, part 3. doesn't.

This looks like a rather simple problem about tcp connections in Elixir/Phoenix, but any hint in the right direction is highly appreciated :)

So far I came up with this implementation (based on this blog post):

defmodule MyApi.TcpServerClean do
  use GenServer

  defmodule State do
    defstruct port: nil, lsock: nil, request_count: 0
  end

  def start_link(port) do
    :gen_server.start_link({ :local, :my_api }, __MODULE__, port, [])
  end

  def start_link() do
    start_link 9876 # Default Port if non provided at startup
  end

  def get_count() do # test call from my_frontend
    :gen_server.call(:my_api, :get_count)
  end

  def stop() do
    :gen_server.cast(:my_api, :stop)
  end

  def init (port) do
    { :ok, lsock } = :gen_tcp.listen(port, [{ :active, true }])
    { :ok, %State{lsock: lsock, port: port}, 0 }
  end

  def handle_call(:get_count, _from, state) do
    { :reply, { :ok, state.request_count }, state }
  end

  def handle_cast(:stop , state) do
    { :noreply, state }
  end

  # handles client tcp requests
  def handle_info({ :tcp, socket, raw_data}, state) do
    do_rpc(socket, raw_data) # raw_data = data from robot
    { :noreply, %{ state | request_count: state.request_count + 1 } } # count for testing states
  end

  def handle_info(:timeout, state) do
    { :ok, _sock } = :gen_tcp.accept state.lsock
    { :noreply, state }
  end

  def handle_info(:tcp_closed, state) do
    # do something
    { :noreply, state }
  end

  def do_rpc(socket, raw_data) do
    try do
      # process data from robot and do something with it
      resp = "My tcp server response ..." # test
      :gen_tcp.send(socket, :io_lib.fwrite(resp, []))
    catch
      error -> :gen_tcp.send(socket, :io_lib.fwrite("~p~n", [error]))
    end
  end
end

Update 1:

At some point = A user (e.g. pharmacist) places an order at the ui frontend. Frontend triggers a post to the api and the api handles the post in the OrderController. OrderController has to transform the order (so that robot understands it) and passes it to the TcpServer which holds the connection to the robot. This workflow will happen many times per day.


Solution

  • { :ok, _sock } = :gen_tcp.accept state.lsock
    

    _sock is the socket you do not use. But it is the socket that you can actually send data on. I.e. :gen_tcp.send(_sock, data) will be pushing data to your robot. You will need to make sure your are monitoring this socket for disconnects, and make sure you have access to it for later use. That means you need to create a process that owns that socket and contains reference to the socket so that you server code can send data to the socket at a later point in time. I.e. the simplest thing to do would be to create gen_server.

    However, what you are doing is creating your own acceptor code. There is an acceptor pool implementation that is widely used already. It is called ranch (https://github.com/ninenines/ranch). You can use that instead of rolling your own. It has provisions for a lot more optimal way of doing this than what you have. For example it creates a pool of acceptors. It also will allow for better abstraction of gen_server that is just responsible for communicating to the robot and not worry about listener sockets at all.