簡體   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