Erlang服务器,Java客户端 - TCP消息被分割?

3
正如标题所述,我有一个用Erlang编写的服务器,一个用Java编写的客户端,它们通过TCP进行通信。我面临的问题是gen_tcp:recv显然不知道何时接收到了来自客户端的“完整”消息,并因此将其“分割”成多个消息。
这是我正在做的示例(不完整的代码,试图仅保留相关部分):
-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());
        }
    }
}

Result

Client

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).

Result

Client

Hello!

服务器

Received: Hello!

这让我想知道这是否可以在Java端修复?我已经尝试了几种不同的输出流、写入方法和套接字设置组合在服务器端,但是没有任何解决问题的办法。
此外,网络上有很多Erlang(聊天)服务器示例,它们不需要使用任何定界符,尽管这些通常是在两端都使用Erlang编写的。尽管如此,它们似乎假设消息接收方式与发送方式相同。这是不良实践,还是当客户端和服务器都使用Erlang编写时存在一些关于消息长度的隐藏信息?
如果需要定界符检查,我很惊讶为什么找不到更多相关信息。如何以实用的方式完成这项工作呢?
提前感谢您!
3个回答

4
这让我想知道是否可以在Java端修复此问题?
不,绝对不行。无论为什么你没有看到Erlang客户端的问题,如果你没有在协议中放置任何形式的“消息边界”指示,你将无法可靠地检测到完整的消息。我强烈怀疑,如果你使用Erlang客户端发送一个非常大的消息,你仍然会看到分裂的消息。
你应该:
- 使用某种“消息结束”序列,例如0字节,如果这在你的消息中没有出现。 - 在每个消息前加上消息的长度。
除此之外,你目前没有清楚区分字节和文本。例如,你的Java客户端当前正在“静默”地忽略每个char的前8位。我建议你不要使用DataOutputStream,而是使用OutputStream,然后针对每个消息:
  • Encode it as a byte array using a specific encoding, e.g.

    byte[] encodedText = text.getBytes(StandardCharsets.UTF_8);
    
  • Write a length prefix to the stream (possibly in a 7-bit-encoded integer, or maybe just as a fixed width, e.g. 4 bytes). (Actually, sticking with DataOutputStream would make this bit simpler.)

  • Write the data
在服务器端,您应该通过先读取长度,然后读取指定数量的字节来“读取消息”。
无法避免TCP是一种基于流的协议这个事实。如果您想要一个基于消息的协议,确实需要自己添加。当然,我相信有一些有用的库可以做到这一点,但您不应该只依赖TCP并希望它能完成所有工作。

谢谢解释,现在一切都清楚多了! - kallgren

3
您需要在服务器和客户端之间定义一个协议,将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}选项,这可能就是我所需要的! - kallgren

1

正如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大小。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接