Как написать сервер
Перейти к содержимому

Как написать сервер

Создание многопоточного сервера на C#

Данная статья предназначена для таких же новичков как и я. Вся информация в этой статье основывается на моем опыте создания одного единственного веб-сервера, который был создан в рамкам учебного проекта на 3 курсе по специальности 09.02.07 СПО.

Веб-сервер

Прежде чем писать свой веб сервер, нам нужно понять что это и как он работает.

Веб-сервер — это сервер, который основывается на работе протоколов TCP/IP и HTTP для взаимодействия с клиентом. Основной задачей таких серверов это принимать входящие запросы на основе протокола HTTP.

Под веб-сервером подразумевают две вещи:

В данной статье мы будем рассматривать веб-сервер, как программное обеспечение.

Веб-сервер работает благодаря такой архитектуре как клиент — сервер

Рисунок 1 - Блок-схема архитектуры клиент-сервер

Рисунок 1 — Блок-схема архитектуры клиент-сервер

Чтобы было понятнее, разобьем работу архитектуры по пунктам:

Формирование запроса клиентом

Отправка запроса на сервер

Получение запроса на сервере

Обработка запроса и формирование ответа

Отправка ответа клиенту

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

TCP/IP (Transmission Control Protocol/Internet Protocol) — два основных протокола на которых строится весь современный интернет. TCP предназначен для передачи данных между участниками сети, а IP является межсетевым протоколом, который используется для обозначения участников сети.

HTTP(Hyper Text Transfer Protocol) — протокол прикладного уровня передачи данных. Основной его задачей является передача файлов с расширением HTML, но он так же может передавать и другие файлы.

Компьютер, который является сервером будет прослушивать приходящие по сети подключения по паре ip:port, к примеру: 127.0.0.1:80, где 127.0.0.1 — ip-адрес; 80 — порт, используется для протокола HTTP, так же можно использовать порт 81.

Реализация веб сервера на C#

Писать наш веб-сервер мы будем на C# .Net Core. Желательно, чтобы вы знали базу языка.

Вот мы и перешли от слов к практике, но перед этим, нам нужно определиться, с помощью чего мы будем писать наш веб-сервер ? Нашему вниманию представляются несколько классов которые могут нам в этом помочь:

Socket — представляет реализацию сокетов Беркли на C#, эмпирическим путем было выяснено что использование данного варианта приносит более эффективный результат.

TcpListener — прослушивает входящие TCP соединения по паре ip:port. Может от моей криворукости, больше чем в этом уверен, или от чего-то другого, у меня получалось так, что TcpListener не совсем подходит для этой задачи, так как при отправке пакетов клиенту, некоторые файлы просто не приходили и каждый раз количество файлов разнилось.

В данной статье мы рассмотрим только вариант на основе класса Socket , кому интересно знать, как реализовать веб-сервер на TcpListener , то вот ссылка на статью другого автора.

Для начала мы должны создать 2 класса(они должны располагаться в двух новых файлах):

Server — этот класс будет обозначать наш сервер и он будет принимать входящие подключения

Client — этот класс будет обозначать нашего клиента, в этом классе будет проходить вся обработка запроса

Начнем заполнять класс Server . Для начала мы должны добавить в наш класс библиотеки, которые нам понадобятся:

Затем в классе мы должны создать переменные которыми будем оперировать:

Теперь создадим конструктор для нашего класса. Так как Socket работает по ip:port, то и наш конструктор будет принимать первым аргументом ip, а вторым port:

Ip мы будем задавать в виде строки и преобразовать его к классу IPEndPoint через класс IPAddress . Порт самое простое, просто обычное число типа int . Думаю самое непонятное для вас сейчас, это конструктор класса Socket :

AddressFamily – перечисление, которое обозначает то, с какой версией ip адресов мы будем работать. InterNetwork говорит о том что мы используем IPv4.

SocketType – перечисление, обозначает тип сокета и какое подключение будет устанавливаться. В нашем случаем мы будем работать с типом подключения Stream .

ProtocolType – перечисление, обозначает то, какой тип подключений мы будем принимать. Tcp , означает то что мы будем работать с протоколом TCP.

После конструктора нам стоит создать две функции, первая будет инициализировать работу нашего сервера, а вторая останавливать его соответственно:

Условие внутри функции проверяет, выключен ли сервер. Если он выключен, то мы можем запустить наш сервер. Это нужно для того, чтобы не было конфликта у сокета. Если мы попытаемся запустить второй сокет с тем же ip и port то вылезет ошибка.

После мы заполняем наше условие, если сервер выключен то:

Функция Bind класса Socket означает, что слушатель будет работать по определенному ip:port, который мы передаем в эту функцию.

Функция Listen начинает прослушивание, как аргумент мы передаем в нее переменную типа int , который означает возможное кол-во клиентов в очереди на подключение к сокету.

Теперь приступим к реализации многопоточности. Делать мы ее будем на основе такого класса как ThreadPool . Нет, конечно можно было сделать проще:

Но такой способ не эффективен, так как он будет просто создавать новые потоки для обработки входящего соединения, тем самым тормозя работу сервера, ведь как никак потоки у нашего процессора не безграничны. Поэтому мы берем и вставляем этот кусок кода в наше условие(после Active = true ):

ThreadPool.QueueUserWorkItem(WaitCallback, object) — добавляет в очередь функции, которые должны выполниться

WaitCallback(ClientThread) — принимает функцию и возвращает ответ о ее выполнении

Listener.AcceptTcpClient() — аргумент, который будет передаваться в функцию

Функция будет циклически прослушивать входящие соединения. Listener.Accept() будет временно останавливать цикл, до тех пор, пока не придет запрос на подключение.

Теперь перейдем к нашей функции остановки сервера:

В ней мы пишем условие, обратное тому которое было в Start , т.е тут мы должны проверять включен ли сервер.

Функцией Close класса Socket мы прекращаем прослушивание. Затем мы меняем значение переменной Active на false .

Думаю по функции Start вы заметили, что там присутствовала такая функция как ThreadClient , пришло время создать и ее. Она будет отвечать за создание нового клиента, который подключается к нашему серверу:

Так как делегат WaitCallback требует, чтобы аргументом являлся простой тип object , то функция соответственно будет тоже принимать тип object , который мы будем не явным образов преобразовывать в класс Socket .

Пришло время и для описания класса Client . Для начала подключим нужные нам библиотеки в файле:

Но прежде чем описывать наш класс Client , давайте создадим структуру, с помощью которой мы будем парсить наши HTTP заголовки:

Данная структура будет хранить значения наших HTTP заголовков:

Method — хранит метод, с помощью которого делается запрос

RealPath – хранит полный путь до файла на нашем сервере(пример: C:\Users\Public\Desktop\Server\www\index.html)

File — хранит не полный путь до файла(пример: \www\index.html)

Теперь давайте создадим саму функцию, которая будет парсить заголовки:

Она будет возвращать саму структуру, тогда объявление структуры будет выглядеть так:

Теперь опишем тело функции:

Объяснять принцип работы регулярных выражений я не буду, поэтому в конце статьи есть ссылка на документацию.

При присвоении значения переменной RealPath у объекта структуры result , я написал: AppDomain.CurrentDomain.BaseDirectory — это означает, что мы берем путь до нашего exe файла, пример: C:\Users\Public\Desktop\Server, а затем мы подставляем неполный путь до нашего файла: File , и тогда наш путь будет выглядеть так: C:\Users\Public\Desktop\Server\ + \www\index.html = C:\Users\Public\Desktop\Server\www\index.html . Т.е, файлы сайта будут находиться относительно нашего сервера.

Теперь давайте напишем функцию, которая будет возвращать нам расширения нашего файла, назовем ее FileExtention :

Опять же, делаем это с помощью регулярных выражений.

Эта структура была сделана для удобства, так как когда мы будет парсить большое кол-во заголовков, то лучше если они будут храниться в одном месте.

Создадим в классе Client переменные:

У конструктора нашего класс будет всего 1 аргумент, который будет принимать Socket :

После в конструкторе, мы должны присвоить нашей переменной client наш аргумент и начать принимать данные от клиента:

Код представленный выше описывает то, как сервер принимает запросы от клиента:

data — массив который принимает байты

request — запрос в виде строки

client.Receive(data) — считывает приходящие байты и записывает их в массив.

После того как мы запишем принятые данные от клиента в массив байтов data , мы должны привести это в понятный вид, для этого мы воспользуемся классом Encoding , с помощью которого переведем байты в символы:

Теперь настало время проверок и парсинга наших заголовков.

Первое условие проверяет, пришел ли какой-то запрос вообще ? Если нет, то мы отсоединяем клиента и выходим из функции:

Если у нас все же что-то пришло, то время воспользоваться структурой и распарсить принятое сообщение и выводим сообщение о подключении в консоль:

Дальше мы проверяем нашу ссылку на наличие “..”, если это значение существует, т.е больше -1, то мы выводим сообщение об ошибке:

Ну и наконец последняя проверка в этой функции, если файл по указанному пути Headers.RealPath существует, то мы начинаем работать с этим файлом, иначе выводи ошибку:

Перед описанием основной функции GetSheet , которая будет возвращать пользователю ответ, мы создадим пару функций.

Первая функция SendError , она будет возвращать код ошибки пользователю:

html — представляет разметку нашей страницы

headers — представляет заголовки

data — массив байтов

client.Send(data, data.Length, SocketFlags.None); — отправляет данные клиенту

client.Close(); — закрывает нынешнее соединение

Теперь создадим функцию, которая будет возвращать тип контента, так как в данной статье представлена простая версия сервера, то мы ограничимся типами: text и image. Тип контента мы выводим для того, чтобы отправленный нами файл мог опознаться, записываем мы это значение в специальном заголовке Content-Type (пример: Content-Type: text/html ):

Данная функция принимает нашу структуру HTTPHeaders . Вначале мы воспользуемся функцией которая вернет нам расширение файла, а после начнем сверять его в условной конструкции switch . Если не один из вариантов перечисленных среди case не обнаружится.то мы вернем: applacation/unknown — это означает что файл не был опознан.

Теперь опишем нашу последнюю функцию GetSheet и можно будет тестировать наш сервер:

Данная функция как аргумент принимает нашу структуру HTTPHeaders . Сначала стоит обернуть функцию в блок обработки ошибок try catch , так как могут быть какие-либо ошибки:

Теперь опишем тело оператора try :

После того как мы переведем наши заголовки в массив байтов мы отправим их клиенту с помощью метода Send() класса Socket , который принимает следующие параметры:

byte[] — массив байтов

byte[].Length — длинна передаваемого массива

SocketFlags — перечисление, которое представляет поведение сокета при отправке и получении пакетов. Значение None обозначает что флаги не используются

И в самом конце нашего оператора мы передаем контент, который запрашивал клиент. Так как мы делали это с помощью FileStream , то сначала нам стоит: читать данные, записать их в массив байтов и отправить по сети.

В этот раз мы поставили SocketFlags.Partial . Это означает что в данном случаем, отправляется часть сообщения, так как не все байты файла могут поместятся в массив размером 1024. Но так же может и работать с SocketFlags.None

Так как у нас многопоточный сервер, который работает на ThreadPool, то для начала в файле который содержит функцию Main мы подключим библиотеку: System.Threading , а затем укажем минимальное кол-во потоков, которое он может использовать:

Первый параметр указывает на минимальное кол-во работающих потоков, а второй на минимальное кол-во асинхронно работающих потоков. Минимальное значение стоит всегда указывать 2, так как если указать 1, то основной поток будет блокироваться для обработки запроса.

Теперь зададим максимальные значения для нашего пула:

После чего мы просто инициализируем наш класс Server в функции и запускаем его:

Давайте создадим в папке, где располагается наш exe(пример пути. /project/bin/Debug/netx.x/ — где project имя вашего проекта) файл простой html файл:

После этого прописываем в адресной строке: http://127.0.0.1/index.html и проверяем результат. Нам должно вывести надпись Hello Server!, а так же должно вывести в консоли данные о нынешнем подключении.

Теперь Вы имеете представление о том, как с помощью сетевых сокетов на языке C# реализовать простой, многопоточный сервер. Для большего понятия как работают разные классы рекомендую почитать документацию:

Благодарю за то что уделили моей статье внимание, надеюсь что если я где-то оказался не прав вы укажете мне на это в комментариях и поможете стать лучше.

Ссылка на сервер на GitHub, в данной версии сервера реализована поддержка php.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *