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

时间:2014-05-18 16:18:39

标签: java sockets tcp erlang

正如标题所说,我有一个用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编写时,是否有一些关于消息长度的隐藏信息?

如果需要进行分隔符检查,我很惊讶我无法找到有关该主题的更多信息。如何以实际的方式完成?

提前致谢!

3 个答案:

答案 0 :(得分:4)

  

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

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

你应该:

  • 使用某种“消息结束”序列,例如如果您的消息中没有出现,则为0字节。
  • 使用邮件的长度为每封邮件添加前缀。

除此之外,您目前还没有明确区分字节和文本。例如,您的Java客户端当前默默地忽略每个char的前8位。我建议只使用DataOutputStream,然后为每封邮件使用OutputStream,而不是使用byte[] encodedText = text.getBytes(StandardCharsets.UTF_8);

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

    DataOutputStream
  • 将长度前缀写入流(可能是7位编码的整数,或者可能只是固定宽度,例如4个字节)。 (实际上,坚持{{1}}会使这个更简单。)

  • 写入数据

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

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

答案 1 :(得分:3)

您需要在服务器和客户端之间定义协议,以将TCP流拆分为消息。 TCP流被分成数据包,但不能保证这些匹配您的发送/写入或读取/读取的调用。

一个简单而强大的解决方案是为所有消息添加长度前缀。 Erlang可以使用{packet, 1|2|4}选项透明地执行此操作,其中前缀以1,2或4个字节编码。您必须在Java端执行编码。如果您选择2或4个字节,请注意长度应采用big-endian格式编码,与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}].

答案 2 :(得分:1)

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

让您的应用获得所需内容的最简单的更改是将erlang服务器端的TCP_OPTIONS {packet,0}更改为{packet,4}

并将java writer代码更改为:

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