我正在探索Rust,并尝试制作一个简单的HTTP请求(使用hyper-crace),并将响应体打印到控制台。响应实现std::io::Read
。阅读了各种文档来源和基本教程,我得到了以下代码,我编译了&使用RUST_BACKTRACE=1 cargo run
:执行
use hyper::client::Client;
use std::io::Read;
pub fn print_html(url: &str) {
let client = Client::new();
let req = client.get(url).send();
match req {
Ok(mut res) => {
println!("{}", res.status);
let mut body = String::new();
match res.read_to_string(&mut body) {
Ok(body) => println!("{:?}", body),
Err(why) => panic!("String conversion failure: {:?}", why)
}
},
Err(why) => panic!("{:?}", why)
}
}
预期:
由HTTP服务器传递的正文中一个漂亮的、可读的HTML内容被打印到控制台。
实际:
200 OK
thread '<main>' panicked at 'String conversion failure: Error { repr: Custom(Custom { kind: InvalidData, error: StringError("stream did not contain valid UTF-8") }) }', src/printer.rs:16
stack backtrace:
1: 0x109e1faeb - std::sys::backtrace::tracing::imp::write::h3800f45f421043b8
2: 0x109e21565 - std::panicking::default_hook::_$u7b$$u7b$closure$u7d$$u7d$::h0ef6c8db532f55dc
3: 0x109e2119e - std::panicking::default_hook::hf3839060ccbb8764
4: 0x109e177f7 - std::panicking::rust_panic_with_hook::h5dd7da6bb3d06020
5: 0x109e21b26 - std::panicking::begin_panic::h9bf160aee246b9f6
6: 0x109e18248 - std::panicking::begin_panic_fmt::haf08a9a70a097ee1
7: 0x109d54378 - libplayground::printer::print_html::hff00c339aa28fde4
8: 0x109d53d76 - playground::main::h0b7387c23270ba52
9: 0x109e20d8d - std::panicking::try::call::hbbf4746cba890ca7
10: 0x109e23fcb - __rust_try
11: 0x109e23f65 - __rust_maybe_catch_panic
12: 0x109e20bb1 - std::rt::lang_start::hbcefdc316c2fbd45
13: 0x109d53da9 - main
error: Process didn't exit successfully: `target/debug/playground` (exit code: 101)
思想
由于我从服务器收到了200 OK
,我相信我已经收到了来自服务器的有效响应(我也可以通过用更熟悉的编程语言执行相同的请求来凭经验证明这一点)。因此,错误一定是由于我错误地将字节序列转换为UTF-8字符串造成的。
备选方案
我还尝试了以下解决方案,它使我可以将字节作为一系列十六进制字符串打印到控制台,但我知道这是根本错误的,因为UTF-8字符可以有1-4个字节。因此,在本例中,尝试将单个字节转换为UTF-8字符只适用于非常有限(确切地说是255)的UTF-8字符子集。
use hyper::client::Client;
use std::io::Read;
pub fn print_html(url: &str) {
let client = Client::new();
let req = client.get(url).send();
match req {
Ok(res) => {
println!("{}", res.status);
for byte in res.bytes() {
print!("{:x}", byte.unwrap());
}
},
Err(why) => panic!("{:?}", why)
}
}
我们可以通过iconv
命令确认从http://www.google.com
返回的数据不是有效的UTF-8:
$ wget http://google.com -O page.html
$ iconv -f utf-8 page.html > /dev/null
iconv: illegal input sequence at position 5591
对于其他一些url(如http://www.reddit.com
),代码运行良好。
如果我们假设大部分数据是有效的UTF-8,我们可以使用String::from_utf8_lossy
来解决这个问题:
pub fn print_html(url: &str) {
let client = Client::new();
let req = client.get(url).send();
match req {
Ok(mut res) => {
println!("{}", res.status);
let mut body = Vec::new();
match res.read_to_end(&mut body) {
Ok(_) => println!("{:?}", String::from_utf8_lossy(&*body)),
Err(why) => panic!("String conversion failure: {:?}", why),
}
}
Err(why) => panic!("{:?}", why),
}
}
请注意,Read::read_to_string
和Read::read_to_end
返回的Ok
具有成功读取的字节数,而不是读取的数据。
如果你真的看到谷歌返回的标题:
HTTP/1.1 200 OK
Date: Fri, 22 Jul 2016 20:45:54 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=82=YwAD4Rj09u6gUA8OtQH73BUz6UlNdeRc9Z_iGjyaDqFdRGMdslypu1zsSDWQ4xRJFyEn9-UtR7U6G7HKehoyxvy9HItnDlg8iLsxzlhNcg01luW3_-HWs3l9S3dmHIVh; expires=Sat, 21-Jan-2017 20:45:54 GMT; path=/; domain=.google.ca; HttpOnly
Alternate-Protocol: 443:quic
Alt-Svc: quic=":443"; ma=2592000; v="36,35,34,33,32,31,30,29,28,27,26,25"
Accept-Ranges: none
Vary: Accept-Encoding
Transfer-Encoding: chunked
你可以看到
内容类型:text/html字符集=ISO-8859-1
另外
因此,错误一定是由于我错误地将字节序列转换为UTF-8字符串造成的。
没有转换为UTF-8。read_to_string
只是确保数据是UTF-8。
简单地说,假设任意HTML页面是用UTF-8编码的是完全不正确的。充其量,您必须解析标头以找到编码,然后转换数据。这很复杂,因为没有真正定义头文件的编码方式
一旦你找到了正确的编码,你就可以使用一个板条箱,比如编码,将结果正确地转换为UTF-8,如果结果甚至是文本!请记住,HTTP可以返回图像等二进制文件。