Skip to content

Commit ae7d8ca

Browse files
committed
add example h2c client
1 parent 717c9b6 commit ae7d8ca

File tree

1 file changed

+359
-0
lines changed

1 file changed

+359
-0
lines changed

examples/h2c_client.rs

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
//! Example toy h2c client using libnghttp2.
2+
3+
use libnghttp2::*;
4+
use std::io::{self, Read, Write};
5+
use std::net::TcpStream;
6+
use std::{ptr, slice};
7+
8+
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
9+
10+
struct Response {
11+
status: u16,
12+
headers: Vec<(String, String)>,
13+
body: Vec<u8>,
14+
}
15+
16+
impl Response {
17+
fn body_str(&self) -> Result<&str> {
18+
Ok(std::str::from_utf8(&self.body)?)
19+
}
20+
}
21+
22+
struct Request {
23+
authority: String,
24+
path: String,
25+
}
26+
27+
impl Request {
28+
fn builder() -> RequestBuilder {
29+
RequestBuilder::default()
30+
}
31+
}
32+
33+
#[derive(Default)]
34+
struct RequestBuilder {
35+
authority: Option<String>,
36+
path: Option<String>,
37+
}
38+
39+
impl RequestBuilder {
40+
fn authority(mut self, authority: impl Into<String>) -> Self {
41+
self.authority = Some(authority.into());
42+
self
43+
}
44+
45+
fn path(mut self, path: impl Into<String>) -> Self {
46+
self.path = Some(path.into());
47+
self
48+
}
49+
50+
fn build(self) -> Result<Request> {
51+
Ok(Request {
52+
authority: self.authority.ok_or("authority is required")?,
53+
path: self.path.unwrap_or_else(|| "/".into()),
54+
})
55+
}
56+
}
57+
58+
struct Session {
59+
ptr: *mut nghttp2_session,
60+
callbacks: *mut nghttp2_session_callbacks,
61+
}
62+
63+
impl Session {
64+
fn new(user_data: *mut std::os::raw::c_void) -> Result<Self> {
65+
unsafe {
66+
let mut callbacks = ptr::null_mut();
67+
check(nghttp2_session_callbacks_new(&mut callbacks))?;
68+
69+
nghttp2_session_callbacks_set_send_callback(callbacks, Some(send_cb));
70+
nghttp2_session_callbacks_set_on_frame_recv_callback(
71+
callbacks,
72+
Some(frame_recv_cb),
73+
);
74+
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
75+
callbacks,
76+
Some(data_cb),
77+
);
78+
nghttp2_session_callbacks_set_on_header_callback(
79+
callbacks,
80+
Some(header_cb),
81+
);
82+
nghttp2_session_callbacks_set_on_stream_close_callback(
83+
callbacks,
84+
Some(stream_close_cb),
85+
);
86+
87+
let mut session_ptr = ptr::null_mut();
88+
check(nghttp2_session_client_new(
89+
&mut session_ptr,
90+
callbacks,
91+
user_data,
92+
))?;
93+
94+
check(nghttp2_submit_settings(
95+
session_ptr,
96+
NGHTTP2_FLAG_NONE as u8,
97+
ptr::null(),
98+
0,
99+
))?;
100+
101+
Ok(Self {
102+
ptr: session_ptr,
103+
callbacks,
104+
})
105+
}
106+
}
107+
108+
fn submit(&mut self, req: &Request) -> Result<i32> {
109+
unsafe {
110+
let headers = [
111+
header(":method", "GET"),
112+
header(":scheme", "http"),
113+
header(":authority", &req.authority),
114+
header(":path", &req.path),
115+
];
116+
117+
let stream_id = nghttp2_submit_request(
118+
self.ptr,
119+
ptr::null(),
120+
headers.as_ptr(),
121+
headers.len(),
122+
ptr::null(),
123+
ptr::null_mut(),
124+
);
125+
126+
check(stream_id)?;
127+
check(nghttp2_session_send(self.ptr))?;
128+
Ok(stream_id)
129+
}
130+
}
131+
132+
fn wants_io(&self) -> bool {
133+
unsafe {
134+
nghttp2_session_want_read(self.ptr) != 0
135+
|| nghttp2_session_want_write(self.ptr) != 0
136+
}
137+
}
138+
139+
fn recv(&mut self, data: &[u8]) -> Result<()> {
140+
unsafe {
141+
check(
142+
nghttp2_session_mem_recv(self.ptr, data.as_ptr(), data.len()) as i32,
143+
)?;
144+
check(nghttp2_session_send(self.ptr))
145+
}
146+
}
147+
}
148+
149+
impl Drop for Session {
150+
fn drop(&mut self) {
151+
unsafe {
152+
if !self.ptr.is_null() {
153+
nghttp2_session_del(self.ptr);
154+
}
155+
if !self.callbacks.is_null() {
156+
nghttp2_session_callbacks_del(self.callbacks);
157+
}
158+
}
159+
}
160+
}
161+
162+
struct Context {
163+
stream: TcpStream,
164+
response: Response,
165+
stream_id: i32,
166+
done: bool,
167+
}
168+
169+
struct Client {
170+
context: Box<Context>,
171+
session: Session,
172+
}
173+
174+
impl Client {
175+
fn connect(host: &str) -> Result<Self> {
176+
let stream = TcpStream::connect(format!("{}:80", host))?;
177+
178+
let mut context = Box::new(Context {
179+
stream,
180+
response: Response {
181+
status: 0,
182+
headers: Vec::new(),
183+
body: Vec::new(),
184+
},
185+
stream_id: -1,
186+
done: false,
187+
});
188+
189+
let user_data = context.as_mut() as *mut Context as *mut _;
190+
let session = Session::new(user_data)?;
191+
192+
Ok(Self { context, session })
193+
}
194+
195+
fn execute(mut self, request: Request) -> Result<Response> {
196+
self.context.stream_id = self.session.submit(&request)?;
197+
198+
let mut buf = [0u8; 16384];
199+
while !self.context.done && self.session.wants_io() {
200+
match self.context.stream.read(&mut buf) {
201+
Ok(0) => break,
202+
Ok(n) => self.session.recv(&buf[..n])?,
203+
Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue,
204+
Err(e) => return Err(e.into()),
205+
}
206+
}
207+
208+
Ok(std::mem::replace(
209+
&mut self.context.response,
210+
Response {
211+
status: 0,
212+
headers: Vec::new(),
213+
body: Vec::new(),
214+
},
215+
))
216+
}
217+
}
218+
219+
fn check(code: i32) -> Result<()> {
220+
if code < 0 {
221+
Err(format!("nghttp2 error: {}", code).into())
222+
} else {
223+
Ok(())
224+
}
225+
}
226+
227+
fn header(name: &str, value: &str) -> nghttp2_nv {
228+
let name = name.as_bytes();
229+
let value = value.as_bytes();
230+
nghttp2_nv {
231+
name: name.as_ptr() as *mut u8,
232+
value: value.as_ptr() as *mut u8,
233+
namelen: name.len(),
234+
valuelen: value.len(),
235+
flags: NGHTTP2_NV_FLAG_NONE as u8,
236+
}
237+
}
238+
239+
unsafe fn to_str(ptr: *const u8, len: usize) -> String {
240+
String::from_utf8_lossy(unsafe { slice::from_raw_parts(ptr, len) }).into()
241+
}
242+
243+
unsafe extern "C" fn send_cb(
244+
_: *mut nghttp2_session,
245+
data: *const u8,
246+
len: usize,
247+
_: i32,
248+
user_data: *mut std::os::raw::c_void,
249+
) -> isize {
250+
unsafe {
251+
let ctx = &mut *(user_data as *mut Context);
252+
match ctx.stream.write_all(slice::from_raw_parts(data, len)) {
253+
Ok(_) => len as isize,
254+
Err(_) => NGHTTP2_ERR_CALLBACK_FAILURE as isize,
255+
}
256+
}
257+
}
258+
259+
unsafe extern "C" fn data_cb(
260+
_: *mut nghttp2_session,
261+
_: u8,
262+
stream_id: i32,
263+
data: *const u8,
264+
len: usize,
265+
user_data: *mut std::os::raw::c_void,
266+
) -> i32 {
267+
unsafe {
268+
let ctx = &mut *(user_data as *mut Context);
269+
if stream_id == ctx.stream_id {
270+
ctx
271+
.response
272+
.body
273+
.extend_from_slice(slice::from_raw_parts(data, len));
274+
}
275+
0
276+
}
277+
}
278+
279+
unsafe extern "C" fn frame_recv_cb(
280+
_: *mut nghttp2_session,
281+
frame: *const nghttp2_frame,
282+
user_data: *mut std::os::raw::c_void,
283+
) -> i32 {
284+
unsafe {
285+
let ctx = &mut *(user_data as *mut Context);
286+
if (*frame).hd.stream_id == ctx.stream_id
287+
&& ((*frame).hd.flags & NGHTTP2_FLAG_END_STREAM as u8) != 0
288+
{
289+
ctx.done = true;
290+
}
291+
0
292+
}
293+
}
294+
295+
unsafe extern "C" fn header_cb(
296+
_: *mut nghttp2_session,
297+
frame: *const nghttp2_frame,
298+
name: *const u8,
299+
namelen: usize,
300+
value: *const u8,
301+
valuelen: usize,
302+
_: u8,
303+
user_data: *mut std::os::raw::c_void,
304+
) -> i32 {
305+
unsafe {
306+
let ctx = &mut *(user_data as *mut Context);
307+
if (*frame).hd.stream_id == ctx.stream_id {
308+
let name_str = to_str(name, namelen);
309+
let value_str = to_str(value, valuelen);
310+
311+
if name_str == ":status" {
312+
ctx.response.status = value_str.parse().unwrap_or(0);
313+
}
314+
ctx.response.headers.push((name_str, value_str));
315+
}
316+
0
317+
}
318+
}
319+
320+
unsafe extern "C" fn stream_close_cb(
321+
_: *mut nghttp2_session,
322+
stream_id: i32,
323+
_: u32,
324+
user_data: *mut std::os::raw::c_void,
325+
) -> i32 {
326+
unsafe {
327+
let ctx = &mut *(user_data as *mut Context);
328+
if stream_id == ctx.stream_id {
329+
ctx.done = true;
330+
}
331+
0
332+
}
333+
}
334+
335+
fn main() -> Result<()> {
336+
let request = Request::builder()
337+
.authority("nghttp2.org")
338+
.path("/")
339+
.build()?;
340+
341+
let client = Client::connect("nghttp2.org")?;
342+
let response = client.execute(request)?;
343+
344+
println!("Status: {}", response.status);
345+
println!("\nHeaders:");
346+
for (name, value) in
347+
response.headers.iter().filter(|(n, _)| !n.starts_with(':'))
348+
{
349+
println!(" {}: {}", name, value);
350+
}
351+
352+
println!("\nBody ({} bytes):", response.body.len());
353+
if let Ok(body) = response.body_str() {
354+
let preview = body.chars().take(500).collect::<String>();
355+
println!("{}{}", preview, if body.len() > 500 { "..." } else { "" });
356+
}
357+
358+
Ok(())
359+
}

0 commit comments

Comments
 (0)