原文:Why is there trailing garbage when I try to decode the bytes of a HttpContent object? – The Old New Thing (microsoft.com)September 23rd, 2021
一个客户无法从HTTP响应中提取正确的文本:
- winrt::HttpRequest request = ...; auto result = co_await request.Content().ReadAsStringAsync(); -
- {"name":"ðAPCDCS±meow"} -
这个版本生成的字符串看起来基本正常,但有些部分损坏了。
从检查来看,很明显这里有mojibake(🐱),其中一个UTF-8字符串被错误的解读为其它8位字符集。
根据RFC 2616第3.7.1节,如果文本媒体子类型没有显式指定字符集,则默认字符集为ISO-8859-1(Latin-1)。显然,该服务器返回一个编码为UTF-8的字符串,但在报告其Content-Type时未包含显式字符集解释。因此,该字符串默认为ISO-8859-1。
哦~~
现在,RFC 2616的3.4.1节承认,HTTP客户端通常会对缺乏显式字符集解释的返回进行最佳可能性猜测。如果没有提供字符集,Windows在运行时确实会对字符集进行一些猜测:
- 如果缓冲区以UTF-8 BOM、UTF-16LE BOM、UTF-16BE BOM或GB18030 BOM开头,则根据该字符集对缓冲区进行解码。
- 如果内容类型是application/json或者是*+json的形式,那么它被解码为UTF-8。
以上实际情况中,服务端既没有显示指定UTF-8 BOM,也没有将内返回容类型设置为application/json。所以,即便他最后返回的是一个json对象,也不满足上面猜测的每一步,而被解释为IOS-8859-1编码。
哦~~
好吧,让我们来尝试在获取服务器响应数据时,将它们显式解码为UTF-8来解决这个问题(明显非常坏的)。
std::wstring Utf8ToUtf16(char const* str) { std::wstring result; if (str) { auto resultLen = MultiByteToWideChar( CP_UTF8, MB_ERR_INVALID_CHARS, str, -1, nullptr, 0); if (resultLen) { result.resize(resultLen); MultiByteToWideChar( CP_UTF8, MB_ERR_INVALID_CHARS, str, -1, result.data(), resultLen); } } return result; } winrt::HttpRequest request = ...; auto buffer = co_await request.Content().ReadAsBufferAsync(); auto result = Utf8ToUtf16((char const*)buffer.data());
这个版本运行得更好,但却在尾部出现了一些乱码:
- {"name":"🐱meow"}SOH -
在这种情况下,问题不是缓冲区数据,而是如何将缓冲区转换为字符串。data()方法返回一个指向缓冲区开头的指针,代码将其作为源字符串传递给Multi-Byte-To-Wide-Char,字符串长度为-1。
这个特殊值-1意味着该指针应被视为以空结束的字符串的开始位置。但是Read-As-Buffer-Async生成的Buffer只是从服务器返回的原始字节,服务器不会在末尾放一个空结束符。服务器说,“响应是19个字节长”,它发送19个字节,就这样。
因此,这个尾部的乱码是读缓冲区溢出,代码只是读过缓冲区的真实末端后,并“一直继续解码,直到达到空结束符”为止。
当您希望解码缓冲区中的字节数据时,您需要指定缓冲区中的长度,而不是说“一直继续解码,直到达到空结束符”。
std::wstring Utf8ToUtf16(char const* str, int32_t inputLen) { std::wstring result; if (str) { auto resultLen = MultiByteToWideChar( CP_UTF8, MB_ERR_INVALID_CHARS, str, inputLen, nullptr, 0); if (resultLen) { result.resize(resultLen); MultiByteToWideChar( CP_UTF8, MB_ERR_INVALID_CHARS, str, inputLen, result.data(), resultLen); } } return result; } winrt::HttpRequest request = ...; auto buffer = co_await request.Content().ReadAsBufferAsync(); auto result = Utf8ToUtf16( (char const*)buffer.data(), static_cast<int32_t>(buffer.Length()));
以上代码将生成实际所需的字符串解码为UTF-8,没有乱码。
现在,您不需要编写代码来获取包含UTF-8编码字符串的Buffer并将其转换为UTF-16字符串。Windows运行时已经提供了一个帮助函数:
- winrt::HttpRequest request = ...; auto buffer = co_await request.Content().ReadAsBufferAsync(); auto result = CryptographicBuffer::ConvertBinaryToString( BinaryStringEncoding::Utf8, buffer); -
但由于我们使用的是c++ /WinRT,我们完全可以使用 the conversion built into C++/WinRT we learned last time去避免这些问题。其中难点部分是从缓冲区中获取std::string_view 。
winrt::HttpRequest request = ...; auto buffer = co_await request.Content().ReadAsBufferAsync(); auto result = winrt::to_hstring( std::string_view{ static_cast<char const*>(buffer.data()), buffer.Length() });
好了,你可以这样,读取原始缓冲区并将其从UTF-8转换为UTF-16字符串。
与此同时,去修复你的服务器。
尾声:客户最终发现他们的服务器确实有错误。响应是通过回调服务器生成的,它将Content-Type报头放在ResponseHeaders中而不是contenttheaders。