【问题标题】:Erlang server, Java client - TCP messages get split?Erlang 服务器,Java 客户端 - TCP 消息被拆分?
【发布时间】:2014-07-06 14:36:19
【问题描述】:

正如标题所说,我有一个用 Erlang 编写的服务器,一个用 Java 编写的客户端,它们通过 TCP 进行通信。我面临的问题是 gen_tcp:recv 显然不知道何时收到来自客户端的“完整”消息,因此将其“拆分”为多条消息。

这是我正在做的一个例子(不完整的代码,试图只保留相关部分):

代码

Erlang 服务器

-module(server).
-export([start/1]).

-define(TCP_OPTIONS, [list, {packet, 0}, {active, false}, {reuseaddr, true}].

start(Port) ->
   {ok, ListenSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
   accept(ListenSocket).

accept(ListenSocket) ->
    {ok, Socket} = gen_tcp:accept(ListenSocket),
    spawn(fun() -> loop(Socket) end),
    accept(ListenSocket).

loop(Socket) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Data} ->
            io:format("Recieved: ~s~n", [Data]),
            loop(Socket);
        {error, closed} ->
            ok
    end.

Java 客户端

public class Client {
    public static void main(String[] args) {
        Socket connection = new Socket("localhost", Port);
        DataOutputStream output = new DataOutputStream(connection.getOutputStream());
        Scanner sc = new Scanner(System.in);

        while(true) {
            output.writeBytes(sc.nextLine());
        }
    }
}

结果

客户

Hello!

服务器

Received: H
Received: el
Received: lo!

我一直在搜索,如果我理解正确,TCP不知道消息的大小,您需要手动设置某种分隔符。

但我不明白的是,如果我用 Erlang 编写客户端,消息似乎永远不会分开,如下所示:

Erlang 客户端

-module(client).
-export([start/1]).

start(Port) ->
    {ok, Socket} = gen_tcp:connect({127,0,0,1}, Port, []),
    loop(Socket).

loop(Socket) ->
    gen_tcp:send(Socket, io:get_line("> ")),
    loop(Socket).

结果

客户

Hello!

服务器

Received: Hello!

这让我想知道它是否可以在 Java 端修复?我在服务器端尝试了几种不同的输出流、写入方法和套接字设置的组合,但都没有解决问题。

此外,网络上有大量 Erlang(聊天)服务器示例,它们不做任何分隔符,尽管它们通常在两端都用 Erlang 编写。然而,他们似乎认为消息就像发送一样被接收。这只是不好的做法,还是当客户端和服务器都用 Erlang 编写时,是否有一些关于消息长度的隐藏信息?

如果需要分隔符检查,我很惊讶我找不到关于该主题的太多信息。如何以实际的方式完成?

提前致谢!

【问题讨论】:

    标签: java sockets tcp erlang


    【解决方案1】:

    这让我想知道它是否可以在 Java 端修复?

    不,绝对不是。不管你为什么没有碰巧看到 Erlang 客户端的问题,如果你没有在协议中加入任何类型的“消息边界”指示,你将无法可靠地检测到整个消息。我强烈怀疑如果你用 Erlang 客户端发送一个非常的消息,你仍然会看到拆分消息。

    您应该:

    • 使用某种“消息结束”序列,例如如果您的消息中不会出现,则为 0 字节。
    • 在每条消息前加上消息的长度。

    除此之外,您目前还没有清楚地区分字节和文本。例如,您的 Java 客户端当前静默忽略每个 char 的前 8 位。我建议不要使用DataOutputStream,而是使用OutputStream,然后对每条消息:

    • 使用特定编码将其编码为字节数组,例如

      byte[] encodedText = text.getBytes(StandardCharsets.UTF_8);
      
    • 在流中写入一个长度前缀(可能是一个 7 位编码的整数,或者可能只是一个固定宽度,例如 4 个字节)。 (实际上,坚持使用 DataOutputStream 会使 this 更简单一些。)

    • 写入数据

    在服务器端,你应该通过读取长度来“读取消息”,然后读取指定的字节数。

    您无法回避 TCP 是 基于流 协议的事实。如果你想要一个基于消息的协议,你真的必须自己把它放在首位。 (当然,我确信有一些有用的库可以做到这一点 - 但你不应该只是把它留给 TCP 并希望。)

    【讨论】:

    • 感谢您的解释,一切都变得更加清晰了!
    【解决方案2】:

    您需要在服务器和客户端之间定义一个协议,以将 TCP 流拆分为消息。 TCP 流分为数据包,但不能保证这些数据包与您对 send/write 或 recv/read 的调用相匹配。

    一个简单而强大的解决方案是为所有消息添加一个长度前缀。 Erlang 可以使用 {packet, 1|2|4} 选项透明地做到这一点,其中前缀编码为 1、2 或 4 个字节。您必须在 Java 端执行编码。如果您选择 2 或 4 字节,请注意长度应以大端格式编码,与 DataOutputStream.outputShort(int)DataOutputStream.outputInt(int) java 方法使用的字节顺序相同。

    但是,从您的实现看来,您确实有一个隐式协议:您希望服务器分别处理每一行。

    幸运的是,这也被 Erlang 透明地处理了。您只需传递{packet, line} 选项。但是,您可能需要调整接收缓冲区,因为超过此缓冲区的行将被截断。这可以通过{recbuf, N} 选项来完成。

    因此,只需重新定义您的选项就可以满足您的需求。

    -define(MAX_LINE_SIZE, 512).
    -define(TCP_OPTIONS, [list, {packet, line}, {active, false}, {reuseaddr, true}, {recbuf, ?MAX_LINE_SIZE}].
    

    【讨论】:

    • 哇,我完全忽略了{packet, X} 选项,认为它仅适用于静态长度(在我的情况下不起作用)。非常感谢{packet, line} 选项,这可能就是我所需要的!
    【解决方案3】:

    正如 Jon 所说,TCP 是一种流式协议,在您正在寻找的意义上没有消息的概念。它通常会根据您的读取速率、内核缓冲区大小、网络的 MTU 等进行分解...无法保证您一次不会获得 1 个字节的数据。

    对您的应用进行更改以获得您想要的最简单的方法是将 erlang 服务器端的 TCP_OPTIONS {packet,0} 更改为 {packet,4}

    并将java编写器代码更改为:

    while(true) {
       byte[] data = sc.nextLine().getBytes(StandardCharsets.UTF_8); // or leave out the UTF_8 for default platform encoding
       output.writeInt(data.length);
       output.write(data,0,data.length);
    }
    

    您应该会发现自己收到了完全正确的信息。

    如果您在服务器端进行此更改,您还应该将 {packet,4} 添加到 erlang 客户端,因为服务器现在需要一个 4 字节的标头来指示消息的大小。

    注意:{packet,N} 语法在 erlang 代码中是透明的,客户端不需要发送 int,服务器看不到 int。 Java 在标准库中没有等效的大小框架,因此您必须自己编写 int 大小。

    【讨论】:

      猜你喜欢
      • 2013-04-29
      • 1970-01-01
      • 2014-09-08
      • 2012-11-03
      • 2013-01-18
      • 1970-01-01
      • 2021-07-19
      • 2013-03-02
      • 2018-06-05
      相关资源
      最近更新 更多