正如标题所说,我有一个用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.
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中编写客户端,这些消息似乎永远不会分开,就像这样:
-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编写时,是否有一些关于消息长度的隐藏信息?
如果需要进行分隔符检查,我很惊讶我无法找到有关该主题的更多信息。如何以实际的方式完成?
提前致谢!
答案 0 :(得分:4)
不,绝对没有。无论您为什么没有看到Erlang客户端的问题,如果您没有在协议中添加任何类型的“消息边界”指示,您将无法能够可靠地检测到整个消息。我强烈怀疑,如果您使用Erlang客户端发送非常大消息,您仍会看到拆分消息。这让我想知道它是否可以在Java端修复?
你应该:
除此之外,您目前还没有明确区分字节和文本。例如,您的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大小。