【问题标题】:Swift 3: how to convert a UTF8 data stream (1,2,3 or 4 bytes per char) to String?Swift 3:如何将 UTF8 数据流(每个字符 1、2、3 或 4 个字节)转换为字符串?
【发布时间】:2016-12-15 23:33:41
【问题描述】:

在我的应用程序中,一个 tcp 客户端正在处理来自远程 tcp 服务器的数据流。当接收到的字符是 1 字节字符时,一切正常。当 tcp 服务器发送特殊字符如“ü”(十六进制“c3b5”-> 一个 2 字节字符)时,我开始遇到问题。

这是 Swift 3 代码行,只要接收到的数据包含一些超过 1 个字节的 UTF8 字符,就会得到一个 nil 字符串:

let convertedString = String(bytes: data, encoding: String.Encoding.utf8)

知道如何解决这个问题吗?基本上,传入的流可能包含编码为 UTF8 的 1 字节或 2 字节字符,我需要将数据流转换为字符串而不会出现问题。

这是我遇到问题的整个代码部分:

func startRead(for task: URLSessionStreamTask) {
    task.readData(ofMinLength: 1, maxLength: 65535, timeout: 300) { (data, eof, error) in
        if let data = data {
            NSLog("stream task read %@", data as NSData)

            let convertedString1 = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))

            if let convertedString = String(bytes: data, encoding: String.Encoding.utf8) {

                self.partialMessage = self.partialMessage + convertedString

                NSLog(convertedString)

                // Assign lengths (delimiter, MD5 digest, minimum expected length, message length)
                let delimiterLength = Constants.END_OF_MESSAGE_DELIMITER.lengthOfBytes(using: String.Encoding.utf8)
                let MD5Length = 32 // 32 characters -> hex representation of 16 bytes
                // 3 = CR+LF+1 char at least
                let minimumExpectedMessageLength = MD5Length + delimiterLength + 3
                let messageLength = self.partialMessage.lengthOfBytes(using: String.Encoding.utf8)

                // Check for delimiter and minimum expected message length (2 char msg + MD5 digest + delimiter)
                if (self.partialMessage.contains(Constants.END_OF_MESSAGE_DELIMITER)) &&
                    (messageLength >= minimumExpectedMessageLength) {

                    var message = self.partialMessage

                    // Get rid of optional CR+LF
                    var lowBound = message.index(message.endIndex, offsetBy: -1)
                    var hiBound = message.index(message.endIndex, offsetBy: 0)
                    var midRange = lowBound ..< hiBound

                    let optionalCRLF = message.substring(with: midRange)

                    if (optionalCRLF == "\r\n") || (optionalCRLF == "\0") {  // Remove CR+LF if present
                        lowBound = message.index(message.endIndex, offsetBy: -1)
                        hiBound = message.index(message.endIndex, offsetBy: 0)
                        midRange = lowBound ..< hiBound
                        message.removeSubrange(midRange)
                    }

                    // Check for delimiter proper position (has to be at the end)
                    lowBound = message.index(message.endIndex, offsetBy: -delimiterLength)
                    hiBound = message.index(message.endIndex, offsetBy: 0)
                    midRange = lowBound ..< hiBound

                    let delimiter = message.substring(with: midRange)

                    if (delimiter == Constants.END_OF_MESSAGE_DELIMITER)  // Delimiter in proper position?
                    {
                        // Acquire the MD digest
                        lowBound = message.index(message.endIndex, offsetBy: -(MD5Length+delimiterLength))
                        hiBound = message.index(message.endIndex, offsetBy: -(delimiterLength))
                        midRange = lowBound ..< hiBound
                        let receivedMD5 = message.substring(with: midRange)

                        // Acquire the deframed message (normalized message)
                        lowBound = message.index(message.startIndex, offsetBy: 0)
                        hiBound = message.index(message.endIndex, offsetBy: -(MD5Length+delimiterLength))
                        midRange = lowBound ..< hiBound
                        let normalizedMessage = message.substring(with: midRange)

                        // Calculate the MD5 digest on the normalized message
                        let calculatedMD5Digest = normalizedMessage.md5()

                        // Debug
                        print(delimiter)
                        print(normalizedMessage)
                        print(receivedMD5)
                        print(calculatedMD5Digest!)

                        // Check for the integrity of the data
                        if (receivedMD5.lowercased() == calculatedMD5Digest?.lowercased()) || self.noMD5Check  // TEMPORARY
                        {
                            if (normalizedMessage == "Unauthorized Access")
                            {
                                // Update the authorization status
                                self.authorized = false

                                // Stop the refresh control
                                if let refreshControl = self.refreshControl {
                                    if refreshControl.isRefreshing {
                                        refreshControl.endRefreshing()
                                    }
                                }

                                // Stop the stream
                                NSLog("stream task stop")
                                self.stop(task: task)

                                // Shows an alert
                                self.showAlert(title: NSLocalizedString("Unauthorized Access", comment: "Unauthorized Access Title"), message: NSLocalizedString("Please login with the proper Username and Password before to send any command!", comment: "Unauthorized Access Message"))                                    
                            }
                            else if (normalizedMessage == "System Busy")
                            {
                                // Stop the refresh control
                                if let refreshControl = self.refreshControl {
                                    if refreshControl.isRefreshing {
                                        refreshControl.endRefreshing()
                                    }
                                }

                                // Stop the stream
                                NSLog("stream task stop")
                                self.stop(task: task)

                                // Shows an alert
                                self.showAlert(title: NSLocalizedString("System Busy", comment: "System Busy Title"), message: NSLocalizedString("The system is busy at the moment. Only one connection at a time is allowed!", comment: "System Busy Message"))
                            }
                            else if (normalizedMessage == "Error")
                            {
                                // Stop the refresh control
                                if let refreshControl = self.refreshControl {
                                    if refreshControl.isRefreshing {
                                        refreshControl.endRefreshing()
                                    }
                                }

                                // Stop the stream
                                NSLog("stream task stop")
                                self.stop(task: task)

                                // Shows an alert
                                self.showAlert(title: NSLocalizedString("Error", comment: "Error Title"), message: NSLocalizedString("An error occurred during the execution of the command!", comment: "Command Error Message"))
                            }
                            else if (normalizedMessage == "ErrorMachineRunning")
                            {
                                // Stop the refresh control
                                if let refreshControl = self.refreshControl {
                                    if refreshControl.isRefreshing {
                                        refreshControl.endRefreshing()
                                    }
                                }

                                // Stop the stream
                                NSLog("stream task stop")
                                self.stop(task: task)

                                // Shows an alert
                                self.showAlert(title: NSLocalizedString("Error", comment: "Error Title"), message: NSLocalizedString("The command cannot be executed while the machine is running", comment: "Machine Running Message 1")+"!\r\n\n "+NSLocalizedString("Trying to execute any command in this state could be dangerous for both people and machinery", comment: "Machine Running Message 2")+".\r\n\n "+NSLocalizedString("Please stop the machine and leave the automatic or semi-automatic modes before to provide any command", comment: "Machine Running Message 3")+".")
                            }
                            else if (normalizedMessage == "Command Not Recognized")
                            {
                                // Stop the refresh control
                                if let refreshControl = self.refreshControl {
                                    if refreshControl.isRefreshing {
                                        refreshControl.endRefreshing()
                                    }
                                }

                                // Stop the stream
                                NSLog("stream task stop")
                                self.stop(task: task)

                                // Shows an alert
                                self.showAlert(title: NSLocalizedString("Error", comment: "Error Title"), message: NSLocalizedString("Command not recognized!", comment: "Command Unrecognized Message"))
                            }
                            else
                            {
                                // Stop the refresh control
                                if let refreshControl = self.refreshControl {
                                    if refreshControl.isRefreshing {
                                        refreshControl.endRefreshing()
                                    }
                                }

                                // Stop the stream
                                NSLog("stream task stop")
                                self.stop(task: task)

                                //let testMessage = "test\r\nf3ea0b9bff4a2c79e60acf6873f4a1ce</EOM>\r\n"
                                //normalizedMessage = testMessage

                                // Process the received csv file
                                self.processCsvData(file: normalizedMessage)
                            }
                        }
                        else
                        {
                            // Stop the refresh control
                            if let refreshControl = self.refreshControl {
                                if refreshControl.isRefreshing {
                                    refreshControl.endRefreshing()
                                }
                            }

                            // Stop the stream
                            NSLog("stream task stop")
                            self.stop(task: task)

                            // Shows an alert
                            self.showAlert(title: NSLocalizedString("Data Error", comment: "Data Error Title"), message: NSLocalizedString("The received data cannot be read since it's corrupted or incomplete!", comment: "Data Error Message"))
                        }

                    }
                    else
                    {
                        // Stop the refresh control
                        if let refreshControl = self.refreshControl {
                            if refreshControl.isRefreshing {
                                refreshControl.endRefreshing()
                            }
                        }

                        // Stop the stream
                        NSLog("stream task stop")
                        self.stop(task: task)

                        // Shows an alert
                        self.showAlert(title: NSLocalizedString("Data Error", comment: "Data Error Title"), message: NSLocalizedString("The received data cannot be read since it's corrupted or incomplete!", comment: "Data Error Message"))
                    }
                }
            }
        }
        if eof {
            // Stop the refresh control
            if let refreshControl = self.refreshControl {
                if refreshControl.isRefreshing {
                    refreshControl.endRefreshing()
                }
            }

            // Refresh the tableview content
            self.tableView.reloadData()

            // Stop the stream
            NSLog("stream task end")
            self.stop(task: task)

        } else if error == nil {
            self.startRead(for: task)
        } else {
            // We ignore the error because we'll see it again in `didCompleteWithError`.
            NSLog("stream task read error")
        }
    }
}

【问题讨论】:

  • data 是完整的字符串数据还是只是部分字符串的数据?
  • 数据来自实现接收缓冲区处理的函数,因此字节不一定同时到达。我使用转换后的字符串来构建整个消息,一旦检测到终止字符(我实现了一个简单的协议),我就关闭套接字并分析接收到的消息。一旦在缓冲区中接收到由超过 1 个字节表示的某些字符,接收缓冲区就会被锁定,因为无法再创建已转换的字符串(我得到一个 nil)。
  • 查看我的回答,它解释了您的方法中的缺陷以及您必须做什么。
  • 谢谢rmaddy,我已经添加了更完整的代码部分,向您展示如何完成数据的接收。如您所见,我使用收到的数据来构建部分消息,直到获得用于关闭套接字并开始分析的“消息结束”分隔符。我对数据流没有那么丰富的经验,所以您对我可以修改我的函数以在转换为字符串之前接收整串字节有什么建议吗?目前我需要将分隔符代码作为字符串来标记消息接收的结束..
  • 通常你应该以字节数开始你的数据。假设前 4 个字节代表一些商定的“字节顺序”中的 32 位整数。您读取这 4 个字节以获取长度。然后你读取数据,直到你得到更多的字节。然后你知道你在消息的末尾。尝试在数据末尾使用“消息结束”标记的问题在于,“消息结束”标记可能会在读取中拆分。无论哪种方式,您都需要重构代码以在数据级别进行处理,并且在读取所有字符串数据之前不要尝试将数据转换为字符串。

标签: ios utf-8 swift3


【解决方案1】:

data 代表整个字符串的数据,而不仅仅是一个子字符串,这一点至关重要。如果您尝试从整个字符串的部分数据转换子字符串,很多情况下会失败。

它适用于 1 字节字符,因为无论你在哪里截断数据流,部分数据仍然代表一个有效的字符串。但是一旦开始处理多字节字符,部分数据流很容易导致数据的第一个或最后一个字节只是多字节字符的一部分。这会阻止数据被正确解释。

因此,在尝试将数据转换为字符串之前,您必须确保使用给定字符串的所有字节构建一个 data 对象。

通常,您应该以字节数开始数据。假设前 4 个字节代表一些商定的“字节顺序”中的 32 位整数。您读取这 4 个字节以获取长度。然后你读取数据,直到你得到更多的字节。然后你就知道你在消息的末尾了。

尝试在数据末尾使用“消息结束”标记的问题在于,“消息结束”标记可能会在读取中拆分。无论哪种方式,您都需要重构代码以在数据级别进行处理,并且在读取所有字符串数据之前不要尝试将数据转换为字符串。

【讨论】:

    【解决方案2】:

    如您所知,单个 UTF-8 字符是 1、2、3 或 4 个字节。 对于您的情况,您需要处理 1 个或 2 个字节的字符。并且您的接收字节序列可能未与“字符边界”对齐。 但是,正如 rmaddy 所指出的,String.Encoding.utf8 的字节序列必须以右边界开始和结束。

    现在,有两种方法可以处理这种情况。 正如 rmaddy 建议的那样,一种是首先发送长度并计算传入的数据字节数。 这样做的缺点是您还必须修改传输(服务器)端,这可能是不可能的。

    另一种选择是逐字节扫描传入的序列并跟踪字符边界,然后构建合法的 UTF-8 字节序列。 幸运的是,UTF-8 的设计使您可以轻松识别字符边界的位置 通过查看字节流中的任何字节。具体来说,1、2、3 和 4 字节 UTF-8 字符的第一个字节分别以 0xxxxxxx、110xxxxxx、1110xxxx 和 11110xxx 开头,第二个..第四个字节 在位表示中都是 10xxxxxx。这让您的生活更轻松。

    如果您从 1 字节 UTF-8 字符之一中选择“消息结束”标记, 您可以在不考虑字节序列的情况下轻松成功地检测 EOM,因为它是单个字节并且不会出现在 2..4 字节字符中的任何位置。

    【讨论】:

    • 谢谢beshio...事实上,这就是我的想法(我必须做的工作是调整服务器和客户端以改变我处理消息边界的方法)。您的解决方案似乎很有趣,我将对此进行评估。我的消息结束分隔符实际上是以下字符串:“”(类似于 xml)。它仍然是一堆 1 字节字符,因此您的建议似乎适用。我想我需要扫描传入的 6 字节数据流组以识别分隔符并在末尾创建整个字符串过程。
    • 只要您在“接收缓冲区”级别检测到您的 EOM,即在转换为 Swift 字符串之前,我认为您的六字节 EOM 序列方法应该可以工作,因为所有六个字符都是 1 字节 UTF- 8、不要和其他的混在一起,即使中途被打断,只要你管理好接收缓冲区即可。
    • 然而,单字节 EOM 是无状态且方便的,我的意思是我们可以立即终止而不用担心“未来/过去”。如果需要,您可以同时使用这两种 EOM,但这在很大程度上取决于您的应用程序和服务器-客户端连接的可靠性。例如,您可以使用单字节 EOM,例如 0x03(End Of Text char)作为“低级协议事件”来关闭 TCP 会话,同时保持“”作为“高级协议”常规事件并优雅地执行关闭 TCP 会话之前的一些任务。
    • 嗨 beshio,听起来不错,谢谢。我没有考虑将转义字符 0x03 直接用作分隔符的可能性。我会尽快尝试你的建议。再次感谢您的好建议! ;-)
    • 我希望它有效。如果您能接受我的回答,如果它有帮助,我将不胜感激。谢谢!
    猜你喜欢
    • 2018-02-26
    • 1970-01-01
    • 1970-01-01
    • 2012-07-17
    • 2017-05-18
    • 2018-10-05
    • 2016-05-07
    • 1970-01-01
    • 2013-09-14
    相关资源
    最近更新 更多