ГЛАВНАЯ     АРХИВ     НАПИСАТЬ АДМИНУ     ПОДПИСАТЬСЯ НА RSS     ВОЙТИ      

Поиск

Категории

Облако тегов

  << Предыдущий пост       Следующий пост >>  
От: peerAt
30. мая 2011 23:47

Оригинал статьи здесь: Многопоточный сервер на C# за 15 минут

Автор (на хабре): ertaquo

C# довольно простой и гибкий язык. Вместе с .NET поставляется довольно много уже готовых классов, что делает его еще проще. Настолько, что вполне можно написать простой многопоточный HTTP-сервер для отдачи статического содержимого всего за 15 минут. Можно было бы использовать уже готовый класс HttpListener и управиться еще быстрее, но цель этой статьи — показать, как вообще можно сделать нечто подобное в C#.

Для начала создадим новый консольный проект:

using System;
using System.Collections.Generic;
using System.Text;

namespace HTTPServer
{
    class Server
    {
        static void Main(string[] args)
        {

        }
    }
}

В .NET можно очень легко создать TCP-сервер при помощи класса TcpListener, чем мы и воспользуемся:

    class Server
    {
        TcpListener Listener; /* Объект, принимающий TCP-клиентов */

        /* Запуск сервера */
        public Server(int Port)
        {
            /* Создаем "слушателя" для указанного порта */
            Listener = new TcpListener(IPAddress.Any, Port);
            Listener.Start(); /* Запускаем его */

            /* В бесконечном цикле */
            while (true)
            {
                /* Принимаем новых клиентов */
                Listener.AcceptTcpClient();
            }
        }

        /* Остановка сервера */
        ~Server()
        {
            /* Если "слушатель" был создан */
            if (Listener != null)
            {
                /* Остановим его */
                Listener.Stop();
            }
        }

        static void Main(string[] args)
        {
            /* Создадим новый сервер на порту 80 */
            new Server(80);
        }
    }

Если сейчас запустить приложение, то уже можно будет подключиться к порту 80 и… все. Соединение будет лишь простаивать впустую, так как отсутствует его обработчик и оно не закрывается со стороны сервера. Напишем самый простой обработчик:

    /* Класс-обработчик клиента */
    class Client
    {
        /* Конструктор класса. Ему нужно передавать принятого клиента от TcpListener */
        public Client(TcpClient Client)
        {
            /* Код простой HTML-странички */
            string Html = "<html><body><h1>It works!</h1></body></html>";


            /* Необходимые заголовки: ответ сервера, тип и длина содержимого. После двух пустых строк - само содержимое */
            string Str = "HTTP/1.1 200 OK\nContent-type: text/html\nContent-Length:" + Html.Length.ToString() + "\n\n" + Html;


            /* Приведем строку к виду массива байт */
            byte[] Buffer = Encoding.ASCII.GetBytes(Str);


            /* Отправим его клиенту */
            Client.GetStream().Write(Buffer, 0, Buffer.Length);


            /* Закроем соединение */
            Client.Close();
        }
    }

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

      /* Принимаем новых клиентов и передаем их на обработку новому экземпляру класса Client */
      new Client(Listener.AcceptTcpClient());

Теперь можно запустить программу, открыть в браузере адрес 127.0.0.1 и увидеть большими буквами «It works!»
Перед тем, как приступить к написанию парсера HTTP-запроса, сделаем наш сервер многопоточным. Для этого есть два способа: создавать вручную новый поток для каждого клиента или воспользоваться пулом потоков. У обоих способов есть свои преимущества и недостатки. Если создавать по потоку на каждого клиента, то сервер может не выдержать высокой нагрузки, но можно работать с практически неограниченным количеством клиентов одновременно. Если использовать пул потоков, то количество одновременно работающих потоков будет ограничено, но нельзя будет создать новый поток, пока не завершатся старые. Какой из способов вам больше подойдет, я не знаю, поэтому приведу пример обоих.
Напишем простую процедуру потока, которая будет лишь создавать новый экземпляр класса Client:

        static void ClientThread(Object StateInfo)
        {
            new Client((TcpClient)StateInfo);
        }

Для использования первого способа нужно заменить только содержимое нашего бесконечного цикла приема клиентов:

                /* Принимаем нового клиента */
                TcpClient Client = Listener.AcceptTcpClient();


                /* Создаем поток */
                Thread Thread = new Thread(new ParameterizedThreadStart(ClientThread));


                /* И запускаем этот поток, передавая ему принятого клиента */
                Thread.Start(Client);

Для второго способа нужно проделать то же самое:

       /* Принимаем новых клиентов. После того, как клиент был принят, он передается в новый поток (ClientThread) */
       /* с использованием пула потоков. */
       ThreadPool.QueueUserWorkItem(new WaitCallback(ClientThread), Listener.AcceptTcpClient());

Плюс надо установить максимальное и минимальное количество одновременно работающих потоков. Сделаем это в процедуре Main:

            /* Определим нужное максимальное количество потоков */
            /* Пусть будет по 4 на каждый процессор */
            int MaxThreadsCount = Environment.ProcessorCount * 4;


            /* Установим максимальное количество рабочих потоков */
            ThreadPool.SetMaxThreads(MaxThreadsCount, MaxThreadsCount);


            /* Установим минимальное количество рабочих потоков */
            ThreadPool.SetMinThreads(2, 2);

Максимальное количество потоков должно быть не меньше двух, так как в это число входит основной поток. Если установить единицу, то обработка клиента будет возможна лишь тогда, когда основной поток приостановил работу (например, ожидает нового клиента или была вызвана процедура Sleep).
Итак, теперь переключимся целиком на класс Client начнем обрабатывать HTTP-запрос. Получим текст запроса от клиента:

            /* Объявим строку, в которой будет хранится запрос клиента */
            string Request = "";


            /* Буфер для хранения принятых от клиента данных */
            byte[] Buffer = new byte[1024];


            /* Переменная для хранения количества байт, принятых от клиента */
            int Count;


            /* Читаем из потока клиента до тех пор, пока от него поступают данные */
            while ((Count = Client.GetStream().Read(Buffer, 0, Buffer.Length)) > 0)
            {
                /* Преобразуем эти данные в строку и добавим ее к переменной Request */
                Request += Encoding.ASCII.GetString(Buffer, 0, Count);


                /* Запрос должен обрываться последовательностью \r\n\r\n */
                /* Либо обрываем прием данных сами, если длина строки Request превышает 4 килобайта */
                /* Нам не нужно получать данные из POST-запроса (и т. п.), а обычный запрос */
                /* по идее не должен быть больше 4 килобайт */
                if (Request.IndexOf("\r\n\r\n") >= 0 || Request.Length > 4096)
                {
                    break;
                }
            }

Далее осуществляем парсинг полученных данных:

            /* Парсим строку запроса с использованием регулярных выражений */
            /* При этом отсекаем все переменные GET-запроса */
            Match ReqMatch = Regex.Match(Request, @"^\w+\s+([^\s\?]+)[^\s]*\s+HTTP/.*|");

            /* Если запрос не удался */
            if (ReqMatch == Match.Empty)
            {
                /* Передаем клиенту ошибку 400 - неверный запрос */
                SendError(Client, 400);
                return;
            }

            /* Получаем строку запроса */
            string RequestUri = ReqMatch.Groups[1].Value;

            /* Приводим ее к изначальному виду, преобразуя экранированные символы */
            /* Например, "%20" -> " " */
            RequestUri = Uri.UnescapeDataString(RequestUri);

            /* Если в строке содержится двоеточие, передадим ошибку 400 */
            /* Это нужно для защиты от URL типа http://example.com/../../file.txt */
            if (RequestUri.IndexOf("..") >= 0)
            {
                SendError(Client, 400);
                return;
            }

            /* Если строка запроса оканчивается на "/", то добавим к ней index.html */
            if (RequestUri.EndsWith("/"))
            {
                RequestUri += "index.html";
            }

Ну и наконец осуществим работу с файлами: проверим, есть ли нужный файл, определим его тип содержимого и передадим его клиенту.

            string FilePath = "www/" + RequestUri;

            /* Если в папке www не существует данного файла, посылаем ошибку 404 */
            if (!File.Exists(FilePath))
            {
                SendError(Client, 404);
                return;
            }

            /* Получаем расширение файла из строки запроса */
            string Extension = RequestUri.Substring(RequestUri.LastIndexOf('.'));

            /* Тип содержимого */
            string ContentType = "";

            /* Пытаемся определить тип содержимого по расширению файла */
            switch (Extension)
            {
                case ".htm":
                case ".html":
                    ContentType = "text/html";
                    break;
                case ".css":
                    ContentType = "text/stylesheet";
                    break;
                case ".js":
                    ContentType = "text/javascript";
                    break;
                case ".jpg":
                    ContentType = "image/jpeg";
                    break;
                case ".jpeg":
                case ".png":
                case ".gif":
                    ContentType = "image/" + Extension.Substring(1);
                    break;
                default:
                    if (Extension.Length > 1)
                    {
                        ContentType = "application/" + Extension.Substring(1);
                    }
                    else
                    {
                        ContentType = "application/unknown";
                    }
                    break;
            }

            /* Открываем файл, страхуясь на случай ошибки */
            FileStream FS;
            try
            {
                FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            }
            catch (Exception)
            {
                /* Если случилась ошибка, посылаем клиенту ошибку 500 */
                SendError(Client, 500);
                return;
            }

            /* Посылаем заголовки */
            string Headers = "HTTP/1.1 200 OK\nContent-Type: " + ContentType + "\nContent-Length: " + FS.Length + "\n\n";
            byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);
            Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length);

            /* Пока не достигнут конец файла */
            while (FS.Position < FS.Length)
            {
                /* Читаем данные из файла */
                Count = FS.Read(Buffer, 0, Buffer.Length);


                /* И передаем их клиенту */
                Client.GetStream().Write(Buffer, 0, Count);
            }

            /* Закроем файл и соединение */
            FS.Close();
            Client.Close();

Также в коде упоминалась пока не описанная процедура SendError. Напишем и ее:

        /* Отправка страницы с ошибкой */
        private void SendError(TcpClient Client, int Code)
        {
            /* Получаем строку вида "200 OK" */
            /* HttpStatusCode хранит в себе все статус-коды HTTP/1.1 */
            string CodeStr = Code.ToString() + " " + ((HttpStatusCode)Code).ToString();


            /* Код простой HTML-странички */
            string Html = "<html><body><h1>" + CodeStr + "</h1></body></html>";


            /* Необходимые заголовки: ответ сервера, тип и длина содержимого. После двух пустых строк - само содержимое */
            string Str = "HTTP/1.1 " + CodeStr + "\nContent-type: text/html\nContent-Length:" + Html.Length.ToString() + "\n\n" + Html;


            /* Приведем строку к виду массива байт */
            byte[] Buffer = Encoding.ASCII.GetBytes(Str);


            /* Отправим его клиенту */
            Client.GetStream().Write(Buffer, 0, Buffer.Length);


            /* Закроем соединение */
            Client.Close();
        }

На этом написание простого HTTP-сервера окончено. Оно работает в несколько потоков, отдает статику, имеет простую защиту от плохих запросов и ругается на отсутствующие файлы. На все это можно навесить дополнительные примочки: возможность конфигурирования, обработку доменов, изменение адресов наподобие mod_rewrite, даже поддержку CGI. Но это будет уже совсем другая история :-)

Исходник (через ThreadPool)
Исходник (через Thread)
Архив с исходником (через ThreadPool, вариант через Thread закомментирован)
Архив с откомпилированной версией (через ThreadPool)

Похожие записи


Вопросы на собеседовании C#, Net, ASP.NET, SQL
Продолжая тему вопросов на собеседовании. Нашел еще одну подборку. Оригинал лежит здесь . Перенес, чтобы не затерялось. Есть вполне вменяемые ответы (хотя, на некоторые вопросы ответил бы по-другому). Ответы находятся после списка вопросов, я их не менял. 23. Что такое шаблон проектирования Model/View/Controller? Как и зачем его применяют? 2...

Способы заработка на сайте
На этом блоге было много постов про производство и раскрутку сайтов. Но пока не было про заработок на них. Пора исправить этот недочет. Здесь будут описаны по возможности не фантастичные, и не супер низко прибыльные способы. Также упор будет сделан именно на организацию процесса, а не технических деталях, которых в обилии в интернете. Несмотря на то, что как пр...

Silverlight, HTML5 и непрозрачная стратегия развития Microsoft
Оригинал статьи взят отсюда: Silverlight, HTML5 и непрозрачная стратегия развития Microsoft Автор: Peter Bright Переводчик: Mairon     По непонятным мне на данный момент причинам, похоже, что многие разработчики, присутствовавшие на недавней конференции PDC-2010 (Крупнейшая конференция Microsoft для разработчиков — Прим. переводчика), были сильн...

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




biuquote
  • Комментарий
  • Предпросмотр
Loading


  Сохранить комментарий