使用 gen_tcp 的 Erlang 客户端-服务器示例未收到任何内容

Posted

技术标签:

【中文标题】使用 gen_tcp 的 Erlang 客户端-服务器示例未收到任何内容【英文标题】:Erlang client-server example using gen_tcp is not receiving anything 【发布时间】:2017-10-12 22:05:44 【问题描述】:

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

发送消息的服务器代码

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 = #clientsocket=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.

【问题讨论】:

您能为此创建一个MCVE 吗?发布的代码缺少几个函数的定义,生成服务器并接受连接的代码等。 我的代码基本上是这样的。 www2.erlangcentral.org/wiki/?title=Chatserver 主要工作机制是一样的,唯一不同的是循环函数,我调用了一些其他函数来做事情。 【参考方案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, Nactive, true|false 的结论:


send()

当您拨打send() 时,实际上没有数据* 传输到目的地。相反,send() 会阻塞,直到目标调用 recv(),然后才会将数据传输到目标。

* 在“Programming Erlang (2nd)”中,第 1 页。 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() 阻塞直到它从套接字读取长度字节,然后@ 987654360@ 返回整个消息。

数据包,0 |原始(等效):

当指定packet, 0 时,recv() 将读取其 Length 参数指定的字节数。如果 Length 为 0,那么我认为 recv() 将从流中读取一个块,这将是任意数量的字节。因此,packet, 0recv(Socket, 0) 的组合要求您创建一个循环来读取消息的所有块,并且recv() 的指示器将停止阅读,因为它已到达消息的末尾当对方关闭socket时:

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, 0recv(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() 直接从套接字读取消息,而是使用 receive 块从邮箱中提取消息:

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()docs 提到了 recv() 的 Length 参数适用于 raw 模式的套接字。但是因为我不知道 Socket 何时处于原始模式,所以我不相信 Length 参数。但请看这里:Erlang gen_tcp:recv(Socket, Length) semantics。好的,现在我到了某个地方:来自 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 流中读取的长度。

【讨论】:

谢谢!如此愚蠢的错误:)。 Erlang 在我做的每一件事上似乎越来越好。 你的解释很严格。我正在寻找选项 packet, size 并看到了您的评论。

以上是关于使用 gen_tcp 的 Erlang 客户端-服务器示例未收到任何内容的主要内容,如果未能解决你的问题,请参考以下文章

在erlang中中断gen_tcp:recv

Erlang同时连接1M客户端

Erlang gen_tcp:listen/2 for IPv6

Erlang OTP 主管 gen_tcp - error,eaddrinuse

erlang/elixir gen_tcp connect - 不连接但telnet wil

使用不止一种 erlang 行为