Клиент-серверная архитектура
Клиент-серверная архитектура (Client-Server Architecture) делит систему на две роли, которые работают в разных процессах и обычно на разных машинах: клиенты отправляют запросы, а сервер их обрабатывает и отвечает. Данные и правила, которым можно доверять, живут на сервере, и он - единственный источник истины сразу для всех клиентов. Клиент бывает тонким или толстым, но последнее слово в том, чему можно доверять, всегда за сервером. Между ними лежит сеть, поэтому каждое сообщение сериализуется, идёт с задержкой и может потеряться.
Проблема
До клиент-сервера было две крайности. На мейнфрейме всё работало на одной большой машине, а терминал был только экраном и клавиатурой. Когда появились персональные компьютеры, у каждого оказалась своя копия данных: два сотрудника регистрируют один и тот же email на разных машинах, и уже никто не скажет, какой список верный. Нужно одно место, которое хранит общие данные и следит за правилами, и при этом машина пользователя должна сама вести интерфейс. Слоистая архитектура этого не решает: она делит код по ролям, но все слои работают в одном процессе и ничего не говорят о сети между ними.
Решение
Сервер хранит данные и правила и ждёт запросов. Клиент показывает интерфейс, собирает ввод, отправляет запрос и получает ответ. Инициатива у клиента: сервер не обращается к клиенту первым, он отвечает. Даже push-уведомления (WebSocket) идут по соединению, которое открыл клиент. Между ними граница сети: сообщения передаются как данные (в примере JSON), общих объектов нет, ответ приходит с задержкой и может не прийти. Поэтому сервер перепроверяет каждый запрос, ведь клиент можно обойти, а клиент проверяет формат только ради удобства. Каждая регистрация несёт ключ requestId, чтобы повтор после сбоя не выполнил действие второй раз.
Как это работает
Слова старше самого стиля. В ARPANET в 1969 году говорили о «server-host» и «user-host»: так они названы, например, в RFC 5 Джеффа Рулифсона (июнь 1969). Слово client в нынешнем смысле, по данным Википедии, впервые появилось в статье сотрудников Xerox PARC Говарда Стёрджиса, Джеймса Митчелла и Джея Израэля в 1978 году. Названием целого стиля корпоративных систем «клиент-сервер» стал в 1980-х, вместе с персональными компьютерами в локальных сетях.
Предшественник - мейнфрейм с терминалами. Все программы и данные жили на одной центральной машине, а терминал был только экраном и клавиатурой без собственных программ. С 1980-х до конца 1990-х организации переносили вычисления с мейнфреймов и мини-компьютеров на персональные компьютеры. ПК стал клиентом со своим интерфейсом и логикой, а общие данные остались на сервере.
Отсюда шкала от тонкого до толстого клиента. Терминал - предельно тонкий клиент: всё решает сервер. Толстый клиент (его ещё называют rich или fat) многое делает сам и иногда может работать без сети, но всё равно время от времени связывается с сервером. Клиент в примере тонкий: он проверяет только формат, а всё, что имеет значение, решает сервер. Где провести черту - выбор, но правило одно: проверка на клиенте - для удобства, проверка на сервере - закон.
Клиент-сервер - стиль про уровни (tiers), а не про слои (layers). Слой - логическое деление кода, уровень - отдельный процесс или машина. Каждый уровень внутри вполне может быть слоистым. Марк Ричардс и Нил Форд в «Fundamentals of Software Architecture» (2020) относят клиент-сервер к фундаментальным паттернам (Fundamental Patterns) и разбирают три его варианта: настольное приложение с сервером базы данных, браузер с веб-сервером и трёхуровневую схему, где между интерфейсом и базой стоит сервер приложений. В примере два уровня смоделированы в одной вкладке.
Рой Филдинг в диссертации 2000 года, где описан стиль REST, начинает именно с ограничения client-server: это первое ограничение, которое он добавляет. Его довод - разделение ответственности: интерфейс отделён от хранения данных, поэтому интерфейс переносится на разные платформы, сервер упрощается и лучше масштабируется, а части развиваются независимо. Следующее ограничение REST - отсутствие состояния сеанса на сервере: каждый запрос несёт всё, что нужно для его обработки, как запросы в примере.
Роль определяется не размером машины и не тем, где сидит пользователь. В X Window System (MIT, 1984; X11 с 1987 года) X-сервер работает на компьютере пользователя: он владеет экраном, клавиатурой и мышью. А программы - клиенты - могут работать на удалённой машине и посылать серверу запросы на рисование. Многим это кажется перевёрнутым, но логика та же: сервер - тот, кто предоставляет услугу, клиент - тот, кто её запрашивает.
Противоположность - peer-to-peer. Там узлы равноправны, каждый одновременно клиент и сервер, и общаются они напрямую. Единого источника истины нет: узлам приходится договариваться и сводить данные. Клиент-сервер покупает простоту одной истины ценой единой точки отказа и узкого места. Сравнение с соседями по категории: слоистая архитектура делит код, гексагональная и микроядро устраивают одно приложение изнутри, а клиент-сервер разносит систему по процессам.
Нюансы выбора
Слои или уровни - если система работает в одном процессе для одного пользователя, хватит слоистой архитектуры, а сеть будет чистой издержкой. Отдельные уровни нужны, когда пользователей много, данные общие или правила должны жить там, куда клиент не дотянется.
Тонкий или толстый клиент - тонкий проще обновлять (всё на сервере), и логика не уходит к пользователю, но без сети он бесполезен. Толстый быстрее отвечает и работает офлайн, но логика дублируется, а у пользователей годами живут старые версии клиента.
Два уровня или три - клиент, который ходит прямо в базу данных, прост, но база открыта каждому клиенту, и правила расползаются по клиентам. Сервер приложений посередине прячет базу и держит правила в одном месте.
Повторы и идемпотентность - ответ может потеряться после того, как сервер всё сделал, поэтому каждому запросу, который меняет данные, нужен ключ для безопасного повтора. Так устроен заголовок Idempotency-Key в API Stripe.
Против клиент-сервера - если у данных нет естественного владельца и узлы равноправны (обмен файлами, совместная работа без центра), смотрите в сторону peer-to-peer. Если единственный сервер не выдерживает нагрузку, это уже вопрос распределённых систем.
Примеры и история
Roy Fielding, «Architectural Styles and the Design of Network-based Software Architectures» (диссертация, UC Irvine, 2000) - глава 5 о REST: client-server как первое ограничение и его обоснование разделением ответственности.
Mark Richards, Neal Ford, «Fundamentals of Software Architecture» (2020) - глава 9 «Foundations»: в разделе Fundamental Patterns рядом с Big Ball of Mud и Unitary Architecture - Client/Server с вариантами настольное приложение + база, браузер + веб-сервер и трёхуровневая схема.
Tim Berners-Lee, «Information Management: A Proposal» (CERN, март 1989) - предложение, из которого вырос веб; первым веб-сервером стал nxoc01.cern.ch, позже info.cern.ch.
X Window System (MIT, Project Athena, 1984; X11 - сентябрь 1987) - пример, где сервер работает на машине пользователя, а клиенты-программы могут быть удалёнными.
Stripe API, идемпотентные запросы - клиент сам создаёт ключ (рекомендуется UUID v4) и шлёт его в заголовке Idempotency-Key, сервер сохраняет результат первого запроса и на повтор с тем же ключом возвращает его. Похожий заголовок обсуждается как черновик стандарта в IETF.
Когда применять
- Когда у многих пользователей должны быть общие данные: учётные записи, заказы, сообщения, документы.
- Когда правила должны выполняться там, куда пользователь не дотянется: деньги, права доступа, уникальность.
Примеры из практики
- Веб - браузер клиент, веб-сервер отвечает по HTTP. Первый веб-сервер и браузер WorldWideWeb Тим Бернерс-Ли написал в CERN в 1990 году, а первая версия протокола (1991, позже её назвали HTTP/0.9) состояла из одного запроса
GETи одного ответа, после которого сервер закрывал соединение. - Электронная почта - почтовый клиент вроде Thunderbird работает с письмами на сервере по IMAP и отправляет их через SMTP-сервер. Письма хранятся на сервере, поэтому с телефона и ноутбука видна одна и та же папка «Входящие».
Похожие паттерны
Слоистая архитектура
Слоистая архитектура (Layered Architecture) делит приложение на горизонтальные слои, у каждого из которых одна техническая роль: показать данные пользователю, применить бизнес-правила, сохранить и прочитать данные. Слой обращается только к слою под ним, а нижние слои ничего не знают о верхних. Поэтому любой слой можно понять, протестировать и заменить, не разбираясь в остальных.
Гексагональная архитектура
Гексагональная архитектура (Hexagonal Architecture), её второе название - Порты и адаптеры (Ports and Adapters), ставит бизнес-правила в центр приложения и отделяет их от всего внешнего: интерфейса, базы данных, тестов. Ядро объявляет порты - интерфейсы, через которые с ним можно говорить и через которые оно само просит данные, - а снаружи к портам подключаются адаптеры. Все зависимости направлены внутрь, к ядру, поэтому интерфейс или хранилище можно заменить другим адаптером того же порта, не меняя ни строчки в правилах.
Архитектура микроядра
Архитектура микроядра (Microkernel Architecture), её также называют архитектурой плагинов (plug-in architecture), делит приложение на маленькое ядро с минимумом функций и подключаемые к нему плагины. Ядро знает, какие плагины установлены, и вызывает их через общий контракт, а каждая новая функция или правило продукта приходит отдельным плагином. Поэтому возможности продукта растут и настраиваются под конкретного клиента подключением и отключением плагинов, а не правкой ядра - пока новый плагин укладывается в контракт ядра.