适用于 Android 的 TCP 客户端:未完整接收文本 [英] TCP client for Android: text is not received in full

查看:20
本文介绍了适用于 Android 的 TCP 客户端:未完整接收文本的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am converting a Java desktop project to Android. Part of it includes a TCP connection to a server and parsing a long text from the server to the client (the Android application). This is the code that I have for the desktop project that I also try to use in 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%;解析的字符数一直不同).此外,我注意到连接到服务器的时间太长了,但是,我猜这是由于使用了模拟器.

Whereas the code works fine in the desktop Java application, I have problems with Android (using an emulator). The text is not sent in full length and is cut somewhere in the middle (only 20-50% received by the client; the number of parsed characters differs all the time). Besides, I have noticed that it is taking too long to connect to the server, but, I guess, this is due to working with an emulator.

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

Should a TCP client receiving long texts from the server be implemented in Android somewhat differently?

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

Implemented the following code using a suggestion by @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();
}

两个问题.我想在收到的文本中保留空行.未检测到终止符.我认为,它与正文分开,有两个空行.然而,在第一个空行之后,它进入无限循环并且连接从未终止.

Two issues. I would like to keep the empty lines in the received text. The terminator is not detected. I think, it is separated from the main text with two empty lines. However, after the first empty line, it goes to indefinite loop and connection never terminated.

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

EDIT #2. After having spent several hours trying to figure out what is going on, making changes to the server, and comparing the number of bytes sent and received, I have noticed that this is not the problem with the code. It appears that the client receives the full text. The problem is with how the text is written in the console using the Log.i(String, String) method. I have added the good old System.out.println() in the code, and the whole text was shown in the console. However, the text from Log.i() was cut off in the middle. As this is my first experience with Android Studio, what the heck is going on?

非常感谢!

推荐答案

先说 TCP socket.

Let talk about TCP socket first.

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

When talking about TCP socket, it's a stream of data. TCP views data as an unstructured, but ordered, stream of bytes. It's different from the kinds of socket.io.

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

From time to time, TCP will grab chunks of data from the send buffer and pass the data to the network layer. The maximum amount of data that can be grabbed and placed in a segment is limited by the maximum segment size (MSS). The MSS is typically set by first determining the length of the largest link-layer frame.

所以这取决于设备.

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

For example, you have two messages, each of them has 1000 bytes data, and you call:

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

-------------- client side ----------------

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

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

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

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

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

With above pseudocode you should note that:

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

The data which received and called on onReceived block couldn't be 1000 bytes of theFirstMessage.

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

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

TCP views data as an unstructured, but ordered, stream of bytes. Socket.io is a wrapper, when it uses TCP socket, it collect and combine/split the data for you, so that you received the events with exactly the data was sent from other side. When you work with TCP, you have to do it your self, you have to define the application protocol to do it.

发送/接收 TCP 请求有两种常见方式:

There're two common ways to send/receive TCP requests:

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

  1. Splitter, you choose a splitter. For example, we choose 32 bits AABBCCDD as the splitter (same as you choose END_DATA string), but keep in mind it's binary data. Then you have to ensure that the data in request doesn't contains the splitter. To do that, you have to encode the request. For example we can encode request as base64, then use the character which isn't included in base64 table as the splitter.

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

Prefix length, the above method has its overhead as we have to encode request data. The prefix length method is a better choice. We can prefix the length of request before.

伪代码:

// 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

One more thing, when working with TCP socket, it's just stream of bytes, so the endianness is important https://en.wikipedia.org/wiki/Endianness

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

For example, an android device is little endian and server side (or other android device) is big endian. Then 4 bytes of Int32 from the android device, when received on server side, it will be decoded wrongly if you don't care about it.

因此,前缀长度应按特定字节序编码.

So, the prefix length should be encoded by specific endianness.

这篇关于适用于 Android 的 TCP 客户端:未完整接收文本的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆