0

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.

Pascal
  • 1,984
  • 1
  • 21
  • 25
  • 1
    Can you give an example of "at some point"? Who will trigger this action? A short answer would be you can spawn a process (like a different `GenServer`) which stores the socket, waits for the trigger, and then sends a response, if you want `TcpServerClean` to be able to handle more than one client concurrently. – Dogbert Sep 14 '16 at 15:55
  • @Dogbert: I update my questions to give an example of what “at some point” might be. Would you mind and give a short example of you answer if it is still valid after my update? Thanks for your help! – Pascal Sep 15 '16 at 07:22

1 Answers1

2
{ :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.

ash
  • 711
  • 4
  • 8
  • Thanks for the link to ranch, didn’t know about acceptor code! I think the first part of your answer heads in the same direction what Dagobert suggested in his comment. I’ll try that and get back to you. I update my question to give an example of what “at some point” means just to be sure this would not change your answer. Thank you! – Pascal Sep 15 '16 at 07:23