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