为什么有时收不到第二条消息?

opb*_*avo 1 erlang

我创建了一个简单的服务器来接收来自单个客户端的消息,但是当我一次发送多条消息时,有时它们没有按我的预期接收。这是服务器代码:

test() ->
    {ok, LSock} = gen_tcp:listen(7777, [binary, {active, true}, {packet, 0}, {reuseaddr, true}]),
    spawn(fun() -> acceptor(LSock) end).

loop() ->
    receive
        {tcp, Socket, Bin} ->
            io:format("Received: ~p~n", [binary_to_term(Bin)]),
            loop();
        {tcp_closed, Socket} ->
            io:format("Socket Closed~n"),
            exit(normal);
        {tcp_error, _} ->
            exit(normal);
        _ ->
            io:format("Unknown Message~n"),
            exit(normal)
    end.


acceptor(LSock) ->
    {ok, Sock} = gen_tcp:accept(LSock),
    io:format("Client Connected~n"),
    Pid = spawn(fun() -> loop() end),
    gen_tcp:controlling_process(Sock, Pid).
Run Code Online (Sandbox Code Playgroud)

我连接到服务器的方式是通过 Erlang shell,如下所示:

{ok, Socket} = gen_tcp:connect("localhost", 7777, [binary, {packet, 0}, {active, true}]).`
Run Code Online (Sandbox Code Playgroud)

但是当我gen_tcp:send(Socket, term_to_binary(data1)),gen_tcp:send(Socket, term_to_binary(data3)).连续多次执行时,大多数时候它只显示“已接收数据1”而不是“已接收数据3”。例如,当我两次调用该部分代码时,我得到:

Received: data1
Received: data3
Received: data1
Run Code Online (Sandbox Code Playgroud)

为什么会发生这种情况?如果我一一调用每个发送,它会按预期工作,但如果将这两个作为单个“命令”提交,则不会。

注意:我也尝试过在连接和发送之间等待,因为我读到可能存在竞争条件,因为gen_tcp:controlling_process()

leg*_*cia 5

发生这种情况是因为 TCP 是字节流协议,而不是基于数据包的协议。当您几乎同时发送两条数据时,操作系统可以决定将它们组合在一个 TCP 数据包中。(这可以节省带宽,因为每个 TCP 数据包在数据前面都有一个 40 字节的标头。)

因此,在该loop函数中,Bin一条接一条地包含两条消息。将binary_to_term第一个术语转换为术语,但它不会检查该术语是否占据整个二进制文件,因此不会引发错误。你可以在 shell 中尝试一下:

> A = <<(term_to_binary(data1))/binary, (term_to_binary(data3))/binary>>.
<<131,100,0,5,100,97,116,97,49,131,100,0,5,100,97,116,97,51>>
> binary_to_term(A).
data1
Run Code Online (Sandbox Code Playgroud)

您可以采取多种措施来解决此问题:

  • 使用面向数据包的协议(例如 UDP)而不是 TCP。使用 UDP 时,您要么收到发送的确切数据包,要么什么也收不到。请注意,UDP 不提供某些 TCP 功能,例如按顺序传送和丢包重试。如果您需要这些,您必须在您的应用程序中实现它们。(事实上​​,这就是浏览器开发人员发明QUIC时所做的事情,QUIC 用于在 UDP 之上运行 HTTP/3。)

  • 引入某种框架。例如,HTTP 用一个空行来表示请求头的结束和请求体的开始,然后请求体的长度由头指定Content-Length;在这么多字节之后,一个新的请求开始。

    一种更简单的方法是在每条消息的开头添加消息的长度 - Erlang 对此提供了内置支持!您当前{packet,0}在 和 中gen_tcp:listen使用gen_tcp:connect,这会显式关闭该功能。如果您将两者都设置为{packet,1}{packet,2}{packet,4},则发送的每条消息gen_tcp:send将分别以 1、2 或 4 个字节的长度为前缀,并且您收到的每条消息{tcp, Socket, Bin}将是一个完整的消息,解码和缓冲已由 Erlang 运行时处理。

  • 一次从传入数据中读取一项,并保存不完整的数据供以后使用。

    这是有效的,因为 in 本身生成的数据term_to_binary包括长度,因此可以找出下一条消息的开始位置。如果将used选项传递给binary_to_term,您将获得“消耗”的字节数,从而获得您需要开始读取下一条消息的位置。继续上面的例子,包含A两条消息:

    > {Message, Used} = binary_to_term(A, [used]).  
    {data1,9}
    %% we get the first message, and we know that the next message starts 9 bytes in
    > <<_:Used/bytes, B/binary>> = A.
    <<131,100,0,5,100,97,116,97,49,131,100,0,5,100,97,116,97,51>>
    %% we skip over the specified amount of bytes, and store the rest in B
    > {Message2, Used2} = binary_to_term(B, [used]).
    {data3,9}
    %% we get the second message, and it also used 9 bytes
    > <<_:Used2/bytes, C/binary>> = B.              
    <<131,100,0,5,100,97,116,97,51>>
    %% we extract the rest of the binary
    > C.
    <<>>
    %% the remainder is empty, so we have read everything
    
    Run Code Online (Sandbox Code Playgroud)

    当我们在单个 TCP 数据包中获取多个完整消息时,这很简单,但当相反的情况发生时,情况就会变得很棘手:一条消息被拆分到多个 TCP 数据包中。在这种情况下,我们必须保留已收到但尚未构成完整术语的数据缓冲区,当我们获得更多数据时,将其附加到缓冲区并重试binary_to_term

话虽如此,如果您同时控制客户端和服务器,我建议您使用{packet,4}- 但重要的是要了解它为何有效,以及为什么有时没有它就无法工作。


您的代码中存在与此相关的竞争条件gen_tcp:controlling_process。因为您已指定{active,true}to gen_tcp:listen,所以任何传入数据都将立即作为消息发送到当前控制进程。如果您在调用gen_tcp:accept和之间收到数据gen_tcp:controlling_process,该消息将被发送到错误的进程。您可以通过指定{active,false}in来修复此问题gen_tcp:listen,并明确告诉子进程它可以将其切换回来:

acceptor(LSock) ->
    {ok, Sock} = gen_tcp:accept(LSock),
    io:format("Client Connected~n"),
    Pid = spawn(fun() -> pre_loop(Sock) end),
    gen_tcp:controlling_process(Sock, Pid),
    Pid ! controlling_process_changed.

pre_loop(Sock) ->
    receive
        controlling_process_changed ->
            inet:setopts(Sock, [{active, true}]),
            loop()
    end.

loop() ->
    %% no changes here
Run Code Online (Sandbox Code Playgroud)

  • 美丽的答案。 (2认同)