1. Wprowadzenie
W tym ćwiczeniu dowiesz się, jak za pomocą gRPC-Rust utworzyć klienta i serwer, które będą stanowić podstawę aplikacji do mapowania tras napisanej w Rust.
Po ukończeniu tego samouczka będziesz mieć klienta, który łączy się z serwerem zdalnym za pomocą gRPC, aby uzyskiwać informacje o elementach na trasie klienta, tworzyć podsumowanie trasy klienta i wymieniać informacje o trasie, takie jak aktualizacje ruchu, z serwerem i innymi klientami.
Usługa jest zdefiniowana w pliku Protocol Buffers, który będzie używany do generowania powtarzalnego kodu dla klienta i serwera, aby mogły się ze sobą komunikować. Dzięki temu zaoszczędzisz czas i wysiłek potrzebny na wdrożenie tej funkcji.
Wygenerowany kod obsługuje nie tylko złożoność komunikacji między serwerem a klientem, ale także serializację i deserializację danych.
Czego się nauczysz
- Jak używać buforów protokołu do definiowania interfejsu API usługi.
- Jak utworzyć klienta i serwer oparte na gRPC na podstawie definicji Protocol Buffers za pomocą automatycznego generowania kodu.
- znajomość komunikacji strumieniowej klient-serwer za pomocą gRPC;
Te warsztaty są przeznaczone dla programistów Rust, którzy dopiero zaczynają korzystać z gRPC lub chcą sobie przypomnieć jego działanie, a także dla wszystkich innych osób zainteresowanych tworzeniem systemów rozproszonych. Nie musisz mieć wcześniejszego doświadczenia z gRPC.
2. Zanim zaczniesz
Wymagania wstępne
Sprawdź, czy masz zainstalowane te elementy:
- GCC. Postępuj zgodnie z instrukcjami tutaj
- Rust, najnowsza wersja. Postępuj zgodnie z instrukcjami instalacji, które znajdziesz tutaj.
Pobierz kod
Aby nie musieć zaczynać od zera, w tym ćwiczeniu znajdziesz szkielet kodu źródłowego aplikacji, który możesz uzupełnić. Z podanych niżej instrukcji dowiesz się, jak ukończyć aplikację, w tym jak użyć wtyczek kompilatora buforów protokołu do wygenerowania kodu gRPC.
Najpierw utwórz katalog roboczy ćwiczenia i przejdź do niego:cd
mkdir streaming-grpc-rust-getting-started && cd streaming-grpc-rust-getting-started
Pobierz i rozpakuj ćwiczenia:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \ | tar xvz --strip-components=4 \ grpc-codelabs-1/codelabs/grpc-rust-streaming/start_here
Możesz też pobrać plik ZIP zawierający tylko katalog z ćwiczeniem i rozpakować go ręcznie.
Jeśli nie chcesz wpisywać implementacji, gotowy kod źródłowy jest dostępny na GitHubie.
3. Określanie wiadomości i usług
Pierwszym krokiem jest zdefiniowanie usługi gRPC aplikacji, jej metod RPC oraz typów wiadomości żądań i odpowiedzi za pomocą buforów protokołu. Twoja usługa będzie zapewniać:
- Metody RPC wywoływane przez serwer i klienta:
ListFeatures,RecordRouteiRouteChat. - Typy wiadomości
Point,Feature,Rectangle,RouteNoteiRouteSummary, czyli struktury danych wymieniane między klientem a serwerem podczas wywoływania powyższych metod.
Te metody RPC i ich typy wiadomości będą zdefiniowane w pliku proto/routeguide.proto dostarczonego kodu źródłowego.
Bufory protokołu są powszechnie nazywane protobufami. Więcej informacji o terminologii gRPC znajdziesz w artykule Podstawowe koncepcje, architektura i cykl życia.
Definiowanie typów wiadomości
Najpierw zdefiniujmy wiadomości, które będą używane przez nasze wywołania RPC. W pliku routeguide/route_guide.proto kodu źródłowego najpierw zdefiniuj typ wiadomości Point. Symbol Point reprezentuje parę współrzędnych szerokości i długości geograficznej na mapie. W tym ćwiczeniu używaj liczb całkowitych jako współrzędnych:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Numery 1 i 2 to unikalne identyfikatory poszczególnych pól w strukturze message.
Następnie określ Featuretyp wiadomości. Feature używa pola string na nazwę lub adres pocztowy czegoś w lokalizacji określonej przez Point:
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
Następnie Rectangle, czyli wiadomość reprezentująca prostokąt o określonych współrzędnych geograficznych, przedstawiony jako 2 przeciwległe punkty „lo” i „hi”.
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
Jest to też wiadomość RouteNote, która reprezentuje wiadomość wysłaną w danym momencie.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
Wymagamy też wiadomości RouteSummary. Ta wiadomość jest otrzymywana w odpowiedzi na wywołanie RecordRoute RPC, które opisujemy w następnej sekcji. Zawiera liczbę otrzymanych punktów, liczbę wykrytych cech i całkowity przebyty dystans jako sumę odległości między poszczególnymi punktami.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
Określanie metod usługi
Najpierw zdefiniujmy usługę, a potem wiadomości. Aby zdefiniować usługę, w pliku .proto podaj jej nazwę. Plik proto/routeguide.proto ma strukturę service o nazwie RouteGuide, która definiuje co najmniej 1 metodę udostępnianą przez usługę aplikacji.
Zdefiniuj metody RPC w definicji usługi, określając typy żądań i odpowiedzi. W tej części ćwiczenia zdefiniujemy:
ListFeatures
Pobiera Feature dostępne w danym Rectangle. Wyniki są przesyłane strumieniowo, a nie zwracane od razu (np. w wiadomości z odpowiedzią zawierającej pole powtarzane), ponieważ prostokąt może obejmować duży obszar i zawierać ogromną liczbę obiektów.
Odpowiednim typem RPC w tym przypadku jest strumieniowe RPC po stronie serwera: klient wysyła żądanie do serwera i otrzymuje strumień, z którego może odczytywać sekwencję komunikatów. Klient odczytuje zwrócony strumień, dopóki nie będzie już żadnych wiadomości. Jak widać w naszym przykładzie, metodę przesyłania strumieniowego po stronie serwera określa się, umieszczając słowo kluczowe stream przed typem odpowiedzi.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
Akceptuje strumień Point na pokonywanej trasie i zwraca RouteSummary po zakończeniu przemierzania.
W tym przypadku odpowiednie wydaje się wywołanie RPC strumieniowania po stronie klienta: klient zapisuje sekwencję wiadomości i wysyła je na serwer, ponownie używając udostępnionego strumienia. Gdy klient skończy pisać wiadomości, czeka, aż serwer je wszystkie odczyta i zwróci odpowiedź. Metodę przesyłania strumieniowego po stronie klienta określa się, umieszczając słowo kluczowe stream przed typem żądania.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
Akceptuje strumień RouteNote wysyłanych podczas pokonywania trasy, a także odbiera inne RouteNote (np. od innych użytkowników).
To jest właśnie przypadek użycia strumieniowania dwukierunkowego. Dwukierunkowe wywołanie RPC przesyła sekwencję wiadomości w obu kierunkach za pomocą strumienia odczytu i zapisu. Oba strumienie działają niezależnie, więc klienci i serwery mogą odczytywać i zapisywać dane w dowolnej kolejności.
Na przykład serwer może poczekać na otrzymanie wszystkich wiadomości od klienta, zanim napisze odpowiedzi, lub może odczytać wiadomość, a następnie napisać odpowiedź albo zastosować inną kombinację odczytów i zapisów.
Kolejność wiadomości w każdym strumieniu jest zachowana. Ten typ metody określa się, umieszczając słowo kluczowe stream przed żądaniem i odpowiedzią.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. Generowanie kodu klienta i serwera
Wygenerowany kod z pliku .proto w wygenerowanym katalogu został już Ci przekazany.
Jeśli chcesz dowiedzieć się, jak samodzielnie wygenerować kod z pliku .proto lub wprowadzić w nim zmiany i je przetestować, zapoznaj się z tymi instrukcjami..proto
Wygenerowany kod zawiera:
- Definicje struktur dla typów wiadomości
Point,Feature,Rectangle,RouteNoteiRouteSummary. - Cechy usługi, które musimy wdrożyć:
route_guide_server::RouteGuide. - Typ klienta, którego użyjemy do wywołania serwera:
route_guide_client::RouteGuideClient<T>.
Następnie zaimplementujemy metody po stronie serwera, aby w odpowiedzi na żądanie klienta serwer mógł przesłać odpowiedź.
5. Wdrażanie usługi
Najpierw przyjrzyjmy się, jak utworzyć RouteGuideserwerRouteGuide. Aby usługa RouteGuide działała prawidłowo, musisz wykonać 2 czynności:
- Implementacja interfejsu usługi wygenerowanego na podstawie definicji usługi: wykonywanie rzeczywistej „pracy” usługi.
- Uruchomienie serwera gRPC, który będzie nasłuchiwać żądań od klientów i przekazywać je do odpowiedniej implementacji metody.
W src/server/server.rs możemy uwzględnić wygenerowany kod za pomocą makra include_generated_proto! gRPC i zaimportować cechę RouteGuide oraz Point.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
pub use grpc_pb::{
route_guide_server::{RouteGuideServer, RouteGuide},
Point, Feature, Rectangle, RouteNote, RouteSummary
};
Możemy zacząć od zdefiniowania struktury reprezentującej naszą usługę. Obecnie możemy to zrobić na src/server/server.rs:
#[derive(Debug)]
pub struct RouteGuideService {
features: Vec<Feature>,
}
Teraz musimy zaimplementować cechę route_guide_server::RouteGuide z wygenerowanego kodu.
Implementacja RouteGuide
Musimy wdrożyć wygenerowany interfejs RouteGuide. Implementacja może wyglądać tak: Jest to już w szablonie.
#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
async fn list_features(
&self,
request: Request<Rectangle>,
) -> Result<Response<ListFeaturesStream>, Status> {
...
}
async fn record_route(
&self,
request: Request<tonic::Streaming<Point>>,
) -> Result<Response<RouteSummary>, Status> {
...
}
async fn route_chat(
&self,
request: Request<tonic::Streaming<RouteNote>>,
) -> Result<Response<RouteChatStream>, Status> {
...
}
}
Przyjrzyjmy się szczegółowo każdej implementacji RPC.
RPC przesyłania strumieniowego po stronie serwera
Zacznijmy od ListFeatures. Jest to strumieniowe wywołanie RPC po stronie serwera, więc musimy odesłać do klienta wiele obiektów Feature.
async fn list_features(
&self,
request: Request<Rectangle>,
) -> Result<Response<ListFeaturesStream>, Status> {
println!("ListFeatures = {:?}", request);
let (tx, rx) = mpsc::channel(4);
let features = self.features.clone();
tokio::spawn(async move {
for feature in &features[..] {
if in_range(&feature.location().to_owned(), request.get_ref()) {
println!(" => send {feature:?}");
tx.send(Ok(feature.clone())).await.unwrap();
}
}
println!(" /// done sending");
});
let output_stream = ReceiverStream::new(rx);
Ok(Response::new(Box::pin(output_stream)))
}
Jak widać, otrzymujemy obiekt żądania (Rectangle, w którym klient chce znaleźć Features). Tym razem musimy zwrócić strumień wartości. Tworzymy kanał i uruchamiamy nowe zadanie asynchroniczne, w którym przeprowadzamy wyszukiwanie i wysyłamy do kanału funkcje spełniające nasze ograniczenia. Połowa strumienia kanału jest zwracana do wywołującego, opakowana w tonic::Response.
RPC przesyłania strumieniowego po stronie klienta
Przyjrzyjmy się teraz nieco bardziej skomplikowanej metodzie: przesyłaniu strumieniowemu po stronie klienta RecordRoute, w której otrzymujemy strumień Points od klienta i zwracamy pojedynczy RouteSummary z informacjami o jego podróży. Otrzymuje strumień jako dane wejściowe, którego serwer może używać do odczytywania i zapisywania wiadomości. Może iterować wiadomości klientów za pomocą metody next() i zwracać pojedynczą odpowiedź.
async fn record_route(
&self,
request: Request<tonic::Streaming<Point>>,
) -> Result<Response<RouteSummary>, Status> {
println!("RecordRoute");
let mut stream = request.into_inner();
let mut summary = RouteSummary::default();
let mut last_point = None;
let now = Instant::now();
while let Some(point) = stream.next().await {
let point = point?;
println!(" ==> Point = {point:?}");
// Increment the point count
summary.set_point_count(summary.point_count() + 1);
// Find features
for feature in &self.features[..] {
if feature.location().latitude() == point.latitude() {
if feature.location().longitude() == point.longitude(){
summary.set_feature_count(summary.feature_count() + 1);
}
}
}
// Calculate the distance
if let Some(ref last_point) = last_point {
let new_dist = summary.distance() + calc_distance(last_point, &point);
summary.set_distance(new_dist);
}
last_point = Some(point);
}
summary.set_elapsed_time(now.elapsed().as_secs() as i32);
Ok(Response::new(summary))
}
W treści metody używamy metody next() strumienia, aby wielokrotnie odczytywać żądania klienta do obiektu żądania (w tym przypadku Point), dopóki nie będzie już więcej wiadomości. Jeśli jest to None, strumień jest nadal prawidłowy i można kontynuować odczytywanie.
Dwukierunkowe wywołanie RPC strumieniowania
Na koniec przyjrzyjmy się dwukierunkowemu strumieniowemu wywołaniu RPC RouteChat().
async fn route_chat(
&self,
request: Request<tonic::Streaming<RouteNote>>,
) -> Result<Response<RouteChatStream>, Status> {
println!("RouteChat");
let mut notes: HashMap<(i32, i32), Vec<RouteNote>> = HashMap::new();
let mut stream = request.into_inner();
let output = async_stream::try_stream! {
while let Some(note) = stream.next().await {
let note = note?;
let location = note.location();
let key = (location.latitude(), location.longitude());
let location_notes = notes.entry(key).or_insert(vec![]);
location_notes.push(note);
for note in location_notes {
yield note.clone();
}
}
};
Ok(Response::new(Box::pin(output)))
}
Tym razem otrzymujemy strumień, który, podobnie jak w przykładzie przesyłania strumieniowego po stronie klienta, może służyć do odczytywania i zapisywania wiadomości. Tym razem jednak zwracamy wartości za pomocą strumienia metody, gdy klient nadal pisze wiadomości do swojego strumienia wiadomości. Składnia odczytu i zapisu jest tu bardzo podobna do naszej metody przesyłania strumieniowego przez klienta, z tym wyjątkiem, że serwer zwraca RouteChatStream. Chociaż każda ze stron zawsze będzie otrzymywać wiadomości drugiej strony w kolejności, w jakiej zostały napisane, zarówno klient, jak i serwer mogą odczytywać i zapisywać dane w dowolnej kolejności – strumienie działają całkowicie niezależnie.
Strumień output tworzymy za pomocą funkcji try_stream!, co oznacza, że strumień może zwracać błędy.
Uruchom serwer
Po wdrożeniu tej metody musimy też uruchomić serwer gRPC, aby klienci mogli korzystać z naszej usługi. Wypełnij main().
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:10000".parse().unwrap();
println!("RouteGuideServer listening on: {addr}");
let route_guide = RouteGuideService {
features: load(),
};
let svc = RouteGuideServer::new(route_guide);
Server::builder().add_service(svc).serve(addr).await?;
Ok(())
}
Oto, co dzieje się w main():
- Określ port, którego mamy używać do nasłuchiwania żądań klientów
- Tworzenie
RouteGuideServicez wczytanymi funkcjami - Utwórz instancję serwera gRPC za pomocą
RouteGuideServer::new(), korzystając z utworzonej usługi. - Zarejestruj implementację usługi na serwerze gRPC.
- Wywołaj funkcję
serve()na serwerze z naszymi szczegółami portu, aby wykonać blokujące oczekiwanie do momentu zakończenia procesu.
6. Tworzenie klienta
W tej sekcji przyjrzymy się tworzeniu klienta Rust dla naszej usługi RouteGuide w src/client/client.rs.
Najpierw umieść wygenerowany kod w zakresie.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::route_guide_client::RouteGuideClient;
use grpc_pb::{Point, Rectangle, RouteNote};
Metody usługi połączeń
Zobaczmy teraz, jak wywołujemy metody usługi. W gRPC-Rust wywołania RPC działają w trybie blokującym/synchronicznym, co oznacza, że wywołanie RPC czeka na odpowiedź serwera i zwraca odpowiedź lub błąd.
RPC przesyłania strumieniowego po stronie serwera
W tym miejscu wywołujemy metodę strumieniowania po stronie serwera ListFeatures, która zwraca strumień obiektów geograficznych Feature.
async fn print_features(client: &mut RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
let rectangle = proto!(Rectangle {
lo: proto!(Point {
latitude: 400_000_000,
longitude: -750_000_000,
}),
hi: proto!(Point {
latitude: 420_000_000,
longitude: -730_000_000,
}),
});
let mut stream = client
.list_features(Request::new(rectangle))
.await?
.into_inner();
while let Some(feature) = stream.message().await? {
println!("FEATURE: Name = \"{}\", Lat = {}, Lon = {}",
feature.name(),
feature.location().latitude(),
feature.location().longitude());
}
Ok(())
}
Przekazujemy do metody żądanie i otrzymujemy instancję elementu ListFeaturesStream. Klient może używać strumienia ListFeaturesStream do odczytywania odpowiedzi serwera. Używamy metody ListFeaturesStreammessage(), aby wielokrotnie odczytywać odpowiedzi serwera do obiektu bufora protokołu odpowiedzi (w tym przypadku Feature), dopóki nie będzie już więcej wiadomości.
RPC przesyłania strumieniowego po stronie klienta
W przypadku record_route przekształcamy wektor punktów w strumień. Następnie przekazujemy ten strumień do record_route() jako żądanie i po pełnym przetworzeniu strumienia przez serwer otrzymujemy pojedynczą odpowiedź RouteSummary.
async fn run_record_route(client: &mut RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
let mut rng = rand::rng();
let point_count: i32 = rng.random_range(2..100);
let mut points = vec![];
for _ in 0..=point_count {
points.push(random_point(&mut rng))
}
println!("Traversing {} points", points.len());
let request = Request::new(tokio_stream::iter(points));
match client.record_route(request).await {
Ok(response) => {
let response = response.into_inner();
println!("SUMMARY: Feature Count = {}, Distance = {}", response.feature_count(), response.distance())},
Err(e) => println!("something went wrong: {e:?}"),
}
Ok(())
}
Dwukierunkowe wywołanie RPC strumieniowania
Na koniec przyjrzyjmy się dwukierunkowemu strumieniowemu wywołaniu RPC RouteChat(). Przekazujemy do metody żądanie strumienia, do którego zapisujemy dane, i otrzymujemy strumień, z którego możemy odczytywać wiadomości. Tym razem zwracamy wartości za pomocą strumienia metody, gdy serwer nadal zapisuje wiadomości w swoim strumieniu wiadomości.
async fn run_route_chat(client: &mut RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
let start = time::Instant::now();
let outbound = async_stream::stream! {
let mut interval = time::interval(Duration::from_secs(1));
for _ in 0..10 {
let time = interval.tick().await;
let elapsed = time.duration_since(start);
let note = proto!(RouteNote {
location: proto!(Point {
latitude: 409146138 + elapsed.as_secs() as i32,
longitude: -746188906,
}),
message: format!("at {elapsed:?}"),
});
yield note;
}
};
let response = client.route_chat(Request::new(outbound)).await?;
let mut inbound = response.into_inner();
while let Some(note) = inbound.message().await? {
println!("Note: Latitude = {}, Longitude = {}, Message = \"{}\"",
note.location().latitude(),
note.location().longitude(),
note.message());
}
Ok(())
}
Chociaż każda ze stron zawsze będzie otrzymywać wiadomości drugiej strony w kolejności, w jakiej zostały napisane, zarówno klient, jak i serwer mogą odczytywać i zapisywać dane w dowolnej kolejności – strumienie działają całkowicie niezależnie.
Wywoływanie metod pomocniczych
Aby wywołać metody usługi, musimy najpierw utworzyć kanał do komunikacji z serwerem. Najpierw tworzymy punkt końcowy, łączymy się z nim i przekazujemy utworzony w trakcie połączenia kanał do RouteGuideClient::new() w ten sposób:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create endpoint to connect to
let endpoint = Endpoint::new("http://[::1]:10000")?;
let channel = endpoint.connect().await?;
// Create a new client
let mut client = RouteGuideClient::new(channel);
Ok(())
}
W main() wykonaj utworzone przed chwilą metody.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create endpoint to connect to
let endpoint = Endpoint::new("http://[::1]:10000")?;
let channel = endpoint.connect().await?;
// Create a new client
let mut client = RouteGuideClient::new(channel);
println!("\n*** SERVER STREAMING ***");
print_features(&mut client).await?;
println!("\n*** CLIENT STREAMING ***");
run_record_route(&mut client).await?;
println!("\n*** BIDIRECTIONAL STREAMING ***");
run_route_chat(&mut client).await?;
Ok(())
}
7. Wypróbuj
Aby uruchomić klienta i serwer, najpierw dodajmy je jako binarne elementy docelowe do naszego pakietu. Musimy odpowiednio zmodyfikować plik Cargo.toml:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
Podobnie jak w przypadku każdego projektu musimy też pomyśleć o zależnościach, które są niezbędne do działania naszego kodu. W przypadku projektów Rust zależności będą znajdować się w pliku Cargo.toml. Wymagane zależności zostały już wymienione w pliku Cargo.toml.
Następnie uruchom te polecenia z naszych katalogów roboczych:
- Uruchom serwer w jednym terminalu:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- Uruchom klienta w innym terminalu:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client
Zobaczysz dane wyjściowe podobne do tych:
*** SERVER STREAMING *** FEATURE: Name = "Patriots Path, Mendham, NJ 07945, USA", Lat = 407838351, Lon = -746143763 FEATURE: Name = "101 New Jersey 10, Whippany, NJ 07981, USA", Lat = 408122808, Lon = -743999179 FEATURE: Name = "U.S. 6, Shohola, PA 18458, USA", Lat = 413628156, Lon = -749015468 ... *** CLIENT STREAMING *** Traversing 86 points SUMMARY: Feature Count = 0, Distance = 803709356 *** BIDIRECTIONAL STREAMING *** Note: Latitude = 409146138, Longitude = -746188906, Message = "at 112.45µs" Note: Latitude = 409146139, Longitude = -746188906, Message = "at 1.00011245s" Note: Latitude = 409146140, Longitude = -746188906, Message = "at 2.00011245s"
8. Co dalej?
- Dowiedz się, jak działa gRPC, w artykułach Wprowadzenie do gRPC i Podstawowe pojęcia.
- Zapoznaj się z samouczkiem dotyczącym podstaw.