Skip to content

Commit 8ba9a8d

Browse files
committed
feat(body): add body::aggregate and body::to_bytes functions
Adds utility functions to `hyper::body` to help asynchronously collecting all the buffers of some `HttpBody` into one. - `aggregate` will collect all into an `impl Buf` without copying the contents. This is ideal if you don't need a contiguous buffer. - `to_bytes` will copy all the data into a single contiguous `Bytes` buffer.
1 parent 5a59875 commit 8ba9a8d

File tree

15 files changed

+282
-128
lines changed

15 files changed

+282
-128
lines changed

Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ required-features = ["runtime"]
9898
[[example]]
9999
name = "client_json"
100100
path = "examples/client_json.rs"
101-
required-features = ["runtime", "stream"]
101+
required-features = ["runtime"]
102102

103103
[[example]]
104104
name = "echo"
@@ -162,6 +162,11 @@ path = "examples/web_api.rs"
162162
required-features = ["runtime", "stream"]
163163

164164

165+
[[bench]]
166+
name = "body"
167+
path = "benches/body.rs"
168+
required-features = ["runtime", "stream"]
169+
165170
[[bench]]
166171
name = "connect"
167172
path = "benches/connect.rs"

benches/body.rs

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#![feature(test)]
2+
#![deny(warnings)]
3+
4+
extern crate test;
5+
6+
use bytes::Buf;
7+
use futures_util::stream;
8+
use futures_util::StreamExt;
9+
use hyper::body::Body;
10+
11+
macro_rules! bench_stream {
12+
($bencher:ident, bytes: $bytes:expr, count: $count:expr, $total_ident:ident, $body_pat:pat, $block:expr) => {{
13+
let mut rt = tokio::runtime::Builder::new()
14+
.basic_scheduler()
15+
.build()
16+
.expect("rt build");
17+
18+
let $total_ident: usize = $bytes * $count;
19+
$bencher.bytes = $total_ident as u64;
20+
let __s: &'static [&'static [u8]] = &[&[b'x'; $bytes] as &[u8]; $count] as _;
21+
22+
$bencher.iter(|| {
23+
rt.block_on(async {
24+
let $body_pat = Body::wrap_stream(
25+
stream::iter(__s.iter()).map(|&s| Ok::<_, std::convert::Infallible>(s)),
26+
);
27+
$block;
28+
});
29+
});
30+
}};
31+
}
32+
33+
macro_rules! benches {
34+
($($name:ident, $bytes:expr, $count:expr;)+) => (
35+
mod aggregate {
36+
use super::*;
37+
38+
$(
39+
#[bench]
40+
fn $name(b: &mut test::Bencher) {
41+
bench_stream!(b, bytes: $bytes, count: $count, total, body, {
42+
let buf = hyper::body::aggregate(body).await.unwrap();
43+
assert_eq!(buf.remaining(), total);
44+
});
45+
}
46+
)+
47+
}
48+
49+
mod manual_into_vec {
50+
use super::*;
51+
52+
$(
53+
#[bench]
54+
fn $name(b: &mut test::Bencher) {
55+
bench_stream!(b, bytes: $bytes, count: $count, total, mut body, {
56+
let mut vec = Vec::new();
57+
while let Some(chunk) = body.next().await {
58+
vec.extend_from_slice(&chunk.unwrap());
59+
}
60+
assert_eq!(vec.len(), total);
61+
});
62+
}
63+
)+
64+
}
65+
66+
mod to_bytes {
67+
use super::*;
68+
69+
$(
70+
#[bench]
71+
fn $name(b: &mut test::Bencher) {
72+
bench_stream!(b, bytes: $bytes, count: $count, total, body, {
73+
let bytes = hyper::body::to_bytes(body).await.unwrap();
74+
assert_eq!(bytes.len(), total);
75+
});
76+
}
77+
)+
78+
}
79+
)
80+
}
81+
82+
// ===== Actual Benchmarks =====
83+
84+
benches! {
85+
bytes_1_000_count_2, 1_000, 2;
86+
bytes_1_000_count_10, 1_000, 10;
87+
bytes_10_000_count_1, 10_000, 1;
88+
bytes_10_000_count_10, 10_000, 10;
89+
}

examples/client.rs

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ async fn fetch_url(url: hyper::Uri) -> Result<()> {
4040
println!("Response: {}", res.status());
4141
println!("Headers: {:#?}\n", res.headers());
4242

43+
// Stream the body, writing each chunk to stdout as we get it
44+
// (instead of buffering and printing at the end).
4345
while let Some(next) = res.body_mut().data().await {
4446
let chunk = next?;
4547
io::stdout().write_all(&chunk).await?;

examples/client_json.rs

+7-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#[macro_use]
55
extern crate serde_derive;
66

7-
use futures_util::StreamExt;
7+
use bytes::buf::BufExt as _;
88
use hyper::Client;
99

1010
// A simple type alias so as to DRY.
@@ -27,14 +27,13 @@ async fn fetch_json(url: hyper::Uri) -> Result<Vec<User>> {
2727
let client = Client::new();
2828

2929
// Fetch the url...
30-
let mut res = client.get(url).await?;
31-
// asynchronously concatenate chunks of the body
32-
let mut body = Vec::new();
33-
while let Some(chunk) = res.body_mut().next().await {
34-
body.extend_from_slice(&chunk?);
35-
}
30+
let res = client.get(url).await?;
31+
32+
// asynchronously aggregate the chunks of the body
33+
let body = hyper::body::aggregate(res.into_body()).await?;
34+
3635
// try to parse as json with serde_json
37-
let users = serde_json::from_slice(&body)?;
36+
let users = serde_json::from_reader(body.reader())?;
3837

3938
Ok(users)
4039
}

examples/echo.rs

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
//#![deny(warnings)]
1+
#![deny(warnings)]
22

3-
use futures_util::{StreamExt, TryStreamExt};
3+
use futures_util::TryStreamExt;
44
use hyper::service::{make_service_fn, service_fn};
55
use hyper::{Body, Method, Request, Response, Server, StatusCode};
66

77
/// This is our service handler. It receives a Request, routes on its
88
/// path, and returns a Future of a Response.
9-
async fn echo(mut req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
9+
async fn echo(req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
1010
match (req.method(), req.uri().path()) {
1111
// Serve some instructions at /
1212
(&Method::GET, "/") => Ok(Response::new(Body::from(
@@ -34,10 +34,7 @@ async fn echo(mut req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
3434
// So here we do `.await` on the future, waiting on concatenating the full body,
3535
// then afterwards the content can be reversed. Only then can we return a `Response`.
3636
(&Method::POST, "/echo/reversed") => {
37-
let mut whole_body = Vec::new();
38-
while let Some(chunk) = req.body_mut().next().await {
39-
whole_body.extend_from_slice(&chunk?);
40-
}
37+
let whole_body = hyper::body::to_bytes(req.into_body()).await?;
4138

4239
let reversed_body = whole_body.iter().rev().cloned().collect::<Vec<u8>>();
4340
Ok(Response::new(Body::from(reversed_body)))

examples/params.rs

+2-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
use hyper::service::{make_service_fn, service_fn};
55
use hyper::{Body, Method, Request, Response, Server, StatusCode};
66

7-
use futures_util::StreamExt;
87
use std::collections::HashMap;
98
use url::form_urlencoded;
109

@@ -13,15 +12,12 @@ static MISSING: &[u8] = b"Missing field";
1312
static NOTNUMERIC: &[u8] = b"Number field is not numeric";
1413

1514
// Using service_fn, we can turn this function into a `Service`.
16-
async fn param_example(mut req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
15+
async fn param_example(req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
1716
match (req.method(), req.uri().path()) {
1817
(&Method::GET, "/") | (&Method::GET, "/post") => Ok(Response::new(INDEX.into())),
1918
(&Method::POST, "/post") => {
2019
// Concatenate the body...
21-
let mut b = Vec::new();
22-
while let Some(chunk) = req.body_mut().next().await {
23-
b.extend_from_slice(&chunk?);
24-
}
20+
let b = hyper::body::to_bytes(req.into_body()).await?;
2521
// Parse the request body. form_urlencoded::parse
2622
// always succeeds, but in general parsing may
2723
// fail (for example, an invalid post of json), so

examples/web_api.rs

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![deny(warnings)]
22

3-
use futures_util::{StreamExt, TryStreamExt};
3+
use bytes::buf::BufExt;
4+
use futures_util::{stream, StreamExt};
45
use hyper::client::HttpConnector;
56
use hyper::service::{make_service_fn, service_fn};
67
use hyper::{header, Body, Client, Method, Request, Response, Server, StatusCode};
@@ -24,25 +25,24 @@ async fn client_request_response(client: &Client<HttpConnector>) -> Result<Respo
2425

2526
let web_res = client.request(req).await?;
2627
// Compare the JSON we sent (before) with what we received (after):
27-
let body = Body::wrap_stream(web_res.into_body().map_ok(|b| {
28-
format!(
29-
"<b>POST request body</b>: {}<br><b>Response</b>: {}",
28+
let before = stream::once(async {
29+
Ok(format!(
30+
"<b>POST request body</b>: {}<br><b>Response</b>: ",
3031
POST_DATA,
31-
std::str::from_utf8(&b).unwrap()
3232
)
33-
}));
33+
.into())
34+
});
35+
let after = web_res.into_body();
36+
let body = Body::wrap_stream(before.chain(after));
3437

3538
Ok(Response::new(body))
3639
}
3740

38-
async fn api_post_response(mut req: Request<Body>) -> Result<Response<Body>> {
39-
// Concatenate the body...
40-
let mut whole_body = Vec::new();
41-
while let Some(chunk) = req.body_mut().next().await {
42-
whole_body.extend_from_slice(&chunk?);
43-
}
41+
async fn api_post_response(req: Request<Body>) -> Result<Response<Body>> {
42+
// Aggregate the body...
43+
let whole_body = hyper::body::aggregate(req.into_body()).await?;
4444
// Decode as JSON...
45-
let mut data: serde_json::Value = serde_json::from_slice(&whole_body)?;
45+
let mut data: serde_json::Value = serde_json::from_reader(whole_body.reader())?;
4646
// Change the JSON...
4747
data["test"] = serde_json::Value::from("test_value");
4848
// And respond with the new JSON.

src/body/aggregate.rs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use bytes::Buf;
2+
3+
use super::HttpBody;
4+
use crate::common::buf::BufList;
5+
6+
/// Aggregate the data buffers from a body asynchronously.
7+
///
8+
/// The returned `impl Buf` groups the `Buf`s from the `HttpBody` without
9+
/// copying them. This is ideal if you don't require a contiguous buffer.
10+
pub async fn aggregate<T>(body: T) -> Result<impl Buf, T::Error>
11+
where
12+
T: HttpBody,
13+
{
14+
let mut bufs = BufList::new();
15+
16+
futures_util::pin_mut!(body);
17+
while let Some(buf) = body.data().await {
18+
let buf = buf?;
19+
if buf.has_remaining() {
20+
bufs.push(buf);
21+
}
22+
}
23+
24+
Ok(bufs)
25+
}

src/body/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@
1818
pub use bytes::{Buf, Bytes};
1919
pub use http_body::Body as HttpBody;
2020

21+
pub use self::aggregate::aggregate;
2122
pub use self::body::{Body, Sender};
23+
pub use self::to_bytes::to_bytes;
24+
2225
pub(crate) use self::payload::Payload;
2326

27+
mod aggregate;
2428
mod body;
2529
mod payload;
30+
mod to_bytes;
2631

2732
/// An optimization to try to take a full body if immediately available.
2833
///

src/body/to_bytes.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use bytes::{Buf, BufMut, Bytes};
2+
3+
use super::HttpBody;
4+
5+
/// dox
6+
pub async fn to_bytes<T>(body: T) -> Result<Bytes, T::Error>
7+
where
8+
T: HttpBody,
9+
{
10+
futures_util::pin_mut!(body);
11+
12+
// If there's only 1 chunk, we can just return Buf::to_bytes()
13+
let mut first = if let Some(buf) = body.data().await {
14+
buf?
15+
} else {
16+
return Ok(Bytes::new());
17+
};
18+
19+
let second = if let Some(buf) = body.data().await {
20+
buf?
21+
} else {
22+
return Ok(first.to_bytes());
23+
};
24+
25+
// With more than 1 buf, we gotta flatten into a Vec first.
26+
let cap = first.remaining() + second.remaining() + body.size_hint().lower() as usize;
27+
let mut vec = Vec::with_capacity(cap);
28+
vec.put(first);
29+
vec.put(second);
30+
31+
while let Some(buf) = body.data().await {
32+
vec.put(buf?);
33+
}
34+
35+
Ok(vec.into())
36+
}

0 commit comments

Comments
 (0)