2016년 12월 19일 월요일

Reqwest 소스 분석

https://github.com/seanmonstar/reqwest


RequestBuilder는 HTTP 요청에 가장 기본이 되는 structure로서 Request에 관련된 값들을 가지고 있는다.
// Arc: An atomically reference counted wrapper for shared state.
pub struct RequestBuilder {
  // client를 항상 새로 만들지 않고 재활용을 하기 위해 Arc를 사용한다.
  client: Arc<ClientRef>,
  method: Method,
  url: Result<Url, ::UrlError>,
  _version: HttpVersion,
  headers: Headers,
  body: Option<::Result<Body>>,
}

// Result와 Option은 rust에서 기본으로 제공한다.
enum Result<T, E> {
   Ok(T),
   Err(E),
}

pub enum Option<T> {
    None,
    Some(T),
}
Client는 실제 네트워크상으로 HTTP 요청을 하는 것에 관련되어 hyper에 관련된 정보(client, redirect_policy)를 내부에 가지고 있는다. Method는 어떤 메소드(Get, Post같은)를 사용할 것인지를 의미하고 HttpVersion은 HTTP/1.1, HTTP/2.0등 어떤 버전을 사용할 것인지를 의미한다. 현재 Reqwest는 HTTP/1.1만을 사용한다. RequestBuilder가 생성되고 나면 header와 body를 관련 함수를 사용하여 설정할 수 있게 한다. RequestBuilder를 자세히 살펴보기 전에 Client를 먼저 살펴보자.
pub struct Client {
  // 재활용이 가능하도록 하기 위해 Arc로 보호
  inner: Arc<ClientRef> // ::hyper::Client,
}

// Mutex: A mutual exclusion primitive useful for protecting shared data
struct ClientRef {
  hyper: ::hyper::client,
  // 값의 변경이 가능하도록 하기 위해 Mutex를 사용한다.
  redirect_policy: Mutex<RedirectPolicy>,
}
Client의 생성은 Client::new()를 호출함으로써 이루어진다.
impl Client {
  pub fn new() -> ::Result<Client> {
    let mut client = try!(new_hyper_client());
    client.set_redirect_policy(::hyper::client::RedirectPolicy::FollowNone);
    Ok(Client {
      inner: Arc::new(ClientRef {
        hyper:client,
        redirect_policy: Mutex::new(RedirectPolicy::default()),
      }),
    })
  }

  pub fn redirect(&mut self, policy: RedirectPolicy) {
    *self.inner.redirect_policy.lock().unwrap() = policy;
  }
}
try!는 Result가 error이면 바로 error를 리턴하는 매크로이다. new_hyper_client()를 사용하여 hyper의 Client를 생성한 후 redirect policy를 FollowNone으로 설정한다. 그리고, Client를 만들어서 리턴한다. 기본으로 제공하는 Result를 간단하게 사용하기 위해 아래의 type alias를 지정하여 사용한다. Error도 여기서 정의한 enum으로서 request시 에러가 발생했을 때 사용된다.
pub type Result<T> = ::std::result::Result<T, Error>;
new_hyper_client()를 살펴보자.
fn new_hyper_client() -> ::Result<::hyper::Client> {
  use tls:TlsClient;
  Ok(::hyper::Client::with_connector(
    ::hyper::client::Pool::with_connector(
      Default::default(),
      ::hyper::net::HttpsConnector::new(try!(TlsClient::new()))
    )
  ))
}
hyper의 Client를 만들어주기 위해 관련 함수들을 사용한다. 현재는 HTTPS만을 지원하는 것으로 한다. HTTPS 지원을 위하여 tls.rs에 있는 관련 함수들을 사용한다.
간단하게 get을 요청하는 경우에 사용하기 위한 간단한 함수를 만들어 보자.
impl Client {
  pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
    self.request(Method::Get, url)
  }

  pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
    let url = url.into_url();
    RequestBuilder {
      client: self.inner.clone(), // reference count가 증가한다.
      method: method,
      url: url,
      _version: HttpVersion::Http11,
      headers: Headers::new(),
      body: None
    }
  }
}
IntoUrl은 hyper에서 url에 사용하는 Url로의 변환을 해주는 trait이다. hyper에는 Url, str, String에 대해 정의되어 있다.
trait IntoUrl {
  fn into_url(self) -> Result<Url, UrlError>;
}
위와 같이 get을 만들면 마찬가지로 post와 head도 만들 수 있다.
impl Client {
  pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
    self.request(Method::Post, url)
  }

  pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
    self.request(Method::Head, url)
  }
}
RequestBuilder를 만들고 나면 send를 통해 서버에 요청을 할 수 있다.
impl RequestBuilder {
  pub fn send(mut self) -> ::Result<Response> {
    // user agent를 지정해주지 않았으면 기본값으로 지정한다.
    // Headers는 지정한 타입을 받을 수 있게 정의되어 있고 이 타입이 정의한 함수들로부터 필요한 값을 가져온다.
    if !self.headers.has::<UserAgent>() {
      self.headers.set(UserAgent(DEFAULT_USER_AGENT.to_owned()));
    }

    if !self.headers.has::<Accept>() {
      // 'Accept: */*'
      self.headers.set(Accept::star());
    }

    let client = self.client;
    let mut method = self.method;
    // Url로 변경
    let mut url = try!(self.url);
    let mut headers = self.headers;
    // Option<Result<Body>>를 Option<Body>로 변경
    let mut body = match self.body {
      Some(b) => Some(try!(b)),
      None => None,
    };

    // redirect되는 url들을 저장한다.
    let mut urls = Vec::new();

    loop {
      // 서버로부터 응답을 가져온다.
      let res = {
        let mut req = client.hyper.request(method.clone(), url.clone())
            .headers(headers.clone());

        if let Some(ref mut b) = body {
          // hyper에서 사용하는 Body로 변환
          let body = body::as_hyper_body(b);
          req = req.body(body);
        }

        try!(req.send())
      };

      // status를 확인하여 redirect가 필요한지를 본다.
      let should_redirect = match res.status {
        StatusCode::MovedPermanently |
        StatusCode::Found |
        StatusCode::SeeOther => {
          body = None;
          match Method {
            Method::Get | Method::Head => {},
            _ => {
              method = Method::Get;
            }
          }
          true
        },
        StatusCode::TemporaryRedirect |
        StatusCode::PermanentRedirect => {
          if let Some(mut body) = body {
            // redirect시 body를 다시 사용할 수 있는지 확인한다.
            body::can_reset(body)
          } else {
            true
          }
        },
        _ => false,
      };

      if should_redirect {
        // response로부터 Location 헤더를 가져온다.
        // Result가 된다.
        let loc = {
          // Option<Result>가 된다.
          let loc = res.headers.get::<Location>().map(|loc| url.join(loc));
          if let Some(loc) = loc {
            loc
          } else {
            // Location이 오지 않은 경우
            return Ok(Response {
              inner: res
            });
          }
        };

        url = match loc {
          Ok(loc) => {
            headers.set(Referer(url.to_string()));
            urls.push(url);
            // check_redirect 함수가 끝날때까지 redirect_policy의 lock을 잡고 있는다.
            if check_redirect(&client.redirect_policy.lock().unwrap(), &loc, &urls)? {
              loc
            } else {
              // redirect를 허락하지 않는다.
              return Ok(Response {
                inner: res
              });
            }
          },
          Err(e) => {
            // url.join이 실패한 경우
            return Ok(Response {
              inner: res
            });
          },
        };
        // 새 url로 다시 서버에 요청하도록 한다.
      } else {
        // redirect하지 않는 경우
        return Ok(Response {
          inner: res
        });
      }
    }
  }
}
기본 user agent를 사용할 때 to_owned를 통해 복사된 값을 사용한다(Cloning). DEFAULT_USER_AGENT는 아래와 같이 정의되어 있다.
static DEFAULT_USER_AGENT: &'static str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
send 함수가 리턴하는 Response는 다음과 같다.
pub struct Response {
  inner: ::hyper::client::Response,
}
Response로부터 사용할 수 있는 몇가지 함수들을 정의하여 쉽게 사용할 수 있도록 한다.
impl Response {
  // status code를 리턴한다.
  pub fn status(&self) -> &StatusCode {
    &self.inner.status
  }
  // headers를 리턴한다.
  pub fn headers(&self) -> &Headers {
    &self.inner.headers
  }
  // http version을 리턴한다.
  pub fn version(&self) -> &HttpVersion {
    &self.inner.version
  }
}
RequestBuilder에 header를 추가할 수 있게 하자.
imple RequestBuilder<'a> {
  // 헤더 하나를 추가한다.
  pub fn header<H: ::header::Header + ::header::HeaderFormat>(mut self, header: H) -> RequestBuilder<'a> {
    self.headers.set(header);
    self
  }

  // 헤더의 집합을 추가한다.
  pub fn header<H: >(mut self, headers: ::header::Headers) -> RequestBuilder<'a> {
    self.headers.extend(headers.iter());
    self
  }
}
RequestBuilder에 body를 추가할 수 있게 하자.
// 넣으려는 body는 Into<Body>를 구현하고 있어야 한다.
impl RequestBuilder {
  pub fn body<T: Into<Body>>(mut self, body: T) -> RequestBuilder {
    // Body로 변환한 후 Option<Result>로 저장한다.
    self.body = Some(Ok(body.into()));
    self
  }
}
현재까지 사용된 Body에 관련된 부분은 Into<Body>와 body::as_hyper_body이다. 이와 관련하여 Body의 구현을 살펴보자. Body의 구조는 아래와 같다.
pub struct Body {
  reader: Kind,
}

// Box: A pointer type for heap allocation.
// Read: The Read trait allows for reading bytes from a source.
// Send: Types that can be transferred across thread boundaries.
enum Kind {
  // Read가 구현된 것에 대한 처리. File을 읽는다던지 하는 것.
  Reader(Box<Read + Send>, Option<u64>),
  // byte에 대한 처리.
  Bytes(Vec<u8>),
}
Body를 위한 From구현은 Vec<u8>, String, [u8], &str, File의 5개의 타입에 대해 구현한다. From과 Into는 둘 중 어느 한쪽이 구현되면 나머지 한쪽도 자동으로 구현된다.
impl From<Vec<u8>> for Body {
  fn from(v: Vec<u8>) -> Body {
    Body {
      reader: Kind::Bytes(v),
    }
  }
}

impl From<String> for Body {
  fn from(s: String) -> Body {
    s.into_bytes().into() // Vec<u8>로 변환 후 이의 into를 사용한다.
  }
}

impl<'a> From<&'a [u8]> for Body {
  fn from(s: &'a [u8]) -> Body {
    s.to_vec().into() // Vec<u8>로 변환 후 이의 into를 사용한다.
  }
}

impl<'a> From<&'a str> for Body {
  fn from(s: &'a str) -> Body {
    s.as_bytes().into() // Vec<u8>로 변환 후 이의 into를 사용한다.
  }
}

impl From<File> for Body {
  fn from(f: File) -> Body {
    // 파일의 크기를 가져온다.
    let len = f.metadata().map(|m| m.len()).ok();
    Body {
      reader: Kind::Reader(Box::new(f), len),
    }
  }
}
reader로부터 Body를 생성해주는 헬퍼함수로 Body::new()를 정의한다.
impl Body {
  pub fn new<R: Read + Send + 'static>(reader: R) -> Body {
    Body {
      reader: Kind::Reader(Box::new(reader), None),
    }
  }
}
이제 as_hyper_body를 정의하자.
// Body를 hyper에서 사용하는 Body로 변환한 후 이것을 리턴한다.
pub fn as_hyper_body<'a>(body: &'a mut Body) -> ::hyper::client::Body<'a> {
  match body.reader {
    Kind::Bytes(ref bytes) => {
      let len = bytes.len();
      ::hyper::client::Body::BufBody(bytes, len)
    },
    Kind::Reader(ref mut reader, len_opt) => {
      match len_opt {
        Some(len) => ::hyper::client::Body::SizedBody(reader, len),
        None => ::hyper::client::Body::ChunkedBody(reader),
      }
    },
  }
}
재활용이 가능한 Body인지 아닌지를 판별해주는 함수를 정의한다.
// File같은 경우 한번 읽으면 읽는 위치가 계속 증가하기 때문에 그대로 재활용할 수 없다.
pub fn can_reset(body: &Body) -> bool {
  match body.reader {
    Kind::Bytes(_) => true,
    Kind::Reader(..) => false,
  }
}
ClientRef에 redirect policy를 위한 redirect_policy가 있어서 설정을 변경할 수 있다. 이에 관련된 내용을 살펴보자.
pub struct RedirectPolicy {
  inner: Policy,
}

// Sync: Types for which it is safe to share references between threads.
enum Policy {
  // 사용자 지정 함수에 의해 redirect를 할지 말지를 정한다.
  Custom(Box<Fn(&Url, &[Url]) -> ::Result<bool> + Send + Sync + 'static>),
  Limit(usize), // redirect의 수에 제한을 둔다.
  None, // redirect를 하지 않는다.
}
위 enum의 값들을 생성하는 함수를 제공한다.
impl RedirectPolicy {
  pub fn limited(max: usize) -> RedirectPolicy {
    RedirectPolicy {
      inner: Policy::Limit(max),
    }
  }

  pub fn none() -> RedirectPolicy {
    RedirectPolicy {
      inner: Policy::None,
    }
  }

  pub fn custom<T>(policy: T) -> RedirectPolicy
  where T: Fn(&Url, &[Url]) -> ::Result<bool> + Send + Sync + 'static {
    RedirectPolicy {
      inner: Policy::Custom(Box::new(policy)),
    }
  }
}
Client 생성시 사용하는 default 함수는 다음과 같다.
// Default: A trait for giving a type a useful default value.
// default는 redirect를 10번까지 허용하는 것으로 지정한다.
impl Default for RedirectPolicy {
  fn default() -> RedirectPolicy {
    RedirectPolicy::limited(10)
  }
}
RequestBuilder의 send 함수에서는 redirect를 확인하기 위해 check_redirect 함수를 호출한다.
// Client에서 설정한 RedirectPolicy를 받아서 이의 redirect를 호출한다.
pub fn check_redirect(policy: &RedirectPolicy, next: &Url, previous: &[Url]) -> ::Result<bool> {
  policy.redirect(next, previous)
}
RedirectPolicy에 redirect를 구현한다.
impl RedirectPolicy {
  fn redirect(&self, next: &Url, previous: &[Url]) -> ::Result<bool> {
    match self.inner {
      Policy::Custom(ref custom) => custom(next, previous),
      Policy::Limit(max) => {
        if previous.len() == max { // 최대한 허용하는 redirect를 넘은 경우.
          Err(::Error::TooManyRedirects)
        } else if previous.contains(next) { // 이전에 이미 redirect한 url인 경우.
          Err(::Error::RedirectLoop)
        } else { // redirect를 허용하는 경우.
          Ok(true)
      },
      Policy::None => Ok(false),
    }
  }
}
앞에서 ::hyper::Client를 만들때 HttpsConnector를 만들기 위해 파라메터로 TlsClient::new()를 사용했다.
// 멤버 하나를 가지고 있는 tuple
pub struct TlsClient(TlsConnector);

impl TlsClient {
  pub fn new() -> ::Result<TlsClient> {
    // builder의 결과가 Ok이면 and_then의 내용을 실행하고 map을 통해 T를 U로 변경하고 map_err를 통해 E를 F로 변경한다.
    // and_then: Result가 Ok이면 실행.
    // map: Result가 Ok이면 Result<T, E>를 Result<U, E>로 변경.
    // map_err: Result가 Err이면 Result<T, E>를 Result<T, F>로 변경.
    TlsConnector::builder()
        .and_then(|c| c.build()) // c.build()의 리턴값은 Result<TlsConnector>이다.
        .map(TlsClient) // Ok(TlsClient(TlsConnector))가 된다.
        .map_err(|e| ::Error::Http(::hyper:Error:Ssl(Box::new(e))))
  }
}
TlsClient가 HttpsConnector에 사용되기 위해서는 SslClient를 구현해야 한다.
impl SslClient for TlsClient {
  type SslStream = TlsStream;

  // wrap a client stream with SSL.
  fn wrap_client(&self, stream: HttpStream, host: &str) -> ::hyper::Result<Self::Stream> {
    self.0.connect(host, stream).map(TlsStream).map_err(|e| {
      match e {
        HandshakeError::Failure(e) => ::hyper::Error::Ssl(Box::new(e)),
        HandshakeError::Interrupted(..) => {
          unreachable!("TlsClient::handshake interrupted")
        }
      }
    })
  }
}
TlsStream이 SslStream으로 사용되기 위해서 Read, Write, Clone, NetworkStream을 구현해야 한다. native_tls의 TlsStream이 이를 다 구현하고 있기 때문에 TlsStream은 이를 wrapping해서 필요한 구현을 delegate로 구현한다.
// 이름 중복을 피하기 위해 native_tls의 TlsStream을 NativeTlsStream로 이름을 바꿔서 사용한다.
pub struct TlsStream(NativeTlsStream<HttpStream>);

impl Read for TlsStream {
  fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
    self.0.read(buf)
  }
}

impl Write for TlsStream {
  fn write(&mut self, data: &[u8]) -> io::Result<usize> {
    self.0.write(data)
  }

  fn flush(&mut self) -> io::Result<()> {
    self.0.flush()
  }
}

impl Clone for TlsStream {
  fn clone(&self) -> TlsStream {
    unreachable!("TlsStream::clone is never used for the Client")
  }
}

impl NetworkStream for TlsStream {
  fn peer_addr(&mut self) -> io::Result<SocketAddr> {
    self.0.get_mut().peer_addr()
  }

  fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
    self.0.get_ref().set_read_timeout(dur)
  }

  fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
    self.0.get_ref().set_write_timeout(dur)
  }
}

댓글 없음:

댓글 쓰기

Building asynchronous views in SwiftUI 정리

Handling loading states within SwiftUI views self loading views View model 사용하기 Combine을 사용한 AnyPublisher Making SwiftUI views refreshable r...