Гнездо эликсира дублирует STX и ETX в ответ - PullRequest
0 голосов
/ 25 июня 2019

У меня есть сервер сокетов с этим методом:

@impl true
def handle_call({:tcp, socket, packet}, state) do
  Logger.info("Received packet: \x02#{packet}\x03 and send response")
  {:reply, {:ok, packet}, state}
end

Я написал скрипт на python, который отправляет "\ x02Test \ x03" в сокет:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", port))
s.send("\x02Test\x03".encode())
print(s.recv(1024))

Но ответ, напечатанный python: b '\ x02 \ x02Test \ x03 \ x03'

1 Ответ

0 голосов
/ 26 июня 2019

Какое отношение handle_call() имеет к gen_tcp?

gen_tcp можно настроить для чтения из сокета и send() сообщений в любой процесс, называемый :gen_tcp.accept(), так называемый управляющий процесс , который может быть gen_server;но gen_server обрабатывает сообщения, отправленные в его почтовый ящик с handle_info() - не handle_call().handle_call() обрабатывает сообщения, отправленные :gen_server.call().

Вот пример :

defmodule TcpServer do
  use GenServer
  require Logger

  def start_link() do
    ip = Application.get_env :gen_tcp, :ip, {127,0,0,1}
    port = Application.get_env :gen_tcp, :port, 6666
    IO.puts "gen_tcp is listening on port: #{port}"
    GenServer.start_link(__MODULE__, {ip, port},[])
  end

  def init({ip, port}) do
    {:ok, listen_socket}= :gen_tcp.listen(
      port,
      [:binary, {:packet,0}, {:active,true}, {:ip,ip}]
    )

    {:ok, socket } = :gen_tcp.accept listen_socket
    {:ok, %{ip: ip, port: port, socket: socket} }
  end

  def handle_call({:tcp, _socket, packet}, state) do
    Logger.info("handle_call(): Received packet: #{inspect packet}")
    {:reply, {:ok, packet}, state}
  end

  def handle_info({:tcp,socket,packet},state) do
      Logger.info "handle_info(:tcp, ...): incoming packet: #{inspect packet}"
      :gen_tcp.send(socket, "****#{packet}*****")
      {:noreply,state}
  end   
  def handle_info({:tcp_closed, _socket}, state) do
    Logger.info("handle_info({:tcp_closed, ...): Client closed socket.")
    {:noreply, state}
  end   
  def handle_info({:tcp_error, socket, reason}, state) do
    Logger.info("Connection closed due to #{reason}: #{socket}")
    {:noreply,state}
  end

end

Для запуска сервера:

~/elixir_programs/tcp_server$ iex -S mix
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.8.2) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> TcpServer.start_link()
gen_tcp is listening on port: 6666

В другом окне терминала:

~/python_programs$ cat 5.py

import socket

port = 6666
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", port))
s.send("\x02Test\x03".encode())
print(s.recv(1024))

~/python_programs$ p36 5.py 
b'****\x02Test\x03*****'

~/python_programs$ 

Как видите, не было дублирования символов \x02 и \x03.

Вернитесь в окно сервера:

{:ok, #PID<0.123.0>}
iex(2)> 
21:54:18.363 [info]  handle_info(:tcp, ...): incoming packet: <<2, 84, 101, 115, 116, 3>>

21:54:18.369 [info]  handle_info({:tcp_closed, ...): Client closed socket.

У вас есть другой процесс, который контролирует процесс и вызывает :gen_server.call({:tcp, socket, packet})?

Кстати, в этом коде:

def handle_info({:tcp,socket,packet},state) do

packet может быть целым пакетом, 1/2 пакета или 1/10 пакета.Так работают сокеты.Опция конфигурации {packet, 0} сообщает gen_tcp, что на передней части пакета нет заголовка длины (0 байт), а {packet, 1|2|4} сообщает gen_tcp, что длина пакета содержится в первом байте, первом2 байта или первые 4 байта соответственно.Таким образом, gen_tcp может прочитать первые 1|2|4 байтов, чтобы получить длину пакета, скажем, L, затем продолжить чтение из сокета, пока он не получит L байтов.Затем gen_tcp упаковывает части в одно сообщение и отправляет все сообщение в gen_server.С другой стороны, когда вы указываете {packet, 0}, вы говорите gen_tcp, что пакет не имеет заголовка длины;и поскольку сокет разделяет один пакет на неопределенное количество порций, gen_tcp не знает, где находится конец пакета, поэтому единственная опция gen_tcp - это чтение порции из сокета и отправка порциисерверу gen_server;затем прочитайте другой кусок и отправьте его на gen_server и т. д., и т. д. Это означает, что gen_server должен выяснить, где находится конец пакета.

Следовательно, ваш сервер и ваш клиент должны договориться опротокол для оповещения об окончании пакета;и handle_info(:tcp, ...) придется хранить фрагменты пакета в state (или в дБ) до тех пор, пока он не прочитает все фрагменты, составляющие пакет.

Один протокол, который вы можете использовать дляСигналом окончания пакета является: клиент закрывает сокет.В этом случае будет вызываться

def handle_info({:tcp_closed, _socket}, state)

, и внутри этого функционального предложения вы можете собрать фрагменты, хранящиеся в state (или в db), в законченное сообщение, а затем сделать все, что необходимо, например,отправьте сообщение обратно клиенту.

Если вы используете STX и ETX в качестве начального сообщения, протокола конечного сообщения, тогда handle_info(:tcp, ...) все равно придется искать символ STX для сигналачто он должен начать хранить чанки в state, а когда handle_info(:tcp, ...) находит чанк с символом ETX, вам нужно собрать все сообщение.

...