Применение gRPC при разработке приложений под iOS
Всем привет! При широком распространении 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
}
}
}
}
}В случае успешного запуска приложения при запущенном сервере все будет работать.
В следующих статьях разберу подробнее, что генерируется и как работает все взаимодействие "под капотом".
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.