Skip to main content

volo_grpc/transport/
client.rs

1use std::{io, marker::PhantomData};
2
3use bytes::Bytes;
4use http::{
5    HeaderValue,
6    header::{CONTENT_TYPE, TE},
7};
8use http_body::Frame;
9use http_body_util::StreamBody;
10use hyper_util::rt::{TokioExecutor, TokioTimer};
11use motore::Service;
12use tower::{Service as TowerService, util::ServiceExt};
13use volo::net::Address;
14
15use super::connect::Connector;
16use crate::{
17    Code, Request, Response, Status,
18    body::boxed,
19    client::Http2Config,
20    codec::{
21        compression::{ACCEPT_ENCODING_HEADER, ENCODING_HEADER},
22        decode::Kind,
23    },
24    context::{ClientContext, Config},
25};
26
27/// A simple wrapper of [`hyper_util::client::legacy::Client`] that implements [`Service`]
28/// to make outgoing requests.
29#[allow(clippy::type_complexity)]
30pub struct ClientTransport<U> {
31    http_client: hyper_util::client::legacy::Client<
32        Connector,
33        StreamBody<crate::BoxStream<'static, Result<Frame<Bytes>, crate::Status>>>,
34    >,
35    _marker: PhantomData<fn(U)>,
36}
37
38impl<U> Clone for ClientTransport<U> {
39    fn clone(&self) -> Self {
40        Self {
41            http_client: self.http_client.clone(),
42            _marker: self._marker,
43        }
44    }
45}
46
47impl<U> ClientTransport<U> {
48    /// Creates a new [`ClientTransport`] by setting the underlying connection
49    /// with the given config.
50    pub fn new(http2_config: &Http2Config, rpc_config: &Config) -> Self {
51        let config = volo::net::dial::Config::new(
52            rpc_config.connect_timeout,
53            rpc_config.read_timeout,
54            rpc_config.write_timeout,
55        );
56        let http_client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
57            .timer(TokioTimer::new())
58            .http2_only(true)
59            .http2_initial_stream_window_size(http2_config.init_stream_window_size)
60            .http2_initial_connection_window_size(http2_config.init_connection_window_size)
61            .http2_max_frame_size(http2_config.max_frame_size)
62            .http2_adaptive_window(http2_config.adaptive_window)
63            .http2_keep_alive_interval(http2_config.http2_keepalive_interval)
64            .http2_keep_alive_timeout(http2_config.http2_keepalive_timeout)
65            .http2_keep_alive_while_idle(http2_config.http2_keepalive_while_idle)
66            .http2_max_concurrent_reset_streams(http2_config.max_concurrent_reset_streams)
67            .http2_max_send_buf_size(http2_config.max_send_buf_size)
68            .build(Connector::new(Some(config)));
69
70        ClientTransport {
71            http_client,
72            _marker: PhantomData,
73        }
74    }
75
76    #[cfg(feature = "__tls")]
77    #[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "native-tls"))))]
78    pub fn new_with_tls(
79        http2_config: &Http2Config,
80        rpc_config: &Config,
81        tls_config: volo::net::tls::ClientTlsConfig,
82    ) -> Self {
83        let config = volo::net::dial::Config::new(
84            rpc_config.connect_timeout,
85            rpc_config.read_timeout,
86            rpc_config.write_timeout,
87        );
88        let http_client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
89            .timer(TokioTimer::new())
90            .http2_only(true)
91            .http2_initial_stream_window_size(http2_config.init_stream_window_size)
92            .http2_initial_connection_window_size(http2_config.init_connection_window_size)
93            .http2_max_frame_size(http2_config.max_frame_size)
94            .http2_adaptive_window(http2_config.adaptive_window)
95            .http2_keep_alive_interval(http2_config.http2_keepalive_interval)
96            .http2_keep_alive_timeout(http2_config.http2_keepalive_timeout)
97            .http2_keep_alive_while_idle(http2_config.http2_keepalive_while_idle)
98            .http2_max_concurrent_reset_streams(http2_config.max_concurrent_reset_streams)
99            .http2_max_send_buf_size(http2_config.max_send_buf_size)
100            .build(Connector::new_with_tls(Some(config), tls_config));
101
102        ClientTransport {
103            http_client,
104            _marker: PhantomData,
105        }
106    }
107}
108
109impl<T, U> Service<ClientContext, Request<T>> for ClientTransport<U>
110where
111    T: crate::message::SendEntryMessage + Send + 'static,
112    U: crate::message::RecvEntryMessage + 'static,
113{
114    type Response = Response<U>;
115
116    type Error = Status;
117
118    #[cfg_attr(not(feature = "compress"), allow(unused_variables))]
119    async fn call(
120        &self,
121        cx: &mut ClientContext,
122        volo_req: Request<T>,
123    ) -> Result<Self::Response, Self::Error> {
124        let mut http_client = self.http_client.clone();
125        // SAFETY: parameters controlled by volo-grpc are guaranteed to be valid.
126        // get the call address from the context
127        let target = cx.rpc_info.callee().address().ok_or_else(|| {
128            io::Error::new(std::io::ErrorKind::InvalidData, "address is required")
129        })?;
130
131        let (metadata, extensions, message) = volo_req.into_parts();
132        let path = cx.rpc_info.method();
133        let rpc_config = cx.rpc_info.config();
134        let accept_compressions = &rpc_config.accept_compressions;
135
136        // select the compression algorithm with the highest priority by user's config
137        let send_compression = rpc_config
138            .send_compressions
139            .as_ref()
140            .map(|config| config[0]);
141
142        let body = http_body_util::StreamBody::new(message.into_body(send_compression));
143
144        let mut req = http::Request::builder()
145            .version(http::Version::HTTP_2)
146            .method(http::Method::POST)
147            .uri(build_uri(target.clone(), path))
148            .extension(extensions)
149            .body(body)
150            .map_err(|err| Status::from_error(err.into()))?;
151        *req.headers_mut() = metadata.into_headers();
152        req.headers_mut()
153            .insert(TE, HeaderValue::from_static("trailers"));
154        req.headers_mut()
155            .insert(CONTENT_TYPE, HeaderValue::from_static("application/grpc"));
156
157        // insert compression headers
158        if let Some(send_compression) = send_compression {
159            req.headers_mut()
160                .insert(ENCODING_HEADER, send_compression.into_header_value());
161        }
162        if let Some(accept_compressions) = accept_compressions {
163            if !accept_compressions.is_empty() {
164                if let Some(header_value) =
165                    accept_compressions[0].into_accept_encoding_header_value(accept_compressions)
166                {
167                    req.headers_mut()
168                        .insert(ACCEPT_ENCODING_HEADER, header_value);
169                }
170            }
171        }
172        cx.stats.record_make_transport_start_at();
173
174        let resp = http_client
175            .ready()
176            .await
177            .map_err(|err| Status::from_error(err.into()))?
178            .call(req)
179            .await
180            .map_err(|err| Status::from_error(err.into()))?;
181
182        cx.stats.record_make_transport_end_at();
183
184        let status_code = resp.status();
185        let headers = resp.headers();
186
187        if let Some(status) = Status::from_header_map(headers) {
188            if status.code() != Code::Ok {
189                return Err(status);
190            }
191        }
192        let path = cx.rpc_info.method();
193        let rpc_config = cx.rpc_info.config();
194
195        #[cfg(not(feature = "compress"))]
196        let accept_compression = None;
197        #[cfg(feature = "compress")]
198        let accept_compression =
199            crate::codec::compression::CompressionEncoding::from_encoding_header(
200                headers,
201                &rpc_config.accept_compressions,
202            )?;
203
204        let (parts, body) = resp.into_parts();
205
206        let body = U::from_body(
207            Some(path),
208            boxed(body),
209            Kind::Response(status_code),
210            accept_compression,
211        )?;
212        let resp = hyper::Response::from_parts(parts, body);
213        Ok(Response::from_http(resp))
214    }
215}
216
217fn build_uri(addr: Address, path: &str) -> hyper::Uri {
218    match addr {
219        Address::Ip(ip) => hyper::Uri::builder()
220            .scheme(http::uri::Scheme::HTTP)
221            .authority(ip.to_string())
222            .path_and_query(path)
223            .build()
224            .expect("fail to build ip uri"),
225        #[cfg(target_family = "unix")]
226        Address::Unix(unix) => hyper::Uri::builder()
227            .scheme("http+unix")
228            .authority(hex::encode(
229                unix.as_pathname()
230                    .expect("target address is an invalid unix socket")
231                    .to_string_lossy()
232                    .as_bytes(),
233            ))
234            .path_and_query(path)
235            .build()
236            .expect("fail to build unix uri"),
237        #[allow(unreachable_patterns)]
238        _ => unimplemented!("unsupported type of address"),
239    }
240}
241
242#[cfg(test)]
243mod tests {
244
245    #[test]
246    fn test_build_uri_ip() {
247        let addr = "127.0.0.1:8000".parse::<std::net::SocketAddr>().unwrap();
248        let path = "/path?query=1";
249        let uri = "http://127.0.0.1:8000/path?query=1"
250            .parse::<hyper::Uri>()
251            .unwrap();
252        assert_eq!(super::build_uri(volo::net::Address::from(addr), path), uri);
253    }
254
255    #[cfg(target_family = "unix")]
256    #[test]
257    fn test_build_uri_unix() {
258        let addr = "/tmp/rpc.sock".parse::<std::path::PathBuf>().unwrap();
259        let path = "/path?query=1";
260        let uri = "http+unix://2f746d702f7270632e736f636b/path?query=1"
261            .parse::<hyper::Uri>()
262            .unwrap();
263        assert_eq!(
264            super::build_uri(
265                volo::net::Address::from(
266                    std::os::unix::net::SocketAddr::from_pathname(addr).unwrap()
267                ),
268                path
269            ),
270            uri
271        );
272    }
273
274    fn is_unpin<T: Unpin>() {}
275
276    #[test]
277    fn test_is_unpin() {
278        is_unpin::<super::ClientTransport<()>>();
279    }
280}