In this module, we’ll implement a very simple HTTP client. This client can only make GET requests to our delayserver since we just use this as a representation of a typical I/O operation and don’t care specifically about being able to do more than we need.
The first thing we’ll do is import some types and traits from the standard library as well as our Futures module:
ch07/a-coroutine/src/http.rs
use crate::future::{Future, PollState};
use std::io::{ErrorKind, Read, Write};
Next, we create a small helper function to write our HTTP requests. We’ve used this exact bit of code before in this book, so I’ll not spend time explaining it again here:
ch07/a-coroutine/src/http.rs
fn get_req(path: &str) -> String {
format!(
“GET {path} HTTP/1.1\r\n\
Host: localhost\r\n\
Connection: close\r\n\
\r\n”
)
}
So, now we can start writing our HTTP client. The implementation is very short and simple:
pub struct Http;
impl Http {
pub fn get(path: &str) -> impl Future<Output = String> {
HttpGetFuture::new(path)
}
}
We don’t really need a struct here, but we add one since we might want to add some state at a later point. It’s also a good way to group functions belonging to the HTTP client together.
Our HTTP client only has one function, get, which, eventually, will send a GET request to our delayserver with the path we specify (remember that the path is everything in bold in this example URL: http://127.0.0.1:8080/1000/HelloWorld),
The first thing you’ll notice in the function body is that there is not much happening here. We only return HttpGetFuture and that’s it.
In the function signature, you see that it returns an object implementing the Future trait that outputs a String when it’s resolved. The string we return from this function will be the response we get from the server.
Now, we could have implemented the future trait directly on the Http struct, but I think it’s a better design to allow one Http instance to give out multiple Futures instead of making the Http implement Future itself.
Let’s take a closer look at HttpGetFuture since there is much more happening there.
Just to point this out so that there is no doubt going forward, HttpGetFuture is an example of a leaf future, and it will be the only leaf future we’ll use in this example.
Let’s add the struct declaration to the file:
ch07/a-coroutine/src/http.r
struct HttpGetFuture {
stream: Option<mio::net::TcpStream>,
buffer: Vec<u8>,
path: String,
}
This data structure will hold onto some data for us:
- stream: This holds an Option<mio::net::TcpStream>. This will be an Option since we won’t connect to the stream at the same point as we create this structure.
- buffer: We’ll read the data from the TcpStream and put it all in this buffer until we’ve read all the dat returned from the server.
- path: This simply stores the path for our GET request so we can use it later.
The next thing we’ll take a look at is the impl block for our HttpGetFuture:
ch07/a-coroutine/src/http.rs
impl HttpGetFuture {
fn new(path: &’static str) -> Self {
Self {
stream: None,
buffer: vec![],
Path: path.to_string(),
}
}
fn write_request(&mut self) {
let stream = std::net::TcpStream::connect(“127.0.0.1:8080”).unwrap();
stream.set_nonblocking(true).unwrap();
let mut stream = mio::net::TcpStream::from_std(stream);
stream.write_all(get_req(&self.path).as_bytes()).unwrap();
self.stream = Some(stream);
}
}
The impl block defines two functions. The first is new, which simply sets the initial state.
The next function is write_requst, which sends the GET request to the server. You’ve seen this code before in the example in Chapter 4, so this should look familiar.