Отправка сообщений с tcp-сервера в Elixir на tcp-клиент в рамках открытого соединения

Я разработал сервер TCP в Phoenixframework, используя реализацию Erlang : модуль gen_tcp.

Я могу запустить сервер, вызвав :gen_tcp.listen(port), который затем прослушивает новые соединения на этом порту.

Один клиент — автоматизированная система комплектования для аптек (по сути, автоматизированный робот для выдачи лекарств).

Таким образом, как TCP-клиент, робот может открыть соединение с моим TCP-сервером. Сервер прослушивает новые сообщения робота с помощью метода handle_info-обратного вызова, а также может ответить клиенту в рамках этого запроса (:gen_tcp.send).

Проблема, с которой я столкнулся, заключается в том, что я понятия не имею, как использовать это соединение и отправлять данные обратно роботу без запроса клиента.

Поскольку робот является tcp-клиентом (компания, стоящая за роботом, говорит, что в настоящее время робот не может действовать как сервер), у него нет открытого порта/адреса сервера робота, на который я мог бы отправлять сообщения. Поэтому я должен использовать уже установленное соединение, инициализированное клиентом.

Настройка

Pharma_ui > Pharma_api (Phoenix) > robot (программное обеспечение поставщика)

Рабочий процесс:

  1. робот инициализирует подключение к API через TCP
  2. робот отправляет информацию о статусе в API и получает ответ
  3. в какой-то момент (см. обновление 1) API должен отправить роботу запрос на выдачу (используя соединение, инициализированное в #1)

Шаги 1 и 2 работают, часть 3 — нет.

Это выглядит как довольно простая проблема с tcp-соединениями в Elixir/Phoenix, но любой намек в правильном направлении приветствуется :)

На данный момент я придумал эту реализацию (на основе этого сообщения в блоге):

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

Обновление 1:

В какой-то момент = пользователь (например, фармацевт) размещает заказ в пользовательском интерфейсе. Интерфейс инициирует публикацию в API, а API обрабатывает публикацию в OrderController. OrderController должен преобразовать заказ (чтобы его понял робот) и передать его на TcpServer, который держит соединение с роботом. Этот рабочий процесс будет происходить много раз в день.


person Pascal    schedule 14.09.2016    source источник
comment
Можете ли вы привести пример в какой-то момент? Кто инициирует это действие? Короткий ответ: вы можете создать процесс (например, другой GenServer), который хранит сокет, ждет триггера, а затем отправляет ответ, если вы хотите, чтобы TcpServerClean мог обрабатывать более одного клиента одновременно.   -  person Dogbert    schedule 14.09.2016
comment
@Dogbert: я обновляю свои вопросы, чтобы привести пример того, что может быть «в какой-то момент». Не могли бы вы привести краткий пример вашего ответа, если он все еще действителен после моего обновления? Спасибо за вашу помощь!   -  person Pascal    schedule 15.09.2016


Ответы (1)


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

_sock — это сокет, который вы не используете. Но это сокет, на который вы действительно можете отправлять данные. т.е. :gen_tcp.send(_sock, data) будет отправлять данные вашему роботу. Вам нужно будет убедиться, что вы отслеживаете этот сокет на предмет отключений, и убедитесь, что у вас есть доступ к нему для последующего использования. Это означает, что вам нужно создать процесс, который владеет этим сокетом и содержит ссылку на сокет, чтобы ваш серверный код мог отправлять данные в сокет в более поздний момент времени. т.е. проще всего было бы создать gen_server.

Однако то, что вы делаете, — это создание собственного кода акцептора. Существует реализация пула акцепторов, которая уже широко используется. Оно называется ранчо (https://github.com/ninenines/ranch). Вы можете использовать это вместо того, чтобы сворачивать свое собственное. В нем есть положения для гораздо более оптимального способа сделать это, чем то, что у вас есть. Например, он создает пул акцепторов. Это также позволит лучше абстрагироваться от gen_server, который просто отвечает за связь с роботом и вообще не беспокоится о сокетах прослушивателя.

person ash    schedule 14.09.2016
comment
Спасибо за ссылку на ранчо, не знал про код акцептора! Я думаю, что первая часть вашего ответа направлена ​​в том же направлении, что и Дагоберт в своем комментарии. Я попробую это и вернусь к вам. Я обновляю свой вопрос, чтобы привести пример того, что означает «в какой-то момент», просто чтобы убедиться, что это не изменит ваш ответ. Благодарю вас! - person Pascal; 15.09.2016