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