Клиент-серверная архитектура
В самом базовом представлении, мы имеем:
- Клиент - приложение, через которое клиент общается с сервером
- Сервер - приложение, которое принимает запросы и отвечает на них, база данных и остальные обвязки

Сервер исполняет (или нет) действия, которые запросил у него клиент

Клиентом может быть как браузер или мобильное приложение, так и другой сервер.
Сервер - это поставщик данных. Клиент - это потребитель данных.

HTTP
HTTP - это протокол сетевого уровня для передачи информации. Является самым универсальным и широкораспространённым.

HTTP является самым верхним уровнем модели OSI

Любой HTTP запрос состоит из:
- стартовой строки - метода, url (пути запроса) и версии протокола
- заголовков - данные, которые влияют на запрос (авторизация, тип отправляемых данных, хост и так далее)
- тела (опционально) - пейлод от клиента

Основные методы запросов:
- GET - получение данных
- POST - передача данных / создание ресурса
- PUT - обновление ресурса
- PATCH - частичное обновление ресурса
- DELETE - удаление ресурса
Строка статуса HTTP/1.1 200
Статус коды:
- 1xx - информационные ответы
- 2xx - успех
- 200 - успех
- 201 - успех без ответа (ОК, создано)
- 3xx - редиректы
- 4xx - ошибки на стороне клиента
- 400 - некорректный запрос
- 404 - ресурс не найден
- 5xx - ошибки на стороне сервера
- 500 - ошибка сервера
RestAPI
API (application programming interface) - интерфейс для общения с программой

REST API - это архитектурный стиль, который описывает, как строить правильный и удобный API и правильно использовать возможности HTTP (его семантику)
Есть 6 принципов, которыми мы должны руководствоваться
- Клиент-серверная модель (client-server model) - есть потребитель и провайдер данных;
- Отсутствие состояния (statelessness) - сервер не должен запоминать промежуточные состояния между запросом и ответом. При каждом обращении клиента к серверу, их общение начинается как в первый раз;
- Кэширование (cacheability) - возможность закэшировать результат выполнения операции. Такое состояние сохраняют только POST и GET запросы - остальные кэшировать нельзя;
- Единообразие интерфейса (uniform interface) - все сущности должны выполнять консистентные операции в рамках одного приложения. Так же правильнее называть сущности во множественном числе, а для деления операции использовать правильные методы запросов;

- Многоуровневая система (layered system) - система должна иметь возможность состоять из большого количества слоёв (прокси, обработчики и так далее);
- Код по требованию (code on demand) — необязательно.
Идемпотентность - это свойство запроса, который при нескольких повторениях будет возвращать один и тот же результат. Такими могут быть только методы GET, PUT и DELETE.
Форматом обмена данными преимущественно является JSON, но так же им может быть и XML
Версионирование. Так же принято использовать версионирование в рамках REST, когда для обновления интерфейса, создаётся другой хэндлер с прописанной версией:
access/checkaccess/check/v2

Документирование. Средтством документирования API обычно является:
- спецификация OpenAPI
- и инструмент его имплементации - Swagger
SOAP
SOAP - это протокол обмена структурированными сообщениями. Применим для любого протокола прикладного уровня (SMTP, FTP, HTTP)
Можно в на одном бэкэнде написать как REST, так и SOAP ручки
Для описания веб-сервисов и доступа к ним использует язык WSDL, который описывает:
- какие операции предоставляет сервис
- какие типы данных используются
- где находится сервис (endpoint)
Простой WSDL-файл для сервиса, который возвращает данные о пользователе:
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://example.com/userservice"
targetNamespace="http://example.com/userservice">
<!-- Типы данных -->
<types>
<xs:schema targetNamespace="http://example.com/userservice">
<xs:element name="GetUserRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="UserId" type="xs:int"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="GetUserResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="Id" type="xs:int"/>
<xs:element name="Name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
</types>
<!-- Сообщения -->
<message name="GetUserRequest">
<part name="parameters" element="tns:GetUserRequest"/>
</message>
<message name="GetUserResponse">
<part name="parameters" element="tns:GetUserResponse"/>
</message>
<!-- Операции -->
<portType name="UserServicePortType">
<operation name="GetUser">
<input message="tns:GetUserRequest"/>
<output message="tns:GetUserResponse"/>
</operation>
</portType>
<!-- Привязка -->
<binding name="UserServiceBinding" type="tns:UserServicePortType">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
<operation name="GetUser">
<soap:operation soapAction="http://example.com/userservice/GetUser"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<!-- Сервис -->
<service name="UserService">
<port name="UserServicePort" binding="tns:UserServiceBinding">
<soap:address location="http://localhost:8080/userservice"/>
</port>
</service>
</definitions>Если RESTAPI представляет собой множество разных окон для общения, то SOAP описывает одно окно для общения, в которое мы передаём запрашиваемую для выполнения процедуру
Запрос процедуры состоит из 3 обязательных блоков:
- Envelope - блок для определения начала и конца сообщения с данными для адресации
- Header - хранит вспомогательную информацию по запросу
- Body - хранит в себе наименование целевой процедуры и payload запроса
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ex="http://example.com/">
<soapenv:Header/>
<soapenv:Body>
<ex:GetUserRequest>
<ex:UserId>123</ex:UserId>
</ex:GetUserRequest>
</soapenv:Body>
</soapenv:Envelope>GraphQL
Что такое GraphQL
GraphQL - это язык запросов для общения между клиентом и сервером. Он отличается от базового RestAPI полным указанием нужных полей, которые нам нужно вернуть с сервера.
Apollo - это платформа, которая позволяет удобно работать с GraphQL

GraphQL - это технология, которая позволяет на сервере описать схему данных, а на клиенте из этой схемы выбрать только нужные для конкретного запроса данные.
Это крайне удобно, так как в разных местах, например, по продукту нам нужная подробная информация с описанием и характеристиками, а в другом только название и цена.
Если REST описывает схему и формат возвращаемых данных с сервера, то GQL говорит описать формат на сервере, а на клиенте позволяет выбрать нужные данные из схемы.
Виды запросов
Для осуществления запросов на сервер, нам потребуется описать:
- Schema - схема данных, которые находятся на сервере (грубо говоря - интерфейсы)
- Query - запрос на сервер, который вернёт определённые данные

Query
Query (GET) - запрос на получение данных
- описываем схему
- отправляем запрос
- получаем ответ по схеме
# Schema
type Query { hero: Character}
# Request
{
hero {
name
}
}
# Response
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
Fragment
Fragments позволяют нам объединить несколько query и собрать из них один запрос, переиспользовав query
query {
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}И ответ будет представлен таким образом
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3PO"
},
{
"name": "R2-D2"
}
]
},
"rightComparison": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],Mutation
Mutation уже позволяет какие-либо данные добавить, обновить или перезаписать (аналог post/put запросов)
Конкретно тут вызвана мутация updateHumanName, по которой мы:
- отправляем на сервер
idиnameопределённого персонажа - по этим параметрам мы находим персонажа и изменяем его имя
- просим вернуть его новые
idиnameэтого персонажа
# Schema
type Mutation { updateHumanName(id: ID!, name: String!): Human}
# Request
## Operation
mutation UpdateHumanName($id: ID!, $name: String!) {
updateHumanName(id: $id, name: $name ) {
id
name
}
}
## Vars
{
"id": "1000",
"name": "Luke Starkiller"
}
# Response
{
"data": {
"updateHumanName": {
"id": "1000",
"name": "Luke Starkiller"
}
}
}
Subscription
И третий вид запросов - Subscription (подписки) - клиент слушает изменения в БД в режиме реального времени
Этот тип запроса реализован поверх протокола WebSocket
subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) {
storyLikeSubscribe(input: $input) {
story {
likers {
count
}
likeSentence {
text
}
}
}
}Преимущества
- Самодокументация (по эндпоинту доступна документация, где описаны все поля)
- Кодогенерация (можем сгенерировать для фронта типы по GQL
npx apollo codegen:generate --localSchemaFile=grapql-schema.json --target=typescript) - Клиент запрашивает только нужные данные
Реализация
Сервер
Устанавливаем зависимости
npm i express-graphql graphqlДалее нам нужно будет описать схему наших сущностей на сервере
Как выглядит описание сущностей:
- Мы описываем объекты User и Post (через
[Post]указываем вложенность от другой сущности) - Описываем возможные инпуты, которые придут к нам на сервер (через
!указываем обязательность поля) - Определяем мутации и запросы в схеме
schema.js
const {buildSchema} = require('graphql')
const schema = buildSchema(`
type User {
id: ID
username: String
age: Int
posts: [Post]
}
type Post {
id: ID
title: String
content: String
}
input UserInput {
id: ID
username: String!
age: Int!
posts: [PostInput]
}
input PostInput {
id: ID
title: String!
content: String!
}
type Query {
getAllUsers: [User]
getUser(id: ID): User
}
type Mutation {
createUser(input: UserInput): User
}
`)
module.exports = schemaЗатем реализовать сервер, на котором у нас будет эндпоинт с graphql
В него нам нужно будет передать rootValue (методы, которые будут работать с описанными запросами GraphQL) и нашу схему
index.js
const express = require('express')
const {graphqlHTTP} = require('express-graphql')
const cors = require('cors')
const schema = require('./schema')
const users = [{id: 1, username: "Vasya", age: 25}]
const app = express()
app.use(cors())
const createUser = (input) => {
const id = Date.now()
return {
id, ...input
}
}
const root = {
getAllUsers: () => {
return users
},
getUser: ({id}) => {
return users.find(user => user.id == id)
},
createUser: ({input}) => {
const user = createUser(input)
users.push(user)
return user
}
}
app.use('/graphql', graphqlHTTP({
graphiql: true,
schema,
rootValue: root
}))
app.listen(5000, () => console.log('server started on port 5000'))Клиент
Устанавливаем граф и клиент аполло для более удобного взаимодействия с сервером
npm i graphql @apollo/clientТут мы уже должны использовать Apollo клиент, который будет общаться с graphql сервером, кэшировать и провайдить данные
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client'
const client = new ApolloClient({
uri: 'http://localhost:5000/graphql',
cache: new InMemoryCache()
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);Опишем схемы для запросов данных
query / user.js
import {gql} from '@apollo/client'
export const GET_ALL_USERS = gql`
query {
getAllUsers {
id, username, age
}
}
`
export const GET_ONE_USER = gql`
query getUser($id: ID){
getUser(id: $id) {
id, username
}
}
`Опишем схемы для изменения данных
mutations / user.js
import {gql} from '@apollo/client'
export const CREATE_USER = gql`
mutation createUser($input: UserInput) {
createUser(input: $input) {
id, username, age
}
}
`И далее используем клиент на нашем вебе
Для отправки запросов мы должны воспользоваться хуками:
- useQuery, в который мы первым аргументом передаём схему запроса, а вторым пропсы
- useMutation, в который мы передаём наши мутации и на выходе получаем функцию-триггер запроса
App.js
import React, {useEffect, useState} from 'react';
import './App.css'
import {useMutation, useQuery} from "@apollo/client";
import {GET_ALL_USERS, GET_ONE_USER} from "./query/user";
import {CREATE_USER} from "./mutations/user";
const App = () => {
const [users, setUsers] = useState([])
const [username, setUsername] = useState('')
const [age, setAge] = useState(0)
const {data, loading, error, refetch} = useQuery(GET_ALL_USERS)
const {data:oneUser, loading: loadingOneUser} = useQuery(GET_ONE_USER, {
variables: {
id: 1
}
})
const [newUser] = useMutation(CREATE_USER)
console.log(oneUser)
useEffect(() => {
if (!loading) {
setUsers(data.getAllUsers)
}
}, [data])
const addUser = (e) => {
e.preventDefault()
newUser({
variables: {
input: {
username, age
}
}
}).then(({data}) => {
console.log(data)
setUsername('')
setAge(0)
})
}
const getAll = e => {
e.preventDefault()
refetch()
}
if (loading) {
return <h1>Loading...</h1>
}
return (
<div>
<form>
<input value={username} onChange={e => setUsername(e.target.value)} type="text"/>
<input value={age} onChange={e => setAge(e.target.value)} type="number"/>
<div className="btns">
<button onClick={(e) => addUser(e)}>Создать</button>
<button onClick={e => getAll(e)}>Получить</button>
</div>
</form>
<div>
{users.map(user =>
<div className="user">{user.id}. {user.username} {user.age}</div>
)}
</div>
</div>
);
};
export default App;WebSockets
WebSockets - это протокол, который позволяет установить между клиентом и сервером постоянную (дуплексную) связь для обмена данными
Это самый надёжный и real-time вариантов соединения, который используется для:
- стримингового потока видео (twitch)
- в чатах
- на холстах с несколькими пользователями (miro, figma)
- в новостях
- на биржах с непрерывным изменением стоимости акций
И у нас есть несколько способов реализации real-time взаимодействия кроме WS
Long polling
Это способ, при котором мы с клиента отправляем запрос на сервер, который висит ровно до тех пор, пока сервер не ответит на него. Если время ожидания закончилось, то мы с клиента отправляем запрос заново.
Если сервер нам что-нибудь вернёт, то мы опять отправляем запрос на получение данных с сервера.
Этот способ самый простой и требует от нас просто постоянно запрашивать данные с клиента на сервер

Тут пользователь отправляет get-запрос, но мы не возвращаем ему ответ (ответ мы забиндили в событие). Взамен мы ждём, когда другой участник чата отправит сообщение и уже только после этого событие в get-запросе вызываем, после чего всем участникам чата возвращается ответ
На сервере мы имеем:
- пост-запрос, который вызывает функцию внутри гет-запроса
- гет-запрос, который хранит в себе ивент, срабатываемый, когда на сервер отправляют запрос с данными
const express = require('express');
const cors = require('cors');
const events = require('events');
const PORT = 5000;
// инициализируем эмиттер событий
const emitter = new events.EventEmitter();
const app = express();
app.use(cors());
app.use(express.json());
// будет возвращать новые сообшения
app.get('/get-messages', (req, res) => {
// если пользователь отправил сообщение, то остальных пользователей чата нужно осведомить о доставке сообщения
emitter.once('newMessage', (message) => {
// всем пользователям, у кого висит подключение, отправяем сообщение
res.json(message);
});
});
// будет
app.post('/new-messages', (req, res) => {
const message = req.body;
// тут мы вывзаем событие из get, после того, как мы получили новое сообщение
emitter.emit('newMessage', message);
res.status(200);
});
// прослушиваем порт
app.listen(PORT, () => console.log(`server started on PORT ${PORT}`));На клиенте нам нужно написать функцию, которая будет отправлять постоянно запрос на получение данных subscribe() и функцию, которая будет отправлять эти данные sendMessage(). Уже только тогда после отправки сообщения триггернётся гет на сервере и отдаст сообщения
import React, { useEffect, useState } from 'react';
import './styles.css';
import axios from 'axios';
const LongPulling = () => {
const [messages, setMessages] = useState([]);
const [value, setValue] = useState('');
useEffect(() => {
subscribe();
}, []);
const subscribe = async () => {
try {
// тут мы получаем ответ от сервера и связь пропадает
const { data } = await axios.get('http://localhost:5000/get-messages');
setMessages((prev) => [data, ...prev]);
// тут мы переоформляем подписку, чтобы связь не пропадала
await subscribe();
} catch (e) {
// если произойдёт ошибка, то нам нужно будет просто переоформить подписку
setTimeout(() => {
subscribe();
}, 500);
}
};
const sendMessage = async () => {
await axios.post('http://localhost:5000/new-messages', {
message: value,
id: Date.now(),
});
};
return (
<div>
<div>
<h2>LongPulling</h2>
<div className='form'>
<input value={value} onChange={(e) => setValue(e.target.value)} type='text' />
<button onClick={sendMessage}>Отправить</button>
</div>
<div className='messages'>
{messages.map((mess) => (
<div className='message' key={mess.id}>
{mess.message}
</div>
))}
</div>
</div>
</div>
);
};
export default LongPulling;И теперь в двух разных браузерах мы реализовали чат в реальном времени

event soursing (server sent events)
Второй варинат взаимодействия - это event soursing, который подразумевает под собой, чтобы было установлено постоянно одностороннее соединение с сервера к клиенту.
Клиент только получает ответы от сервера на изменённые данные и больше ничего не происходит.
Строится данный подход на базе обычного https

На сервере мы создаём специальный заголовок, который определит, что наши запросы имеют постоянный характер и создаём многоповторный ивент, который будет записывать ответ специальным образом
const express = require('express');
const cors = require('cors');
const events = require('events');
const PORT = 5000;
const emitter = new events.EventEmitter();
const app = express();
app.use(cors());
app.use(express.json());
app.get('/connect', (req, res) => {
// тут мы задаём заголовок, что связь у нас будет постоянная
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
});
// этот же ивент может срабатывать множество раз, поэтому меняем once на on
emitter.on('newMessage', (message) => {
// тут мы обязательно оборачиваем строку в такой шаблон, чтобы она принялась классом EventSource
res.write(`data: ${JSON.stringify(message)} \n\n`);
});
});
app.post('/new-messages', (req, res) => {
const message = req.body;
emitter.emit('newMessage', message);
res.status(200);
});
app.listen(PORT, () => console.log(`server started on PORT ${PORT}`));На клиенте нужно поменять функцию subscribe(), которая будет работать с классом EventSource, который, в свою очередь, уже будет отслеживать получение сообщений
import React, { useEffect, useState } from 'react';
import './styles.css';
import axios from 'axios';
const EventSourcing = () => {
const [messages, setMessages] = useState([]);
const [value, setValue] = useState('');
useEffect(() => {
subscribe();
}, []);
const subscribe = async () => {
// создаём ивентсурс с ссылкой на коннекшн-контроллер
const eventSource = new EventSource(`http://localhost:5000/connect`);
// тут мы каждый раз при получении сообщения выполняем действие
eventSource.onmessage = function (event) {
const message = JSON.parse(event.data);
setMessages((prev) => [message, ...prev]);
};
};
const sendMessage = async () => {
await axios.post('http://localhost:5000/new-messages', {
message: value,
id: Date.now(),
});
};
return (
<div>
<div>
<h2>EventSourcing</h2>
<div className='form'>
<input value={value} onChange={(e) => setValue(e.target.value)} type='text' />
<button onClick={sendMessage}>Отправить</button>
</div>
<div className='messages'>
{messages.map((mess) => (
<div className='message' key={mess.id}>
{mess.message}
</div>
))}
</div>
</div>
</div>
);
};
export default EventSourcing;И наш чат всё так же продолжает работать, но теперь посредством передачи сырых данных

WebSockets
Это самый мощный способ организовать взаимодействие между клиентом и сервером и требует поднятия отдельного вебсокет-сервера

Изначально нам нужно поднять отдельный сервер вебсокетов
Далее мы описываем события, при которых будут срабатывать сокеты.
WebSocket создаётся для одного человека на одно подключение и сообщение по-умолчанию будет передаваться только самому пользователю. Чтобы оно отправлялось сразу обоим людям в чате, нужно создать широковещатель broadcastMessage().
const ws = require('ws');
// запускаем сервер вебсокетов
const wss = new ws.Server(
{
port: 5000,
},
() => console.log(`Server started on 5000`)
);
// при подлючении сокета
wss.on('connection', function connection(ws) {
// при отпрвке сообщения
ws.on('message', function (message) {
// мы получаем сообщение с клиента
message = JSON.parse(message);
// и при разных событиях в сообщении (есть событие подключения и просто отправки сообщения), будем выполнять "разные" действия
switch (message.event) {
case 'message':
broadcastMessage(message);
break;
case 'connection':
broadcastMessage(message);
break;
}
});
});
// распространяем сообщение по пользователям
function broadcastMessage(message, id) {
// перебираем всех клиентов
wss.clients.forEach((client) => {
// каждый клиент является вебсокетом и можно каждому отправить сообщение
client.send(JSON.stringify(message));
});
}На клиенте нужно уже будет описать подключение к сокетам и описать реакции сокета на его разные состояния (ошибка, подключение и так далее)
import React, { useEffect, useRef, useState } from 'react';
import axios from 'axios';
const WebSock = () => {
const [messages, setMessages] = useState([]);
const [value, setValue] = useState('');
const [connected, setConnected] = useState(false);
const [username, setUsername] = useState('');
// чтобы не потерять сокет при перерендере, присваиваем его в реф
const socket = useRef();
function connect() {
// присваиваем сюда сокет
socket.current = new WebSocket('ws://localhost:5000');
// при открытии сокета
socket.current.onopen = () => {
setConnected(true);
// сообщение о подключении пользователя к сокетам
const message = {
event: 'connection',
username,
id: Date.now(),
};
// отправит сообщение на сервер
socket.current.send(JSON.stringify(message));
console.log('Socket подключен');
};
// при получении сообщения от сокета
socket.current.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages((prev) => [message, ...prev]);
};
// при закрытии сокета
socket.current.onclose = () => {
console.log('Socket закрыт');
};
// при ошибке в сокете
socket.current.onerror = () => {
console.log('Socket произошла ошибка');
};
}
const sendMessage = async () => {
const message = {
username,
message: value,
id: Date.now(),
event: 'message',
};
socket.current.send(JSON.stringify(message));
setValue('');
};
if (!connected) {
return (
<div className='center'>
<div className='form'>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
type='text'
placeholder='Введите ваше имя'
/>
<button onClick={connect}>Войти</button>
</div>
</div>
);
}
return (
<div className='center'>
<div>
<div className='form'>
<input value={value} onChange={(e) => setValue(e.target.value)} type='text' />
<button onClick={sendMessage}>Отправить</button>
</div>
<div className='messages'>
{messages.map((mess) => (
<div key={mess.id}>
{mess.event === 'connection' ? (
<div className='connection_message'>
Пользователь {mess.username} подключился
</div>
) : (
<div className='message'>
{mess.username}. {mess.message}
</div>
)}
</div>
))}
</div>
</div>
</div>
);
};
export default WebSock;
RPC (gRPC, tRPC)
RPC - remote produce call - это технология, которая релизуется как удалённый вызов метода, который имплементирован на бэкэнде
В таком случае взаимодействия, мы на клиенте вызываем не запросы, а реализованные на бэкэнде функции через объект stub

Protobuf
Это строготипизированный язык и формат сериализации данных
Такой подход заставляет сервер сериализовывать данные, а клиенту десериализовывать их
Основным преимуществом данного формата является максимальное сжатие данных и минимизация потоков по сети
Схема описывается в формате .proto и прогоняется через компилятор protoc с дальнейшим переводом в любой нужный нам язык
message User {
int32 id = 1;
string name = 2;
}
gRPC
gRPC - это современное представление технологии от Google
Преимущества gRPC:
- Использует HTTP2 вместо HTTP1/1
- Работает в разы быстрее
- Использует бинарную передачу данных (protobuf) для более сильного сжатия данных
- удобный вызов процедур
- простой и быстрый стриминг данных
- инструментарий из коробки
- генерация кода
- аутентификация
- потоковая передача данных
Пример описанного proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// Определение Greeter сервиса
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloStreamReply (HelloRequest) returns (stream HelloReply) {}
}
// Запрос с именем пользователя
message HelloRequest {
string name = 1;
}
// Сообщение с ответом
message HelloReply {
string message = 1;
}Пример сервера
var PROTO_PATH = __dirname + '/../../protos/helloworld.proto';
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
);
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
// имплементация метода приветствия
function sayHello(call, callback) {
callback(null, {message: 'Hello ' + call.request.name});
}
// запуск сервера, который будет получать запросы Greeter сервиса
function main() {
var server = new grpc.Server();
server.addService(hello_proto.Greeter.service, {sayHello: sayHello});
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err != null) {
return console.error(err);
}
console.log(`gRPC listening on ${port}`)
}
);
}
main();Клиентская сторона
var PROTO_PATH = __dirname + '/../../protos/helloworld.proto';
var parseArgs = require('minimist');
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
function main() {
var argv = parseArgs(process.argv.slice(2), {
string: 'target'
});
var target;
if (argv.target) {
target = argv.target;
} else {
target = 'localhost:50051';
}
var client = new hello_proto.Greeter(target,
grpc.credentials.createInsecure());
var user;
if (argv._.length > 0) {
user = argv._[0];
} else {
user = 'world';
}
client.sayHello({name: user}, function(err, response) {
console.log('Greeting:', response.message);
});
}
main();tRPC
tRPC - typesafety RPC - более простое в реализации представление RPC, которое имеет удобный синтаксис для TS
Сервер
Сервер express + tRPC:
server.ts
import express from "express";
import { inferAsyncReturnType, initTRPC } from "@trpc/server";
import * as trpcExpress from "@trpc/server/adapters/express";
import cors from "cors";
// контекстные опции Express
const createContext = ({ req, res }: trpcExpress.CreateExpressContextOptions) => ({});
type Context = inferAsyncReturnType<typeof createContext>;
// инициализация контекста
const t = initTRPC.context<Context>().create();
// Роутер с методами
const appRouter = t.router({
// процедура получения пользователя
getUser: t.procedure
.input((val: unknown) => {
if (typeof val === "number") return val;
throw new Error("Invalid input");
})
.query(({ input }) => {
return { id: input, name: "Valery" };
}),
// добавить пользователя
add: t.procedure
.input((val: unknown) => {
if (typeof val === "object" && val && "a" in val && "b" in val) {
return val as { a: number; b: number };
}
throw new Error("Invalid input");
})
.mutation(({ input }) => {
return { sum: input.a + input.b };
}),
});
// тип роутера
export type AppRouter = typeof appRouter;
const app = express();
app.use(cors());
// подключение tRPC middleware с префиксом /trpc
app.use(
"/trpc",
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);
// запуск сервера
app.listen(4000, () => {
console.log("🚀 tRPC server running on http://localhost:4000/trpc");
});Клиент
На клиенте нужно будет инициализировать прокси клиент tRPC и описать типы его методов. Тут методы мы стянем напрямую с сервера.
client.ts
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "../server"; // Импортируем типы с сервера
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: "http://localhost:4000/trpc",
}),
],
});И далее мы уже сможем напрямую вызывать процедуры с сервера прямо в useEffect
App.tsx
import { useEffect, useState } from "react";
import { trpc } from "./client";
function App() {
const [user, setUser] = useState<{ id: number; name: string } | null>(null);
const [sum, setSum] = useState<number | null>(null);
useEffect(() => {
// Пример запроса
// вызываются через query
trpc.getUser.query(123).then((res) => setUser(res));
// Пример мутации
// изменения данных вызываются через mutate
trpc.add.mutate({ a: 5, b: 7 }).then((res) => setSum(res.sum));
}, []);
return (
<div style={{ padding: 20 }}>
<h1>tRPC React + Express Example</h1>
{user && (
<p>
👤 User: {user.name} (id: {user.id})
</p>
)}
{sum !== null && <p>➕ Sum: {sum}</p>}
</div>
);
}
export default App;Интеграции на клиенте
Зависимости клиента
npm install @trpc/client @trpc/react-query @tanstack/react-queryИнициализация tRPC для React
trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../server"; // импорт типов с бэкенда
// Инициализация React-хуков
export const trpc = createTRPCReact<AppRouter>();Инициализация клиента
client.ts
import { httpBatchLink } from "@trpc/client";
import { trpc } from "./trpc";
export const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: "http://localhost:4000/trpc", // backend URL
}),
],
});Обёртки
main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { trpc, trpcClient } from "./client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")!).render(
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</trpc.Provider>
);Мутации с ипользованием хуков
App.tsx
import { trpc } from "./trpc";
function App() {
// Автоматический типобезопасный запрос
const { data: user, isLoading: userLoading, error: userError } = trpc.getUser.useQuery(123);
// Мутация с invalidate cache
const utils = trpc.useUtils();
const addMutation = trpc.add.useMutation({
onSuccess: () => {
utils.invalidate(); // перезапросить данные, если надо
},
});
return (
<div style={{ padding: 20 }}>
<h1>tRPC React + Express Example</h1>
{userLoading && <p>Загружаем пользователя...</p>}
{userError && <p>Ошибка: {userError.message}</p>}
{user && <p>👤 {user.name} (id: {user.id})</p>}
<button onClick={() => addMutation.mutate({ a: 5, b: 7 })}>
Сложить 5 + 7
</button>
{addMutation.isLoading && <p>Считаем...</p>}
{addMutation.data && <p>➕ Результат: {addMutation.data.sum}</p>}
</div>
);
}
export default App;