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

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

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

提前致谢!

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

一个简单而强大的解决方案是为所有消息添加长度前缀。 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}]. 

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

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

你应该:

  • 使用某种“消息结束”序列,例如0字节,否则消息中不会出现这种情况。
  • 使用消息的长度为每条消息添加前缀。

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

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

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

  • 写数据

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

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

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