응용: 간단한 HTTP 서버

async/.await 를 사용하여 에코 서버를 만들어 보겠습니다.

본 예제를 시작하려면 rustup update stable을 실행하여 Rust stable 1.39 이상을 유지하십시오. 일단 완료하면 cargo new async-await-echo를 실행하여 새로운 프로젝트를 만들고 여십시오. 실행의 결과는 async-await-echo 폴더에 만들어 집니다.

Cargo.toml 파일에 몇몇 의존성을 추가해 봅시다 :

[dependencies]
# Hyper is an asynchronous HTTP library. We'll use it to power our HTTP
# server and to make HTTP requests.
hyper = "0.13.0"
# To setup some sort of runtime needed by Hyper, we will use the Tokio runtime.
tokio = { version = "0.2", features = ["full"] }

# (only for testing)
failure = "0.1.6"
reqwest = "0.9.24"

이제 의존성이 해결 되었으므로 이제 코딩을 시작하겠습니다. 다음의 imports를 추가합니다 :

use {
    hyper::{
        // Following functions are used by Hyper to handle a `Request`
        // and returning a `Response` in an asynchronous manner by using a Future
        service::{make_service_fn, service_fn},
        // Miscellaneous types from Hyper for working with HTTP.
        Body,
        Client,
        Request,
        Response,
        Server,
        Uri,
    },
    std::net::SocketAddr,
};

이제 의존성이 해결 되었으므로 모든 것을 모아서 리퀘스트를 처리하도록 하는 기본 토대를 만드는 작업을 시작하겠습니다 :

async fn serve_req(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
    // Always return successfully with a response containing a body with
    // a friendly greeting ;)
    Ok(Response::new(Body::from("hello, world!")))
}

async fn run_server(addr: SocketAddr) {
    println!("Listening on http://{}", addr);

    // Create a server bound on the provided address
    let serve_future = Server::bind(&addr)
        // Serve requests using our `async serve_req` function.
        // `serve` takes a closure which returns a type implementing the
        // `Service` trait. `service_fn` returns a value implementing the
        // `Service` trait, and accepts a closure which goes from request
        // to a future of the response.
        .serve(make_service_fn(|_| {
            async {
                {
                    Ok::<_, hyper::Error>(service_fn(serve_req))
                }
            }
        }));

    // Wait for the server to complete serving or exit with an error.
    // If an error occurred, print it to stderr.
    if let Err(e) = serve_future.await {
        eprintln!("server error: {}", e);
    }
}

#[tokio::main]
async fn main() {
  // Set the address to run our socket on.
  let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

  // Call our `run_server` function, which returns a future.
  // As with every `async fn`, for `run_server` to do anything,
  // the returned future needs to be run using `await`;
  run_server(addr).await;
}

지금 cargo run하면, "Listening on http://127.0.0.1:3000" 이라는 메시지가 터미널에 나타납니다. 여러분이 선택한 브라우저에서 위 주소를 열면 "hello, world!"가 브라우저에 나타납니다. 축하합니다! 방금 Rust에서 첫 번째 비동기 웹 서버를 작성했습니다.

다음과 같은 정보(request URI, HTTP version, headers, and other metadata)가 포함 된 요청을 검사 할 수도 있습니다. 예를 들어, 다음과 같이 요청의 URI를 인쇄 할 수 있습니다.

println!("Got request at {:?}", req.uri());

우리가 요청을 처리 할 때 비동기적인 것을 아무 것도 아직 하고 있지 않은 것을 눈치 채셨나요? -우리는 단지 즉시 응답합니다. 따라서 우리는 async fn이 제공하는 유연성을 활용하지 않습니다. 정적 메시지를 반환하는 대신 Hyper의 HTTP 클라이언트를 사용하여 다른 웹 사이트에 사용자의 요청을 프록시 하도록 시도해 봅시다.

요청하려는 URL을 파싱하여 시작합니다.

        let url_str = "http://www.rust-lang.org/en-US/";
        let url = url_str.parse::<Uri>().expect("failed to parse URL");

그런 다음 새로운 hyper::Client를 작성하고 이를 사용하여 GET 요청을 할 수 있습니다. 이는 응답을 사용자에게 반환합니다 :

        let res = Client::new().get(url).await?;
        // Return the result of the request directly to the user
        println!("request finished-- returning response");
        Ok(res)

Client::getFuture<Output = Result<Response<Body>>>을 구현하는 hyper::client::ResponseFuture를 반환합니다. (또는 futures 0.1 버젼에서 Future<Item = Response<Body>, Error = Error>). 우리가 그 future를 .await 하면 현재 작업 중인 HTTP 요청이 전송됩니다. 현재의 타스크는 일시 중지되고 큐에 들아가서 대기하다가 응답이 오면 계속 실행 됩니다.

이제 cargo run을 하고 브라우저에서 http://127.0.0.1:3000/foo를 연다면, Rust 홈페이지와 다음 문자열이 터미널에 나타납니다.

Listening on http://127.0.0.1:3000
Got request at /foo
making request to http://www.rust-lang.org/en-US/
request finished-- returning response

축하합니닥! 여러분은 HTTP 요청을 프록시 했습니다.