Пример клиент-сервера Erlang с использованием gen_tcp ничего не получает

Я пытаюсь получить данные на стороне клиента, но ничего не получается.

Код сервера, который отправляет сообщение

client(Socket, Server) ->
  gen_tcp:send(Socket,"Please enter your name"),
  io:format("Sent confirmation"),
  {ok, N} = gen_tcp:recv(Socket,0),
  case string:tokens(N,"\r\n") of
    [Name] ->
      Client = #client{socket=Socket, name=Name, pid=self()},
      Server ! {'new client', Client},
      client_loop(Client, Server)
  end.

Клиент, который должен получить и распечатать

client(Port)->
   {ok, Sock} = gen_tcp:connect("localhost",Port,[{active,false},{packet,2}]),
   A = gen_tcp:recv(Sock,0),
   A.

person Marko Taht    schedule 13.05.2017    source источник
comment
Можете ли вы создать для этого MCVE? В размещенном коде отсутствует определение нескольких функций, код, который порождает сервер и принимает соединения и т. д.   -  person Dogbert    schedule 13.05.2017
comment
мой код в основном таков. www2.erlangcentral.org/wiki/?title=Chatserver Основная рабочая механика — это то же самое, только разница в функции цикла, где я вызываю некоторые другие функции, чтобы делать что-то.   -  person Marko Taht    schedule 13.05.2017


Ответы (1)


Я думаю, что ваш клиент неисправен, потому что он указывает:

{packet, 2}

но сервер указывает (в коде не показано):

{packet, 0}

В Programming Erlang (2nd) на с. 269 ​​говорится:

Обратите внимание, что аргументы packet, используемые клиентом и сервером, должны быть согласованы. Если бы сервер был открыт с {packet,2}, а клиент с {packet,4}, то ничего не работало бы.

Следующий клиент может успешно получать текст с сервера:

%%=== Server:  {active,false}, {packet,0} ====

client(Port) ->
    {ok, Socket} = gen_tcp:connect(
        localhost, 
        Port, 
        [{active,false},{packet,0}]
     ),

    {ok, Chunk} = gen_tcp:recv(Socket, 0),
    io:format("Client received: ~s", [Chunk]),
    timer:sleep(1000),

    Name = "Marko",
    io:format("Client sending: ~s~n", [Name]),
    gen_tcp:send(Socket, Name),

    loop(Socket).

loop(Socket) ->
    {ok, Chunk} = gen_tcp:recv(Socket, 0),
    io:format("Client received: ~s~n", [Chunk]),
    loop(Socket).

Однако я думаю, что и сервер чата, и мой клиент имеют серьезные проблемы. Когда вы отправляете сообщение через соединение TCP (или UDP), вы должны предположить, что сообщение будет разделено на неопределенное количество фрагментов, каждый из которых имеет произвольную длину. Когда указано {packet,0}, я думаю, что recv(Socket, 0) будет читать только один фрагмент из сокета, а затем возвращаться. Этот фрагмент может быть целым сообщением или только частью сообщения. Чтобы гарантировать, что вы прочитали все сообщение из сокета, я думаю, вам нужно перебрать recv():

get_msg(Socket, Chunks) ->
    Chunk = gen_tcp:recv(Socket, 0),
    get_msg(Socket, [Chunk|Chunks]).

Тогда возникает вопрос: как узнать, что вы прочитали все сообщение, чтобы можно было разорвать цикл? {packet,0} указывает Erlang не добавлять заголовок длины к сообщению, так как же узнать, где находится конец сообщения? Будут ли еще фрагменты, или recv() уже прочитал последний фрагмент? Я думаю, что маркер конца сообщения - это когда другая сторона закрывает сокет:

get_msg(Socket, Chunks) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Chunk} ->
            get_msg(Socket, [Chunk|Chunks]);
        {error, closed} ->
            lists:reverse(Chunks);
        {error, Other} ->
            Other
    end.

Но это поднимает другую проблему: если чат-сервер зацикливается на recv(), ожидая сообщения от клиента, и после того, как клиент отправляет сообщение на сервер, клиент зацикливается на recv(), ожидая сообщения от сервера, и обеим сторонам нужно, чтобы другая сторона закрыла сокет, чтобы выйти из их циклов recv(), тогда вы получите тупик, потому что ни одна из сторон не закрывает свой сокет. В результате клиенту придется закрыть сокет, чтобы сервер чата вышел из своего цикла recv() и обработал сообщение. Но тогда сервер не может отправить() что-либо обратно клиенту, потому что клиент закрыл сокет. В результате я не знаю, можете ли вы установить двустороннюю связь, когда указано {packet,0}.

Вот мои выводы о {packet, N} и {active, true|false} после прочтения документации и поиска:


отправить():

Когда вы звоните send(), никакие данные* фактически не передаются адресату. Вместо этого send() блокируется до тех пор, пока пункт назначения не вызовет recv(), и только после этого данные передаются в пункт назначения.

* В "Программирование Erlang (2-й)", на с. 176 говорится, что небольшое количество данных будет отправлено в место назначения при вызове send() из-за того, как ОС буферизует данные, и после этого send() будет блокироваться до тех пор, пока recv() не доставит данные в место назначения.


Параметры по умолчанию:

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

Defaults = inet:getopts(Socket, [mode, active, packet]),
io:format("Default options: ~w~n", [Defaults]).

--output:--
Default options: {ok,[{mode,list},{active,true},{packet,0}]}

Вы можете использовать inet:getopts(), чтобы показать, что gen_tcp:accept(Socket) возвращает сокет с теми же параметрами, что и Socket.


                    {active, true}   {active,false}
                   +--------------+----------------+
{packet, 1|2|4}:   |    receive   |     recv()     |
                   |    no loop   |     no loop    |
                   +--------------+----------------+
{packet, 0|raw}:   |    receive   |     recv()     |
  (equivalent)     |    loop      |     loop       |
                   +--------------+----------------+     

{активный, ложный}

Сообщения не попадают в почтовый ящик. Этот параметр используется для предотвращения переполнения клиентами почтового ящика сервера сообщениями. Не пытайтесь использовать блок приема для извлечения TCP-сообщений из почтового ящика — их не будет. Когда процесс хочет прочитать сообщение, ему необходимо прочитать сообщение непосредственно из сокета, вызвав recv().

{пакет, 1|2|4}:

Кортеж пакета указывает протокол, которому каждая сторона ожидает, что сообщения будут соответствовать. {packet, 2} указывает, что каждому сообщению будут предшествовать два байта, которые будут содержать длину сообщения. Таким образом, получатель сообщения будет знать, как долго продолжать чтение из потока байтов, чтобы достичь конца сообщения. Когда вы отправляете сообщение через TCP-соединение, вы понятия не имеете, на сколько фрагментов оно будет разделено. Если получатель перестает читать после одного фрагмента, возможно, он не прочитал все сообщение. Следовательно, получателю нужен индикатор, чтобы сообщить ему, когда все сообщение было прочитано.

С {packet, 2} получатель будет читать два байта, чтобы получить длину сообщения, скажем, 100, затем получатель будет ждать, пока он не прочитает 100 байтов из фрагментов байтов произвольного размера, которые передаются получателю.

Обратите внимание, что когда вы вызываете send(), erlang автоматически вычисляет количество байтов в сообщении и вставляет длину в N байт, как указано {packet, N}, и добавляет сообщение. Точно так же, когда вы вызываете recv(), erlang автоматически считывает N байт из потока, как указано {packet, N}, чтобы получить длину сообщения, затем recv() блокирует, пока не прочитает длину байтов из сокета, затем recv() возвращает все сообщение целиком.

{пакет, 0 | необработанный (эквивалент):

Когда указано {packet, 0}, recv() будет считывать количество байтов, указанное его аргументом длины. Если длина равна 0, то я думаю, что recv() прочитает из потока один фрагмент, который будет произвольным количеством байтов. В результате комбинация {packet, 0} и recv(Socket, 0) требует, чтобы вы создали цикл для чтения всех фрагментов сообщения, а индикатор для recv() прекращения чтения, поскольку он достиг конца сообщения, будет, когда другая сторона закроет сообщение. разъем:

get_msg(Socket, Chunks) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Chunk} -> 
            get_msg(Socket, [Chunk|Chunks]);
        {error, closed} ->
            lists:reverse(Chunks);
        {error, Other} ->
            Other
    end.

Обратите внимание, что отправитель не может просто вызвать gen_tcp:close(Socket), чтобы сообщить об окончании отправки данных (см. описание gen_tcp:close/1 в документации). Вместо этого отправитель должен сообщить об окончании отправки данных, вызвав gen_tcp:shutdown. /2.

Я думаю, что чат-сервер неисправен, потому что он указывает {packet, 0} в сочетании с recv(Socket, 0):

client_handler(Sock, Server) ->
    gen_tcp:send(Sock, "Please respond with a sensible name.\r\n"),
    {ok,N} = gen_tcp:recv(Sock,0), %% <**** HERE ****
    case string:tokens(N,"\r\n") of

однако он не использует цикл для recv().


{активно, верно}

Сообщения, отправленные через соединение TCP (или UDP), автоматически считываются из сокета и помещаются в почтовый ящик контролирующего процесса. Управляющий процесс — это процесс, вызвавший accept(), или процесс, вызвавший connect(). Вместо вызова recv() для чтения сообщений непосредственно из сокета вы извлекаете сообщения из почтового ящика с помощью блока получения:

get_msg(Socket)
    receive
        {tcp, Socket, Chunk} ->  %Socket is already bound!
        ...
    end

{пакет, 1|2|4}:

Erlang автоматически считывает все фрагменты сообщения из сокета и помещает полное сообщение (с удаленным заголовком длины) в почтовый ящик:

get_msg(Socket) ->
    receive
        {tcp, Socket, CompleteMsg} ->
            CompleteMsg,
        {tcp_closed, Socket} ->
            io:format("Server closed socket.~n")
    end.

{пакет, 0 | необработанный (эквивалент):

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

get_msg(ClientSocket, Chunks) ->
    receive
        {tcp, ClientSocket, Chunk} ->
            get_msg(ClientSocket, [Chunk|Chunks]);
        {tcp_closed, ClientSocket} ->
            lists:reverse(Chunks)
    end.


В recv() документах упоминается что-то о Аргумент длины recv() только применим к сокетам в режиме raw. Но поскольку я не знаю, когда Socket находится в необработанном режиме, я не доверяю аргументу Length. Но см. здесь: Семантика Erlang gen_tcp:recv(Socket, Length). Хорошо, теперь я кое-что понимаю: из inet docs:

{packet, PacketType}(TCP/IP sockets)
Defines the type of packets to use for a socket. Possible values:

raw | 0
    No packaging is done.

1 | 2 | 4
    Packets consist of a header specifying the number of bytes in the packet, followed by that  
    number of bytes. The header length can be one, two, or four bytes, and containing an  
    unsigned integer in big-endian byte order. Each send operation generates the header, and the  
    header is stripped off on each receive operation.

    The 4-byte header is limited to 2Gb [message length].

Как подтверждают примеры в Erlang gen_tcp:recv(Socket, Length) семантики, когда {packet,0} указан, recv() может указать длину для чтения из потока TCP.

person 7stud    schedule 13.05.2017
comment
Спасибо! Такая глупая ошибка :). Erlang кажется все лучше и лучше с каждой вещью, которую я делаю в нем. - person Marko Taht; 14.05.2017
comment
Вы очень прямолинейно объясняете. Я ищу вариант {packet, size} и увидел ваш комментарий. - person tandathuynh148; 18.10.2020