Всем привет! При широком распространении REST подхода для клиент‑серверного взаимодействия всегда существовали и существуют альтернативные решения, иногда незаслуженно обделенные вниманием. Об одном из них речь пойдет в сегодняшней статье. В ней будут даны некоторые теоретические основы, необходимые для работы и создан небольшой проект, который можно использовать для перехода на gRPC.
Что такое gRPC?
Чтобы начать разбираться в основной теме материала необходимо повторить/изучить то, что подробно рассматриваться не будет, а именно — REST. Хорошее объяснение того, что это такое можно найти в этой статье. Если сформулировать краткую выдержку, то получится, что REST (Representational State Transfer / Передача состояния представления) — это архитектурный стиль для проектирования распределённых приложений, который определяет набор правил для взаимодействия компонентов (клиента и сервера) через интернет, используя протокол HTTP и его методы (GET, POST, PUT, DELETE) для работы с ресурсами (данными). Ресурсы идентифицируются некоторым URL, а формат данных для передачи чаще всего представлен JSON‑ом. REST действительно очень популярен в нынешнее время, и мобильная разработка тут исключением не является.
Однако при всех имеющихся плюсах, недостатки тоже имеют место быть:
Наличие «лишних» данных при передаче — часто бывает так, что с выходом новой версии приложения существующие запросы дорабатываются, какие‑то поля из ответов становятся больше не нужны, однако нельзя забывать о том, что старые версии все еще продолжают с ними работать, и поэтому убрать их нельзя на стороне сервера, что в итоге приводит к избытку данных и дополнительному трафику
Использование текстового а не бинарного формата данных (возможность обмениваться бинарной информацией тоже есть, но не распространена) — тоже создает нагрузку для трафика, хоть и удобен для отладки запросов через программы‑снифферы
Избыток запросов — со временем и в следствие неверного построения API, количество дублирующих/очень похожих друг на друга запросов/методов растет, к тому же сложно применять подобный подход в системах, где требуется постоянная передача данных, к примеру мессенджеры
Все вышеперечисленные моменты привели к появлению других решений. Одно из таких — RPC. Это в некотором роде концепция, объединяющая под собой разные имплементации, однако и про нее стоит почитать, например в этом материале. Если вкратце, то RPC (Remote Procedure Call / Удалённый вызов процедур) — это технология в программировании, которая позволяет программе (клиенту) вызывать функции или процедуры на другом компьютере (сервере) в сети так, будто они находятся локально, скрывая сложность сетевого взаимодействия от разработчика. Часто этот подход используют при создании распределенных систем.
Так вот gRPC и является одним из реализаций RPC‑подхода. gRPC (Google Remote Procedure Calling) — фреймворк с открытым исходным кодом от Google для удалённых вызовов процедур, использующий HTTP/2 в качестве транспорта и Protocol Buffers для сериализации данных, что делает его независимым от языка программирования. В этом и заключается одно из его преимуществ. Для теоретического изучения его основ лучше обратиться к имеющимся материалам, так как данная статья более практическая и всю теорию не охватит.
Настраиваем все необходимое
Для работы потребуется установить некоторое ПО. Предполагается, что компьютер находится под управлением MacOS, среда разработки приложения — Xcode, в общем все максимально нативное с точки зрения Apple.
Пишем сервер
Для тестирования сгодится локальный «сервер», работающий на Вашем компьютере, поэтому следует его написать. Это можно сделать на нескольких языках программирования, что поддерживают grpc, тут в примере будет сделан сервер на Swift, но отталкиваться нужно от того, какая реализация будет удобнее.
Для создания iOS‑приложения (и серверной части) следует предварительно поставить нужные инструменты. Через Homebrew надо установить protobuf.
brew install protobufswift-protobuf protoc-gen-grpc-swift
Создайте папку проекта и выполните следующую командуswift package init --type executable
В SPM добавить зависимость от grpc‑swift. Наш целевой вид package‑а может выглядеть так (есть корреляции в зависимости от версии ОС и grpc, какие решено было использовать)
let package = Package( name: "GrpcServer", platforms: [.iOS(.v13), .macOS(.v12)], dependencies: [ .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.24.1"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"), ], targets: [ .executableTarget( name: "GrpcServer", dependencies: [ .product(name: "GRPC", package: "grpc-swift"), .product(name: "SwiftProtobuf", package: "swift-protobuf"), ] ) ] )
Контракт между сервером и клиентом описывается в файле формата.proto. Его необходимо так же разместить для удобства в директории проекта. Содержимое выглядит следующим образом:
syntax = "proto3"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
После чего требуется запустить команду (пути заменить на локальные):
protoc \ --plugin=protoc-gen-grpc-swift=$(pwd)/.build/debug/protoc-gen-grpc-swift \ --swift_out=. \ --grpc-swift_out=Sources \ --proto_path=Sources \ Sources/greeter.proto
Обратите внимание, что сгенерированные файлы должны лежать вместе с основным кодом — main.swift.
import GRPC import NIO import Foundation final class GreeterProvider1: GreeterAsyncProvider { func sayHello( request: HelloRequest, context: GRPCAsyncServerCallContext ) async throws -> HelloReply { let recipient = request.name.isEmpty ? "stranger" : request.name return HelloReply.with { $0.message = "Hello \(recipient)!" } } } let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) defer { try! group.syncShutdownGracefully() } let provider = GreeterProvider1() let server = Server.insecure(group: group) .withServiceProviders([provider]) .bind(host: "127.0.0.1", port: 50051) server.map { $0.channel.localAddress }.whenSuccess { address in print("Сервер запущен на \(address!)") } _ = try server.flatMap { $0.onClose }.wait()
Запустить код можно, вызвавswift run GrpcServer
При возникновении проблем, можно обратиться к следующим командам для сброса результатов и очистки:
swift package clean rm -rf .build rm -f Package.resolved
Пишем клиент
Создадим iOS‑клиент для тестирования взаимодействия. В SPM поставим зависимости GRPC версии, совместимой с серверной, и SwiftProtobuf. Сгенерированные файлы (pb.swift и grpc.swift) для серверной части необходимо добавить в bundle проекта.
Зависимости могут выглядеть следующим образом:

Код нашего network‑сервиса выглядит так:
import Foundation import GRPC import NIO final class GreeterClientService { private let group: EventLoopGroup private let connection: ClientConnection private let client: GreeterClientProtocol private let channel: GRPCChannel init(host: String = "127.0.0.1", port: Int = 50051) { self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) let configuration = ClientConnection.Configuration( target: .hostAndPort(host, port), eventLoopGroup: group ) self.connection = ClientConnection(configuration: configuration) self.channel = try! GRPCChannelPool.with( target: .host(host, port: port), transportSecurity: .plaintext, eventLoopGroup: group ) self.client = GreeterClient(channel: channel) } func sayHello(name: String) throws -> String { var request = HelloRequest() request.name = name let response = try client.sayHello(request).response.wait() return response.message } func shutdown() { try? connection.close().wait() try? group.syncShutdownGracefully() } }
Код ContentView, где происходит обращение к сервису и получение результата:
struct ContentView: View { @State private var name: String = "" @State private var reply: String = "" @State private var isLoading: Bool = false @State private var errorMessage: String? private let client = GreeterClientService( host: "127.0.0.1", port: 50051 ) var body: some View { VStack(spacing: 20) { TextField("Введите имя", text: $name) .textFieldStyle(.roundedBorder) .padding(.horizontal) Button("Say Hello") { callServer() } .disabled(name.isEmpty || isLoading) .buttonStyle(.borderedProminent) if isLoading { ProgressView() } if let error = errorMessage { Text("Ошибка: \(error)") .foregroundColor(.red) } if !reply.isEmpty { Text(reply) .font(.title2) .padding() } } .padding() .onDisappear { client.shutdown() } } private func callServer() { isLoading = true errorMessage = nil DispatchQueue.global(qos: .userInitiated).async { do { let result = try client.sayHello(name: name) DispatchQueue.main.async { reply = result isLoading = false } } catch { DispatchQueue.main.async { errorMessage = error.localizedDescription isLoading = false } } } } }
В случае успешного запуска приложения при запущенном сервере все будет работать.
В следующих статьях разберу подробнее, что генерируется и как работает все взаимодействие «под капотом».