使用 gen_tcp 的 Erlang 客户端-服务器示例没有收到任何东西

Erlang client-server example using gen_tcp is not receiving anything

我正在尝试在客户端接收数据,但没有收到任何数据。

发送消息的服务器代码

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.

我认为您的客户端有问题,因为它指定:

{packet, 2}

但服务器指定(代码未显示):

{packet, 0}

在第 Programming Erlang (2nd) 页。 269 它说:

Note that the arguments to packet used by the client and the server must agree. If the server was opened with {packet,2} and the client with {packet,4}, then nothing would work.

以下客户端可以成功接收来自服务器的文本:

%%=== 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) 只会从套接字读取一个块,然后是 return。该块 可能 是整个消息,也可能只是消息的一部分。为了保证您已经从套接字中读取了整个消息,我认为您必须遍历 recv():

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

那么问题就变成了:你怎么知道什么时候你已经阅读了整封邮件以便你可以结束循环? {packet,0} 告诉 Erlang 不要在消息前加上长度 header,那么你怎么知道消息的结尾在哪里呢?是否有更多块即将到来,或者 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 它表示,由于 OS 缓冲数据的方式,当您调用 send() 时,少量数据将被推送到目的地,因此 send() 将阻塞直到 [=34] =] 将数据拉到目的地。


默认选项:

您可以通过为其选项指定一个空列表来获取套接字的默认值,然后执行以下操作:

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) return 是一个具有与 Socket 相同选项的套接字。



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

{active, false}

邮件不会进入邮箱。此选项用于防止客户端用消息淹没服务器的邮箱。不要尝试使用 接收块 从邮​​箱中提取 'tcp' 消息——不会有任何消息。当进程要读取消息时,进程需要通过调用recv().

直接从socket中读取消息

{数据包, 1|2|4}:

数据包元组指定了双方希望消息遵守的协议{packet, 2} 指定每条消息前面有两个字节,其中包含消息的长度。这样,消息的接收者将知道从字节流中持续读取多长时间才能到达消息的末尾。当您通过 TCP 连接发送消息时,您不知道消息将被分成多少块。如果接收方在一个块后停止读取,它可能没有读取整条消息。因此,接收方需要一个指示器来告诉它何时阅读了整个消息。

对于 {packet, 2},接收方将读取两个字节以获取消息的长度,比如 100,然后接收方将等待直到它从正在流式传输的随机大小的字节块中读取了 100 个字节到接收器。

请注意,当您调用 send() 时,erlang 会自动计算消息中的字节数并将长度插入 N 字节,如 {packet, N} 所指定,并附加信息。同样,当你调用 recv() 时,erlang 自动从流中读取 N 字节,如 {packet, N} 指定的那样,以获取消息的长度,然后 recv() 阻塞直到它读取来自套接字的长度字节,然后 recv() return 整个消息。

{数据包,0 | raw}(等效):

当指定 {packet, 0} 时,recv() 将读取其 Length 参数指定的字节数。如果 Length 为 0,那么我认为 recv() 将从流中读取一个块,这将是一个任意 num字节数。因此,{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 in the docs). Instead, a sender has to signal that is is done sending data by calling gen_tcp:shutdown/2.

的描述

我认为聊天服务器有问题,因为它指定了 {packet, 0}recv(Socket, 0),但它没有为 recv():

使用循环
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

{active, true}

通过 TCP(或 UDP)连接发送的消息会自动为您从套接字中读取并放入控制进程的邮箱中。控制进程是调用accept() 的进程或调用connect() 的进程。您不是调用 recv() 直接从套接字读取消息,而是使用 接收块 :

从邮​​箱中提取消息
get_msg(Socket)
    receive
        {tcp, Socket, Chunk} ->  %Socket is already bound!
        ...
    end

{数据包, 1|2|4}:

Erlang 自动为您从套接字中读取消息的所有块,并将完整的消息(长度 header 剥离)放入邮箱:

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

{数据包,0 | raw}(等效):

消息没有长度header,所以当Erlang从套接字读取时,Erlang无法知道消息的结尾何时到达。结果,Erlang 将从套接字中读取的每个块放入邮箱中。你需要一个循环来从邮箱中提取所有的块,而另一端必须关闭套接字以表示没有更多的块来了:

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


recv() docs mention something about recv()'s Length argument only being applicable to sockets in raw mode. But because I don't know when a Socket is in raw mode, I don't trust the Length argument. But see here: Erlang gen_tcp:recv(Socket, Length) semantics. Okay, now I'm getting somewhere: from the erlang 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) semantics 中的示例所证实的,当指定 {packet,0} 时,recv() 可以指定从 TCP 流中读取的长度。