【问题标题】:TCP client for Android: text is not received in full适用于 Android 的 TCP 客户端:未完整接收文本
【发布时间】:2020-10-02 09:48:18
【问题描述】:

我正在将 Java 桌面项目转换为 Android。其中一部分包括到服务器的 TCP 连接以及从服务器到客户端(Android 应用程序)解析长文本。这是我也尝试在 Android 中使用的桌面项目的代码:

// Method is called when a button is tapped
public void tapButton() {

    // Create a message to the server that requests for the Departure navdata
    String messageToServer = someMethodToMakeHandshakeMessage();

    // Connect to the server
    if (!messageToServer.equals("")) {
        String finalMessageToServer = messageToServer;

        new Thread(() -> {
            String navdata = connectClient(finalMessageToServer);

            getActivity().runOnUiThread(() -> messageReceived(navdata));
            // I am also using messageReceived(navdata) without runOnUiThread with the same result
        }).start();
    }
}

public String connectClient(String messageOut) {

    Socket socket = null;
    DataInputStream input = null;
    DataOutputStream output = null;
    BufferedReader br = null;
    // Final message from the server
    String data = "";
    // Message from the server that should terminate TCP connection
    String  terminator = "END_DATA";

    try {
        // Create socket and streams
        socket = new Socket(someIPAddress, somePort);
        input = new DataInputStream(socket.getInputStream());
        output = new DataOutputStream(socket.getOutputStream());

        //Send message to the server
        output.writeBytes(messageOut);
        //Read Response
        br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        StringBuilder sb = new StringBuilder();
        String s = "";
        int value = 0;

        // Process the message from the server and add to the StringBuilder
        while((value = br.read()) != -1) {
            // converts int to character
            char c = (char)value;

            sb.append(c);

            if(sb.toString().contains(terminator)) {
                break;
            }
        }

        // Create the final string
        data = sb.toString();
    }

    catch (UnknownHostException e) {
        // Dealing with exception
    }

    catch (EOFException e) {
        // Dealing with exception
    }

    catch (IOException e) {
        // Dealing with exception
    }

    finally {
        try {
            if(socket!=null) { socket.close();}
            if(input != null) { input.close();}
            if(output != null) { output.close();}
            if(br != null) { br.close();}
        }
        catch (IOException ex) {
            // Dealing with exception
        }
        socket = null;
        input = null;
        output = null;
        br = null;
    }

    return data;
}

public void messageReceived(String message) {
    // Method to deal with received data
}

虽然代码在桌面 Java 应用程序中运行良好,但我在使用 Android 时遇到了问题(使用模拟器)。文本不是全长发送的,而是在中间某处被剪切(客户端仅收到 20-50%;解析的字符数一直不同)。此外,我注意到连接服务器花费的时间太长,但我想这是由于使用了模拟器。

从服务器接收长文本的 TCP 客户端是否应该在 Android 中以不同的方式实现?

编辑:使用@blackapps 的建议实现了以下代码:

String line = br.readLine();

   while (line != null) {
     sb.append(line);
     line = br.readLine();

     if (line.trim().isEmpty()) {
        Log.i("EMPTY LINE>>>>>>>>>>>>>>>>>",line);
     }

     if(line.equals(terminator)) {
        break;
     }
  }

// Create the final string
data = sb.toString();
}

两个问题。我想在收到的文本中保留空行。未检测到终止符。我认为,它用两个空行与正文分开。但是,在第一个空行之后,它进入无限循环并且连接永远不会终止。

编辑#2。 在花了几个小时试图弄清楚发生了什么之后,对服务器进行了更改,并比较了发送和接收的字节数,我注意到这不是代码的问题。客户端似乎收到了全文。问题在于如何使用 Log.i(String, String) 方法将文本写入控制台。我在代码中添加了旧的System.out.println(),整个文本显示在控制台中。但是,来自 Log.i() 的文本在中间被截断。这是我第一次使用 Android Studio,到底发生了什么?

非常感谢!

【问题讨论】:

  • input = new DataInputStream(socket.getInputStream())。如果您没有使用input,那为什么会出现呢?
  • 您正在逐字节读取。非常低效。特别是当您还将每个字符的 sb 转换为字符串时。尝试使用 br.readLine()。
  • 让服务器在单独的行上发送“END_DATA\n”。然后检查该字符串的行。不是全部。
  • sb.append(line); 那应该是sb.append(line).append("\n"); 以保留行和空行。将此语句放在 readLine() 语句之后。
  • The terminator is separated from the text by two empty lines. 好的。那不重要。请确保服务器在终止符之后发送一个 \n。

标签: android tcp


【解决方案1】:

先说TCP socket。

当谈到 TCP 套接字时,它是一个数据流。 TCP 将数据视为非结构化、但有序的字节流。它与socket.io的种类不同。

有时,TCP 会从发送缓冲区中抓取数据块并将数据传递给网络层。可以抓取和放置在段中的最大数据量受最大段大小 (MSS) 的限制。 MSS 通常通过首先确定最大链路层帧的长度来设置。

所以这取决于设备。

例如,你有两条消息,每条消息都有 1000 字节的数据,你调用:

-------------客户端----

client.send(theFirstMessage) // 1000 bytes
client.send(theSecondMessage) // 1000 bytes

------------- 服务器端 -----

socket.onReceived(data => {
    // process(data)
})

使用上面的伪代码你应该注意:

在onReceived块上接收和调用的数据不能是FirstMessage的1000字节。

  1. 可能是前 400 个字节,然后在其他事件中您会收到 400 个字节,然后是更多的 400 个字节(第一个字节的 200 个字节和第二个字节的 200 个字节)。
  2. 可能是 1200 字节(第一个字节为 1000,第二个字节为 200)。

TCP 将数据视为非结构化的有序的字节流。 Socket.io 是一个包装器,当它使用 TCP 套接字时,它会为您收集和组合/拆分数据,以便您接收到与从另一端发送的数据完全相同的事件。 当你使用 TCP 时,你必须自己做,你必须定义应用协议来做。

发送/接收 TCP 请求有两种常用方法:

  1. 分离器,你选择分离器。例如,我们选择 32 位 AABBCCDD 作为拆分器(与您选择 END_DATA 字符串相同),但请记住它是二进制数据。然后你必须确保请求中的数据不包含splitter。为此,您必须对请求进行编码。例如我们可以将请求编码为base64,然后使用base64表中不包含的字符作为分隔符。

  2. 前缀长度,上面的方法有它的开销,因为我们必须对请求数据进行编码。前缀长度方法是更好的选择。 我们可以在前面加上请求的长度。

伪代码:

// use Int32, 4 bytes to indicate the length of message after it

-------------- client side ----------------
    client.send(theFirstMessage.length)    // Int32
    client.send(theFirstMessage) // 1000 bytes

    client.send(theSecondMessage.length) 
    client.send(theSecondMessage) // 1000 bytes


-------------- server side -----------------

    var buffer = Buffer()

    socket.onReceived(data => {
        buffer.append(data)

        let length = Int32(buffer[0...3])

        if (buffer.length >= length + 4) {
           let theRequest = buffer[4 ... 4 + length - 1]
           process(theRequest)

           buffer = buffer.dropFirst(4 + length)
        }

    })

还有一点,在使用 TCP 套接字时,它只是字节流,所以字节序很重要https://en.wikipedia.org/wiki/Endianness

例如,一个安卓设备是小端,而服务器端(或其他安卓设备)是大端。然后是来自android设备的4个字节的Int32,在服务器端接收时,如果你不关心它会被错误解码。

所以,前缀长度应该按照特定的字节序进行编码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-12-31
    • 2016-08-02
    • 2017-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多