繁体   English   中英

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

[英]Erlang server, Java client - TCP messages get split?

如标题所示,我有一个用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端固定?

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

您应该:

  • 使用某种“消息结尾”序列,例如0字节(如果否则消息中不会出现)。
  • 给每个消息加上消息长度的前缀。

除此之外,您现在还没有明显区分字节和文本。 例如,您的Java客户端当前无声地忽略每个char的高8位。 而不是使用DataOutputStream ,我只想用建议OutputStream ,然后为每个消息:

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

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

  • 写数据

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

您无法回避TCP是基于流的协议这一事实。 如果您想要基于消息的协议,则确实必须将其放在首位。 (当然,我敢肯定有有用的库可以做到这一点-但您不应该只将它留给TCP和希望。)

您需要在服务器和客户端之间定义协议,以将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}].

正如乔恩所说,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大小。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM