создание клиент серверного приложения c
Клиент-серверное приложение на потоковом сокете TCP
В следующем примере используем TCP, чтобы обеспечить упорядоченные, надежные двусторонние потоки байтов. Построим завершенное приложение, включающее клиент и сервер. Сначала демонстрируем, как сконструировать на потоковых сокетах TCP сервер, а затем клиентское приложение для тестирования нашего сервера.
Следующая программа создает сервер, получающий запросы на соединение от клиентов. Сервер построен синхронно, следовательно, выполнение потока блокируется, пока сервер не даст согласия на соединение с клиентом. Это приложение демонстрирует простой сервер, отвечающий клиенту. Клиент завершает соединение, отправляя серверу сообщение
Сервер TCP
Создание структуры сервера показано на следующей функциональной диаграмме:
Вот полный код программы SocketServer.cs:
Давайте рассмотрим структуру данной программы.
Первый шаг заключается в установлении для сокета локальной конечной точки. Прежде чем открывать сокет для ожидания соединений, нужно подготовить для него адрес локальной конечной точки. Уникальный адрес для обслуживания TCP/IP определяется комбинацией IP-адреса хоста с номером порта обслуживания, которая создает конечную точку для обслуживания.
Класс Dns предоставляет методы, возвращающие информацию о сетевых адресах, поддерживаемых устройством в локальной сети. Если у устройства локальной сети имеется более одного сетевого адреса, класс Dns возвращает информацию обо всех сетевых адресах, и приложение должно выбрать из массива подходящий адрес для обслуживания.
Создадим IPEndPoint для сервера, комбинируя первый IP-адрес хост-компьютера, полученный от метода Dns.Resolve(), с номером порта:
Здесь класс IPEndPoint представляет localhost на порте 11000. Далее новым экземпляром класса Socket создаем потоковый сокет. Установив локальную конечную точку для ожидания соединений, можно создать сокет:
Перечисление AddressFamily указывает схемы адресации, которые экземпляр класса Socket может использовать для разрешения адреса.
В параметре SocketType различаются сокеты TCP и UDP. В нем можно определить в том числе следующие значения:
Dgram
Поддерживает дейтаграммы. Значение Dgram требует указать Udp для типа протокола и InterNetwork в параметре семейства адресов.
Поддерживает доступ к базовому транспортному протоколу.
Stream
Поддерживает потоковые сокеты. Значение Stream требует указать Tcp для типа протокола.
Следующим шагом должно быть назначение сокета с помощью метода Bind(). Когда сокет открывается конструктором, ему не назначается имя, а только резервируется дескриптор. Для назначения имени сокету сервера вызывается метод Bind(). Чтобы сокет клиента мог идентифицировать потоковый сокет TCP, серверная программа должна дать имя своему сокету:
Метод Bind() связывает сокет с локальной конечной точкой. Вызывать метод Bind() надо до любых попыток обращения к методам Listen() и Accept().
Теперь, создав сокет и связав с ним имя, можно слушать входящие сообщения, воспользовавшись методом Listen(). В состоянии прослушивания сокет будет ожидать входящие попытки соединения:
В параметре определяется задел (backlog), указывающий максимальное число соединений, ожидающих обработки в очереди. В приведенном коде значение параметра допускает накопление в очереди до десяти соединений.
В состоянии прослушивания надо быть готовым дать согласие на соединение с клиентом, для чего используется метод Accept(). С помощью этого метода получается соединение клиента и завершается установление связи имен клиента и сервера. Метод Accept() блокирует поток вызывающей программы до поступления соединения.
Метод Accept() извлекает из очереди ожидающих запросов первый запрос на соединение и создает для его обработки новый сокет. Хотя новый сокет создан, первоначальный сокет продолжает слушать и может использоваться с многопоточной обработкой для приема нескольких запросов на соединение от клиентов. Никакое серверное приложение не должно закрывать слушающий сокет. Он должен продолжать работать наряду с сокетами, созданными методом Accept для обработки входящих запросов клиентов.
Как только клиент и сервер установили между собой соединение, можно отправлять и получать сообщения, используя методы Send() и Receive() класса Socket.
Метод Send() записывает исходящие данные сокету, с которым установлено соединение. Метод Receive() считывает входящие данные в потоковый сокет. При использовании системы, основанной на TCP, перед выполнением методов Send() и Receive () между сокетами должно быть установлено соединение. Точный протокол между двумя взаимодействующими сущностями должен быть определен заблаговременно, чтобы клиентское и серверное приложения не блокировали друг друга, не зная, кто должен отправить свои данные первым.
Когда обмен данными между сервером и клиентом завершается, нужно закрыть соединение используя методы Shutdown() и Close():
Сокет закрывается при вызове метода Close(), который также устанавливает в свойстве Connected сокета значение false.
Клиент на TCP
Функции, которые используются для создания приложения-клиента, более или менее напоминают серверное приложение. Как и для сервера, используются те же методы для определения конечной точки, создания экземпляра сокета, отправки и получения данных и закрытия сокета:
Вот полный код для SocketClient.cs и его объяснение:
Простой и быстрый сервер на C/C++ с клиентом на C#: TCP версия
Всем привет. Потихоньку перебирая листы книг, занимаюсь я серверным программированием. И дошёл мой разум до того, что можно было бы и на C++ сервер написать. Ну и недолго думая(точнее вообще не думая и плохо зная плюсы), я пошёл писать сервер.
И начал я натыкаться на ошибки разные, до этого чуждые мне, да на синтаксис непонятный. И в итоге, сейчас делюсь с Вами тем, как надо и как не надо писать даже самый простой сервер.
И так начнём.
Начнём с самого главного элемента — класс сервера SServer:
Тут думаю сложно не будет. SOCKET создаём как сокет для сервера, он будет слушать.
WSAData нужен для активации использования сокетов в Windows.
Это был заголовок. Перейдём к CPP:
А теперь по-порядку. Сначала активируем WSA вызовом WSAStartup(). MAKEWORD задаёт версию библиотек, которые будут подключаться и ссылка на объект WSAData. Обычно всегда срабатывает успешно, только ели у вас не *unix, хе-хе. Дальше. структура SOCKADDR_IN помогает нам определить заранее порт и доступность нашего сервера. А теперь поясню:
SOCK_STREAM говорит об использовании TCP протокола, SOCK_DGRAM — UDP соответственно.
Вызываем обработчик входящих соединений handle() и в бесконечном цикле начинаем проверку.
Создаём сокет для входящего подключения, структуру для заполнения адреса и длину структуры.
Производим проверку на то, что не является ли наш созданный сокет подключённым по адресу такому-то от компьютера того-то. И если всё хорошо, то выводим сообщение и подключении с адреса нашего клиента и создаём клиента, который уже в дальнейшем будет сам всё обрабатывать.
Теперь же код клиента. Заголовок и имплементация вместе:
Тут тоже как видите, ничего сложного нет. главное это handle() функция, которую мы сразу запускаем в конструкторе. Функцию recv() предпочтительно использовать с TCP, а recvfrom() с UDP протоколом соответственно.
И на последок. В main.cpp Всё выглядит вот так:
А файлик, который вы так часто видели includes.h. Вот и он:
Итог: Мы научились создавать простой TCP сервер на C++, прочитав хорошую статью на хабре
Будет так же и UDP версия, но отличия очень малы и думаю скоро тоже будет здесь.
Вступление
Здесь я должен сделать отступление и немного рассказать о себе, что бы в дальнейшем было понятнее, почему именно такие шаги в разработке я предпринимал.
На данный момент я занимаю должность Технического Артиста в одной игровой студии, мой опыт программирования на C# строился только на написании скриптов и утилит для Unity и в довесок к этому создание плагинов для низкоуровневой работы с андроид девайсами. За пределы этого мирка я ещё не выбирался и тут подвернулась такая возможность.
Часть 1. Прототипирование рамы
Решив, что из себя будет представлять данный сервис, я принялся искать варианты для реализации. Проще всего было бы найти какой то готовое решение, на которое, как сову на глобус, можно натянуть наши механики и выложить всё это дело на общественное порицание.
Но это же не интересно, никакого челенджа и смысла в этом я не видел, а посему начал изучать веб технологии и методы взаимодействия с ними.
Сделав первую попытку с ASP я его сразу же отмёл, на мой взгляд это было слишком тяжёлым решением для нашего сервиса. Мы не будем использовать и трети возможностей этой платформы, поэтому я продолжил поиски. Выбор встал между TCP и Http клиент-сервером. Здесь же, на Хабре, я наткнулся на статью про многопоточный сервер, собрав и протестировав который, я решил остановиться именно на взаимодействии с TCP подключениями, почему то я посчитал, что http не позволит создать мне кроссплатформенное решение.
Первая версия сервера включала в себя обработку подключений, отдавала статическое содержимое веб-страниц и включала в себя базу данных пользователей. И для начала я решил строить функционал для работы с сайтом, что бы в последствии прикрутить сюда и обработку приложения на андроиде и ios.
Основной поток, в бесконечном цикле принимающий клиентов:
Сам обработчик клиентов:
И первая база данных построенная на local SQL:
Как можно заметить, эта версия мало отличается от той, что была в статье. По сути здесь только добавилась подгрузка страниц из папки на компьютере и база данных (которая кстати в данной версии не заработала, из-за неверной архитектуры подключения).
Глава 2. Прикручивание колёс
Протестировав работу сервера, я пришёл к выводу, что это будет отличным решением(спойлер: нет), для нашего сервиса, поэтому проект начал обрастать логикой.
Шаг за шагом начали появляться новые модули и функционал сервера разрастался. Сервер обзавёлся тестовым доменом и ssl шифрованием соединения.
Обновлённый вариант сервера, включающий в себя использование сертификата.
А так же новый обработчик клиента с авторизацией по ssl:
Но так как сервер работает исключительно на TCP подключении, то необходимо создать модуль, который мог распознавать контекст запроса. Я решил что здесь подойдёт парсер который будет разбивать запрос от клиента на отдельные части, с которыми я смогу взаимодействовать, что бы отдавать клиенту нужные ответы.
Суть его заключается в том, что бы при помощи регулярных выражений разбить запрос на части. Получаем сообщение от клиента, выделяем первую строку, в которой содержится метод и url запроса. Затем читаем заголовки, которые загоняем в массив вида ИмяЗаголовка=Содержимое, а так же находим, если имеется, сопроводительный контент (например querystring) который так же загоняем в аналогичный массив. К тому же, парсер выясняет, авторизован ли текущий клиент и сохраняет в себе его данные. Все запросы от авторизованных клиентов содержат хэш авторизации, который хранится в куках, благодаря этому можно разделять дальнейшую логику работы для двух типов клиентов и отдавать им правильные ответы.
Ну и небольшая, приятная фича, которую стоило бы вынести в отдельный модуль, преобразование запросов вида «site.com/@UserName» в динамически генерируемые страницы пользователей. После обработки запроса в дело вступают следующие модули.
Глава 3. Установка руля, смазывание цепи
Как только парсер отработал, в дело вступает обработчик, отдающий дальнейшие указания серверу и разделяющий управление на две части.
По сути здесь всего одна проверка на авторизацию юзера, после чего начинается обработка запроса.
Если юзер не авторизован, то для него функционал базируется только на отображении профилей пользователя и окне регистрации\авторизации. Код для авторизованного пользователя выглядит примерно так же, поэтому не вижу смысла его дублировать.
Ну и конечно же, пользователь должен получать какое то содержимое страниц, поэтому для ответов существует следующий модуль, отвечающий за ответ на запрос ресурсов.
Но что бы показывать пользователю его профиль и профили других пользователей я решил использовать RazorEngine, вернее его часть. Он так же включает в себя обработку неверных запросов и выдачу соответствующего кода ошибки.
Ну и конечно же, для того, что бы работала проверка авторизованных пользователей, нужна авторизация. Модуль авторизации взаимодействует с базой данных. Полученные данные из форм на сайте парсятся из контекста, юзер сохраняется и получает взамен куки и доступ к сервису.
А так выглядит обработка базы данных:
И всё работает как часы, авторизация и регистрация работает, минимальный функционал доступа к сервису уже имеется и пришла пора писать приложение и обвязывать всё это дело основными функциями, ради которых всё и делается.
Глава 4. Выбрасывание велосипеда
Что бы сократить трудозатраты на написание двух приложений под две платформы, я решил сделать кроссплатформу на Xamarin.Forms. Опять же, благодаря тому, что она на C#. Сделав тестовое приложение, которое просто отсылает серверу данные, я столкнулся с одним интересным моментом. Для запроса от устройства я для интереса реализовал его на HttpClient и кинул на сервер HttpRequestMessage в котором содержатся данные из формы авторизации в формате json. Особо ничего не ожидая, открыл лог сервера и увидел там реквест с девайса со всеми данными. Лёгкий ступор, осознание всего, что было проделано за последние 3 недели томных вечером. Для проверки верности отправленных данных собрал тестовый сервер на HttpListner. Получив очередной запрос уже на нём, я за пару строк кода разобрал его на части, получил KeyValuePair данных из формы. Разбор запроса уменьшился до двух строк.
Начал тестировать дальше, ранее не упоминалось, но на прежнем сервере я ещё реализовывал чат построенный на вебсокетах. Он довольно неплохо работал, но сам принцип взаимодействия через Tcp был удручающим, слишком много лишнего приходилось плодить, что бы грамотно построить взаимодействие двух пользователей с ведением лога переписки. Это и парсинг запроса на предмет переключения соединения и сбор ответа по протоколу RFC 6455. Поэтому в тестовом сервере я решил создать простое вебсокет соединение. Чисто ради интереса.
И оно заработало. Сервер сам настраивал соединение, генерировал ответный ключ. Мне даже не пришлось отдельно настраивать регистрацию сервера по ssl, достаточно того, что в системе уже установлен сертификат на нужном порту.
На стороне девайса и на стороне сайта два клиента обменивались сообщениями, всё это логировалось. Никаких огромных парсеров, замедляющих работу сервера, ничего этого не требовалось. Время отклика сократилось с 200мс до 40-30мс. И я пришёл к единственному верному решению.
Выкинуть текущую реализацию сервера на Tcp и переписать всё под Http. Теперь же проект находится в стадии перепроектирования, но уже по совсем другим принципам взаимодействия. Работа устройств и сайта синхронизирована и отлажена и имеет общую концепцию, с тем лишь отличием, что для девайсов не нужно генерировать html страницы.
Клиент-серверный чат, используя сокеты Qt/C++
Предисловие
Статья ориентирована в основном на новичков. Целью ее написания является быстрое и максимально подробное описание сокетов, для начального понимания сети и сокетов. В свое время искал подобную, но нужны были подробные примеры. В стандартном примере fortune server/client, который идет с qt очень плохо показывают возможности сокетов.
Для понимания это будут Гуи-приложения:
В qt существуют классы QTcpSocket и QTcpServer для работы с сокетами. Используя сигналы и слоты, с ними можно работать в неблокирующем (асинхронном режиме). Это значит, если подключение к серверу занимает заметное количество времени, гуи не блокируется, а продолжает обрабатывать события, а когда произойдет подключение (либо ошибка), вызовется определенный слот (в текущем случае подключенный к сигналу connected()).
Клиент
Начнем с простого — не будем наследовать классы, а воспользуемся QTcpSocket:
Вообще при работе с сокетами нужно смотреть на данные как набор байтов, иначе могут быть проблемы с отображением информации лишь частично (пришло не полное сообщение, а следующее отображается с куском предыдущего). Чтобы избежать этих неприятностей будем использовать потоки данных (QDataStream) и предавать между сокетами блоки, в которых первые 2 байта это размер текущего блока, 3-й байт это команда клиента серверу (или ответ сервера), а остальное — данные в зависимости от команды. Стоит сказать, что протокол tcp гарантирует доставку всех пакетов, поэтому можно смело ждать полный размер блока, прежде чем его обрабатывать.
На этом с клиентом все — основные моменты описаны, остальное все довольно просто.
Сервер
В сервере будет все посложнее — отделим гуи (dialog), сервер (myserver) и клиент (myclient). Кто малознаком с сокетами может не понять какой клиент может быть в сервере? Так вот, при подключении сокета к серверу, на сервере, как привило, создается «клиентский» сокет, который добавляется в массив (напрашивается мысль использовать ассоциативный массив, но для простоты возьмем QList).
Унаследуем класс от QTcpServer, это нужно для переопределения виртуальной функции incomingConnection, в которой перехватывается входящее соединение (входной параметр — дескриптор сокета)
Для класса-клиента, пожалуй, стоит описать только интерфейс, тут все понятно по аналогии. MyClient не обязательно наследовать от QTcpSocket, можно сделать иначе:
Upd 2017:
Увы, но исходники уже утеряны навсегда
Upd 2018:
Как известно, все что когда либо было загружено в интернет остается там навсегда. Благодаря vladpower исходники снова доступны
Связь между C# и C#: REST, gRPC и все, что между
Оптимальный сценарий
Давайте рассмотрим, как мы хотели бы, чтобы наше клиент-серверное общение выглядело в реальном мире. Я представляю что-то вроде этого:
Я хотел бы иметь полную поддержку Intellisense. Когда я нажимаю server и . хочу, чтобы Visual Studio показывал все контроллеры. И когда я нажимаю CalculatorController и ., я хочу видеть все методы действия. Я также хочу высочайшую производительность, очень малую нагрузку на сеть и двунаправленную передачу данных. И мне нужна надежная система, которая отлично справляется с управлением версиями, чтобы можно было без проблем развертывать новые версии клиентов и новые версии серверов.
Обратите внимание, что я говорю здесь об API без сохранения состояния. Это эквивалентно проекту C #, где есть только два типа классов:
Традиционный REST подход
REST API появился в начале 2000-х и покорил интернет. Сейчас это, безусловно, самый популярный способ создания веб-сервисов.
REST определяет фиксированный набор операций GET, POST, PUT и DELETE для запросов от клиента к серверу. На каждый запрос получаем ответ, содержащий полезную нагрузку(обычно JSON). Запросы включают параметры в сам запрос или как полезную нагрузку (обычно JSON), когда это POST или PUT запрос.
Существует стандарт RESTful API, который определяет следующие правила (который вам на самом деле не нужен):
REST чертовски удобен, но не подходит к оптимальному сценарию. Итак, давайте посмотрим, сможем ли мы сделать это лучше.
ReFit
ReFit не является альтернативой REST. Вместо этого он построен поверх REST и позволяет нам вызывать конечные точки сервера (endpoints ), как если бы они были простым методом. Это достигается путем разделения интерфейса между клиентом и сервером. На стороне сервера ваш контроллер будет реализовывать интерфейс:
Затем на стороне клиента вам нужно будет включить тот же интерфейс и использовать следующий код:
Это так просто. Нет необходимости запускать сложную автоматизацию или использовать сторонние инструменты, кроме пары пакетов NuGet.
Это становится намного ближе к оптимальному сценарию. Теперь у нас есть IntelliSense и надежный контракт между клиентом и сервером. Но есть и другой вариант, который даже лучше в некоторых отношениях.
Swagger
Давайте рассмотрим последствия этого. С помощью JSON-файла, подобного приведенному выше, вы потенциально можете создать клиент C# с полным IntelliSense. В конце концов, вы знаете все пути, операции, какие параметры они ожидают, какие типы параметров, каковы ответы.
Есть несколько инструментов, которые делают именно это. На стороне сервера вы можете использовать Swashbuckle.AspNetCore для добавления Swagger в ваш ASP.NET и создания указанных файлов JSON. Для стороны клиента, вы можете использовать swagger-codegen и AutoRest чтобы обработать эти файлы в формате JSON и сгенерировать клиента. Давайте посмотрим пример того, как это сделать:
Добавление Swagger на ваш сервер ASP.NET
Начните с добавления пакета NuGet Swashbuckle.AspNetCore. В ConfigureServices, зарегистрируйте генератор Swagger:
В файле Startup.cs в методе Configure добавьте:
Наконец, методы действия(actions) внутри контроллера должны быть помечены [HttpXXX] и [FromXXX] атрибутами:
Это так просто для серверной части. При запуске проекта будет сгенерирован файл swagger.json, который вы можете использовать для генерации клиента.
Генерация клиента из Swagger с помощью AutoRest
Это создаст папку GeneratedClient с сгенерированными файлами C#. Обратите внимание, что пространство имен и имя клиента переопределены. Добавьте эту папку в ваш клиентский проект в Visual Studio как показано ниже.
Вам нужно будет установить Microsoft.Rest.ClientRuntime пакет NuGet, потому что сгенерированный код зависит от него. После установки вы можете использовать API, как обычный C# класс:
Есть некоторые тонкости, о которых вы можете прочитать в документации AutoRest. И вам нужно будет автоматизировать этот процесс, поэтому я предлагаю прочитать руководство Патрика Свенссона для получения некоторых полезных советов, а также эту статью Питера Яусовца.
Моя проблема с Swagger заключается в том, что файл JSON создается во время выполнения, поэтому это немного затрудняет автоматизацию процесса CI/CD.
Традиционный REST vs Swagger vs ReFit
Вот несколько моментов, которые следует учитывать при выборе.
gRPC (gRPC — удаленный вызов процедур) — это система удаленного вызова процедур с открытым исходным кодом, разработанная Google. Это немного похоже на REST в том смысле, что он обеспечивает способ отправки запросов от клиента к серверу. Но это во многом отличается, вот сходства и различия:
Это не руководство, а скорее общее представление о том, чего ожидать. Вот как будет выглядеть пример контроллера в gRPC:
Вам нужно добавить следующее в Configure в файле Startup.cs:
API описан в .proto файле, который является частью проекта:
Клиент генерируется из .proto файлов. Сам код очень прост:
gRPC vs REST
GRPC звучит как приятная сделка. Это быстрее и проще под капотом. Так должны ли мы все перейти с REST на gRPC? Ответ в том, что это зависит. Вот некоторые соображения:
По моим впечатлениям, работа с gRPC и ASP.NET все еще невелика. Вам будет лучше со зрелой поддержкой REST. Что касается контрактного общения, это хорошо, за исключением того, что у вас есть похожие альтернативы в REST, о которых мы уже говорили: Swagger и ReFit.
Самым большим преимуществом является производительность. В большинстве случаев, согласно этим критериям, gRPC намного быстрее. Особенно для больших полезных нагрузок, для которых сериализация Protobuf действительно имеет значение. Это означает, что это огромное преимущество для сервера с высокой нагрузкой.
Переход от REST к gRPC в большом приложении ASP.NET будет трудным. Однако если у вас есть архитектура на основе микросервисов, то этот переход станет намного легче выполнять постепенно.
Другие способы общения
Есть несколько других способов общения, о которых я вообще не упоминал, но стоит знать, что они существуют: