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

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

    Для начала создадим новый консольный проект:
    Copy Source | Copy HTML
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Text;
    4.  
    5. namespace HTTPServer
    6. {
    7.     class Server
    8.     {
    9.         static void Main(string[] args)
    10.         {
    11.  
    12.         }
    13.     }
    14. }
    15.  

    В .NET можно очень легко создать TCP-сервер при помощи класса TcpListener, чем мы и воспользуемся:
    Copy Source | Copy HTML
    1. class Server
    2. {
    3.     TcpListener Listener; // Объект, принимающий TCP-клиентов
    4.  
    5.     // Запуск сервера
    6.     public Server(int Port)
    7.     {
    8.         // Создаем "слушателя" для указанного порта
    9.         Listener = new TcpListener(IPAddress.Any, Port);
    10.         Listener.Start(); // Запускаем его
    11.  
    12.         // В бесконечном цикле
    13.         while (true)
    14.         {
    15.             // Принимаем новых клиентов
    16.             Listener.AcceptTcpClient();
    17.         }
    18.     }
    19.  
    20.     // Остановка сервера
    21.     ~Server()
    22.     {
    23.         // Если "слушатель" был создан
    24.         if (Listener != null)
    25.         {
    26.             // Остановим его
    27.             Listener.Stop();
    28.         }
    29.     }
    30.  
    31.     static void Main(string[] args)
    32.     {
    33.         // Создадим новый сервер на порту 80
    34.         new Server(80);
    35.     }
    36. }

    Если сейчас запустить приложение, то уже можно будет подключиться к порту 80 и… все. Соединение будет лишь простаивать впустую, так как отсутствует его обработчик и оно не закрывается со стороны сервера.
    Напишем самый простой обработчик:
    Copy Source | Copy HTML
    1. // Класс-обработчик клиента
    2. class Client
    3. {
    4.     // Конструктор класса. Ему нужно передавать принятого клиента от TcpListener
    5.     public Client(TcpClient Client)
    6.     {
    7.         // Код простой HTML-странички
    8.         string Html = "<html><body><h1>It works!</h1></body></html>";
    9.         // Необходимые заголовки: ответ сервера, тип и длина содержимого. После двух пустых строк - само содержимое
    10.         string Str = "HTTP/1.1 200 OK\nContent-type: text/html\nContent-Length:" + Html.Length.ToString() + "\n\n" + Html;
    11.         // Приведем строку к виду массива байт
    12.         byte[] Buffer = Encoding.ASCII.GetBytes(Str);
    13.         // Отправим его клиенту
    14.         Client.GetStream().Write(Buffer,  0, Buffer.Length);
    15.         // Закроем соединение
    16.         Client.Close();
    17.     }
    18. }

    Чтобы передать ему клиента, нужно изменить одну строчку в классе Server:
    Copy Source | Copy HTML
    1. // Принимаем новых клиентов и передаем их на обработку новому экземпляру класса Client
    2. new Client(Listener.AcceptTcpClient());

    Теперь можно запустить программу, открыть в браузере адрес 127.0.0.1 и увидеть большими буквами «It works!»
    Перед тем, как приступить к написанию парсера HTTP-запроса, сделаем наш сервер многопоточным. Для этого есть два способа: создавать вручную новый поток для каждого клиента или воспользоваться пулом потоков. У обоих способов есть свои преимущества и недостатки. Если создавать по потоку на каждого клиента, то сервер может не выдержать высокой нагрузки, но можно работать с практически неограниченным количеством клиентов одновременно. Если использовать пул потоков, то количество одновременно работающих потоков будет ограничено, но нельзя будет создать новый поток, пока не завершатся старые. Какой из способов вам больше подойдет, я не знаю, поэтому приведу пример обоих.
    Напишем простую процедуру потока, которая будет лишь создавать новый экземпляр класса Client:
    Copy Source | Copy HTML
    1. static void ClientThread(Object StateInfo)
    2. {
    3.     new Client((TcpClient)StateInfo);
    4. }

    Для использования первого способа нужно заменить только содержимое нашего бесконечного цикла приема клиентов:
    Copy Source | Copy HTML
    1. // Принимаем нового клиента
    2. TcpClient Client = Listener.AcceptTcpClient();
    3. // Создаем поток
    4. Thread Thread = new Thread(new ParameterizedThreadStart(ClientThread));
    5. // И запускаем этот поток, передавая ему принятого клиента
    6. Thread.Start(Client);

    Для второго способа нужно проделать то же самое:
    Copy Source | Copy HTML
    1. // Принимаем новых клиентов. После того, как клиент был принят, он передается в новый поток (ClientThread)
    2. // с использованием пула потоков.
    3. ThreadPool.QueueUserWorkItem(new WaitCallback(ClientThread), Listener.AcceptTcpClient());

    Плюс надо установить максимальное и минимальное количество одновременно работающих потоков. Сделаем это в процедуре Main:
    Copy Source | Copy HTML
    1. // Определим нужное максимальное количество потоков
    2. // Пусть будет по 4 на каждый процессор
    3. int MaxThreadsCount = Environment.ProcessorCount * 4;
    4. // Установим максимальное количество рабочих потоков
    5. ThreadPool.SetMaxThreads(MaxThreadsCount, MaxThreadsCount);
    6. // Установим минимальное количество рабочих потоков
    7. ThreadPool.SetMinThreads(2, 2);

    Максимальное количество потоков должно быть не меньше двух, так как в это число входит основной поток. Если установить единицу, то обработка клиента будет возможна лишь тогда, когда основной поток приостановил работу (например, ожидает нового клиента или была вызвана процедура Sleep).
    Итак, теперь переключимся целиком на класс Client начнем обрабатывать HTTP-запрос. Получим текст запроса от клиента:
    Copy Source | Copy HTML
    1. // Объявим строку, в которой будет хранится запрос клиента
    2. string Request = "";
    3. // Буфер для хранения принятых от клиента данных
    4. byte[] Buffer = new byte[1024];
    5. // Переменная для хранения количества байт, принятых от клиента
    6. int Count;
    7. // Читаем из потока клиента до тех пор, пока от него поступают данные
    8. while ((Count = Client.GetStream().Read(Buffer,  0, Buffer.Length)) >  0)
    9. {
    10.     // Преобразуем эти данные в строку и добавим ее к переменной Request
    11.     Request += Encoding.ASCII.GetString(Buffer,  0, Count);
    12.     // Запрос должен обрываться последовательностью \r\n\r\n
    13.     // Либо обрываем прием данных сами, если длина строки Request превышает 4 килобайта
    14.     // Нам не нужно получать данные из POST-запроса (и т. п.), а обычный запрос
    15.     // по идее не должен быть больше 4 килобайт
    16.     if (Request.IndexOf("\r\n\r\n") >=  0 || Request.Length > 4096)
    17.     {
    18.         break;
    19.     }
    20. }

    Далее осуществляем парсинг полученных данных:
    Copy Source | Copy HTML
    1. // Парсим строку запроса с использованием регулярных выражений
    2. // При этом отсекаем все переменные GET-запроса
    3. Match ReqMatch = Regex.Match(Request, @"^\w+\s+([^\s\?]+)[^\s]*\s+HTTP/.*|");
    4.  
    5. // Если запрос не удался
    6. if (ReqMatch == Match.Empty)
    7. {
    8.     // Передаем клиенту ошибку 400 - неверный запрос
    9.     SendError(Client, 400);
    10.     return;
    11. }
    12.  
    13. // Получаем строку запроса
    14. string RequestUri = ReqMatch.Groups[1].Value;
    15.  
    16. // Приводим ее к изначальному виду, преобразуя экранированные символы
    17. // Например, "%20" -> " "
    18. RequestUri = Uri.UnescapeDataString(RequestUri);
    19.  
    20. // Если в строке содержится двоеточие, передадим ошибку 400
    21. // Это нужно для защиты от URL типа http://example.com/../../file.txt
    22. if (RequestUri.IndexOf("..") >=  0)
    23. {
    24.     SendError(Client, 400);
    25.     return;
    26. }
    27.  
    28. // Если строка запроса оканчивается на "/", то добавим к ней index.html
    29. if (RequestUri.EndsWith("/"))
    30. {
    31.     RequestUri += "index.html";
    32. }

    Ну и наконец осуществим работу с файлами: проверим, есть ли нужный файл, определим его тип содержимого и передадим его клиенту.
    Copy Source | Copy HTML
    1. string FilePath = "www/" + RequestUri;
    2.  
    3. // Если в папке www не существует данного файла, посылаем ошибку 404
    4. if (!File.Exists(FilePath))
    5. {
    6.     SendError(Client, 404);
    7.     return;
    8. }
    9.  
    10. // Получаем расширение файла из строки запроса
    11. string Extension = RequestUri.Substring(RequestUri.LastIndexOf('.'));
    12.  
    13. // Тип содержимого
    14. string ContentType = "";
    15.  
    16. // Пытаемся определить тип содержимого по расширению файла
    17. switch (Extension)
    18. {
    19.     case ".htm":
    20.     case ".html":
    21.         ContentType = "text/html";
    22.         break;
    23.     case ".css":
    24.         ContentType = "text/stylesheet";
    25.         break;
    26.     case ".js":
    27.         ContentType = "text/javascript";
    28.         break;
    29.     case ".jpg":
    30.         ContentType = "image/jpeg";
    31.         break;
    32.     case ".jpeg":
    33.     case ".png":
    34.     case ".gif":
    35.         ContentType = "image/" + Extension.Substring(1);
    36.         break;
    37.     default:
    38.         if (Extension.Length > 1)
    39.         {
    40.             ContentType = "application/" + Extension.Substring(1);
    41.         }
    42.         else
    43.         {
    44.             ContentType = "application/unknown";
    45.         }
    46.         break;
    47. }
    48.  
    49. // Открываем файл, страхуясь на случай ошибки
    50. FileStream FS;
    51. try
    52. {
    53.     FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
    54. }
    55. catch (Exception)
    56. {
    57.     // Если случилась ошибка, посылаем клиенту ошибку 500
    58.     SendError(Client, 500);
    59.     return;
    60. }
    61.  
    62. // Посылаем заголовки
    63. string Headers = "HTTP/1.1 200 OK\nContent-Type: " + ContentType + "\nContent-Length: " + FS.Length + "\n\n";
    64. byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers);
    65. Client.GetStream().Write(HeadersBuffer,  0, HeadersBuffer.Length);
    66.  
    67. // Пока не достигнут конец файла
    68. while (FS.Position < FS.Length)
    69. {
    70.     // Читаем данные из файла
    71.     Count = FS.Read(Buffer,  0, Buffer.Length);
    72.     // И передаем их клиенту
    73.     Client.GetStream().Write(Buffer,  0, Count);
    74. }
    75.  
    76. // Закроем файл и соединение
    77. FS.Close();
    78. Client.Close();

    Также в коде упоминалась пока не описанная процедура SendError. Напишем и ее:
    Copy Source | Copy HTML
    1. // Отправка страницы с ошибкой
    2. private void SendError(TcpClient Client, int Code)
    3. {
    4.     // Получаем строку вида "200 OK"
    5.     // HttpStatusCode хранит в себе все статус-коды HTTP/1.1
    6.     string CodeStr = Code.ToString() + " " + ((HttpStatusCode)Code).ToString();
    7.     // Код простой HTML-странички
    8.     string Html = "<html><body><h1>" + CodeStr + "</h1></body></html>";
    9.     // Необходимые заголовки: ответ сервера, тип и длина содержимого. После двух пустых строк - само содержимое
    10.     string Str = "HTTP/1.1 " + CodeStr + "\nContent-type: text/html\nContent-Length:" + Html.Length.ToString() + "\n\n" + Html;
    11.     // Приведем строку к виду массива байт
    12.     byte[] Buffer = Encoding.ASCII.GetBytes(Str);
    13.     // Отправим его клиенту
    14.     Client.GetStream().Write(Buffer,  0, Buffer.Length);
    15.     // Закроем соединение
    16.     Client.Close();
    17. }

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

    Исходник (через ThreadPool)
    Исходник (через Thread)
    Архив с исходником (через ThreadPool, вариант через Thread закомментирован)
    Архив с откомпилированной версией (через ThreadPool)
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 69
    • +19
      хм, без обид, но помнится что это было заданием на лабораторную работу для второго курса. =)

      Впрочем, если это первая статья в цикле, то задел хороший.
      • +16
        Ну, у нас на втором курсе в дельфях кнопочки на форму кидать учились. Так что тут я вам завидую :-) А на эту тему планирую еще одну статью, про конфиги и CGI.
        • +2
          мне мало верится, что такому учат на втором курсе, ибо на «чисто программерских» специальностях на втором курсе обычно пилят ещё чистые С (и это ещё в лучшем случае), а на около программерских вообще таких вещей не изучают на лабах (максимум синтаксис и основные конструкции), всётаки http-server это достаточно частный пример. Где вы учились?
          • 0
            ХАИ, заочное отделение:
            1-2 курсы: паскаль, основы
            3 курс: ооп в делфи
            4 курс: структуры данных, базы данных
            5 курс: компиляторы, основы ИИ.

            как то так…
            • 0
              Аналогичная ситуация, ХНУРЭ.
              1-2 курсы: с++, основы
            • +1
              МГТУ «Станкин» специальность «информационные системы»
              1 семестр — паскаль
              2 семестр — делфи, основы ООП
              3 семестр — Плюсы
              4 семестр — Шарп в курсе по программированию и сабж в курсе, дай бог память, вроде сетей ЭВМ и телекоммуникаций.
              • 0
                Боже мой, наши люди.

                Сейчас там не преподают делфи и плюсы больше, нет средств. Сразу после паскаля шарп.
                • 0
                  МГГУ «Горный» специальность САПР(системы автоматизированного проектирования)
                  1 курс — Паскаль, Делфи
                  2 курс — Си++
                  3 курс:
                  — 1 семестр — СУБД: MS SQL Server, GMax, Blender
                  — 2 семестр — MS Visual Studio и мышеводство на формах с использованием ADO.NET, OpenGL
                  4 курс — пролог, псевдо-веб технологии, диплом
              • +1
                ИТМО, факультет информационных технологий и программирования.
                мы это (правда на джаве) недавно проходили.

                1 курс. паскаль, плюсы
                2 курс. джава, ассемблер.
                • +1
                  БГУИР, ФИТУ — Искусственный интеллект
                  1. С/C++: простенькие алгоритмы
                  2. C++: WinApi, Java: OOP
                  3. Операционные системы, сетевые протоколы, компиляторы, базы данных
                  4. Обработка изображений, параллельная обработка данных
                  5. Нейронные сети
                  • 0
                    Донецк, Донну, кафедра компьютерных технологий
                    1 курс — паскаль
                    2 курс — плюсы, основы ООП на плюсах, WinAPI, структуры данных, базы данных
                    3 курс — C#. Лаба по сетям на шарпе заключалась в получении бд с сервера и отображении на сервере. Почти сабж
                    4 курс — джава

                    Но вот если бы мне сейчас надо было писать ту лабу, топик бы очень даже помог.
                    • 0
                      ВМиК МГУ, прикладная математика и информатика
                      1 семестр — паскаль (и лекции по алгоритмам)
                      2 семестр — ассемблер (архитектура ЭВМ)
                      3 семестр — Си (операционные системы)
                      4 семестр — С++ (формальные грамматики и языки)
                      Потом разделение по кафедрам. Мы учили Яву, программистские кафедры — функциональные языки, Ява, БД и пр.
                      • +3
                        Вообще, по-моему, было бы интересно в отдельной теме собрать информацию, кто что учил в универе. А потом свести всё в единую таблицу, и по программированию, и другие дисциплины.
                        Нам было бы любопытно, абитуриентам полезно.
                        • 0
                          Прикладные математические кафедры (2 поток) учат в основном Матлаб, хотя делать почти все практические вычислительные задачи разрешается в любом языке (включая VBA+Excel), главное, чтобы работало.

                          Ну и да, на 3 курсе, все в обязательном порядке изучают БД (реляционные, нереляционные, алгоритмы, соотв. математика) и основы SQL.
                          • 0
                            Да, забыл, еще на 3 курсе все изучают машинную графику и сдают практическую работу в виде демосцены на OpenGL/DirectX с использованием всяких эффектов, шейдеров и т.д.
                        • +1
                          Политех (Казахстан), САПР заочное (псевдо дистанционное) отделение, сокращенное обучение после колледжа (родственная специальность).
                          1 курс — C++/Pascal.
                          2 курс — БД, Delphi.
                          3 курс — Есть лабораторные выполняемые в Pascal!!!
                          диплом.

                          Как видите у нас ни Java, ни шарпом не пахнет.
                          Самое веселое то, что я просил написать курсовые/лабы на C#, на что мне ответили отказом, опираясь якобы на «программу», в которой ведется весь учет курса.

                          Всегда злило обучение, ни колледж, ни универ не дают реальных навыков которые требуются в работе, действительно, если хочешь чему-то научиться, то уж извини, сам.
                      • +2
                        А где ж бенчмарки? Где потребление ресурсов на разных нагрузках?
                        Нет, я понимаю что цель — не быстродействие, но хочется знать насколько производительно это решение за 20 минут.
                        • +1
                          Проверил через wbox. Отладочная сборка, через ThreadPool (8 потоков). В спокойном режиме жрет 2,7 метра памяти. При 25 клиентах жрет 3,3 метра и 2% процессора. При 100 клиентах — 3,6 метра и до 15%. При отключении клиента может выдавать ошибку (но не завершаться полностью). Ошибка связана с отсутствием в коде лишних блоков try/catch, которые я не стал писать для простоты (достаточно будет экранировать new Client((TcpClient)StateInfo); в ClientThread, пусть это будет не совсем логически верно).
                          • +1
                            2% какого процессора-то?
                          • +2
                            HttpListener вообще по производительности получится быстрее TcpListener, так как использует напрямую драйвер ОС http.sys, поэтому бенчмарки ничего бы путного не показали
                            • 0
                              (разумеется, имел ввиду быстрее чем реализация веб-сервера с помощью TcpListener)
                              • +2
                                О ужас! HTTP в ядре??
                                • +1
                                  Вы ещё весь TCP-стек в юзерленд вынести предложите. Такой же протокол как и все остальные. В ядре ему самое место, смысл данные туда-сюда гонять, если их можно в одном месте обработать?
                                  • +1
                                    Вы еще предложите все программы в ядре запускать. Ведь зачем же данные туда-сюда гонять))
                                    • +4
                                      Глупость сказали. Вы, вероятнее всего, никогда не писали высоконагруженный софт. Там идёт экономия на каждом вызове к ядру, ибо любой обмен данными с ядром — это немаленький такой оверхед, который жрёт процессорное время со страшной силой. Недавно вот писали, что линуксу сейчас прикручивают механизм группировки сисколов, чтобы несколько вызовов шло ядру в одном пакете. Никогда не задумывались, зачем?
                                      И какой смысл делать n запросов на чтение из сокета (в котором данные появляются по кускам), если можно одним вытащить уже распарсенный http-запрос?
                            • +1
                              Это пример того как НЕ надо писать код. Вы не используете IOCP (следствие — ужасная масштабируемость), а для HTTP вообще есть специальное API.
                              • +4
                                Про IOCP я знаю, но оно не входит в .NET (по меньшей мере, те статьи на эту тему, которые я находил в интернете, используют неуправляемый код). Про HttpListener я написал в первом абзаце. Целью статьи было написать простой HTTP-сервер на .NET. За основу брался еще более простой код с RSDN.
                                • +3
                                  Ну здрасьте, а BeginAccept, BeginSend, BeginReceive по вашему как работают, если не через IOCP? Там самый что ни на есть IOCP!

                                  А что касается форума РСДН: Исходники, то там много хлама.
                                  • 0
                                    Пардон, вот этого не знал. Спасибо, учту.
                                  • +1
                                    «Про IOCP я знаю, но оно не входит в .NET (по меньшей мере, те статьи на эту тему, которые я находил в интернете, используют неуправляемый код). „
                                    И это в то время, как в последней рихтеровской CLR via C# мало того, что рассказано про IOCP, так еще и есть пример кода… сервера (правда, будем честными, пайпового) под .net.
                                • +4
                                  Как-то финализатор класса Server нелепо смотрится. Здесь разумнее реализовать IDisposable, а TcpListener должен сам остановиться, когда его GC найдет.
                                  • +7
                                    Я бы сказал, что финализатор не только нелепо смотрится, но это и показательный пример того, как делать не надо. И за бездумное использование финализаторов, считаю, нужно устраивать публичную порку. :)
                                    Объясню почему. Тот же TcpListener.Stop метод может легко кинуть SocketException. А что это значит для нас? Это значит, что возникни это исключение в финализаторе и вся аппликация упадет, причем так, что мы даже не узнаем, что и где случилось.
                                    • 0
                                      Спасибо, учту на будущее :-)
                                      • –2
                                        и вся аппликация упадет

                                        Аппликация — вырезание и наклеивание (нашивание) фигурок, узоров или целых картин из кусочков бумаги, ткани, кожи, растительных и прочих материалов на материал-основу (фон).
                                        • +4
                                          Правильно, но тут даже если мы перехватим все исключения — ничего путного не выйдет :)
                                          Очистка объектов с финализаторами производится за 2 сборки мусора, поэтому у объекта больше шансов попасть во 2-е поколение, которое собирается реже. GC может работать в несколько потоков (как например при фоновой сборке в .net 4, когда объекты поколений 0-1 и 2 собираются в разных потоках), поэтому Listener != null не спасет, надо ставить мьютекс, что опять же чревато (к управляемым ссылками тут обращаться вообще не стоит). Наконец объект TcpListener имеет свой финализатор, т.е. этот наш ~Server не делает ровным счетом ничего, кроме как создает проблемы :)
                                          О финализаторах лучше забыть как о страшном сне до тех пор, пока не понадобится работать с неуправляемыми ресурсами. А сама статья неплохая, про основы сокетов и потоков :)
                                          • 0
                                            Меня больше беспокоит, что ссылка на объект Server нигде не сохраняется. Не умрет ли он в релизной сборке? Кстати, в конкретном примере финализатор будет вызван только при остановке приложения и исключение в нем самому приложению не повредит. Ну а IDisposable нужен только там, где управление освобождением ресурсов осуществляется (или подразумевается, что будет осуществляться) вручную.
                                            • 0
                                              Не умрёт. Часто использую конструкцию new Thread(MyProc).Start(); и никто от этого не умер.
                                              • 0
                                                Ммм… немного некорректный пример, так как новый поток сам является источником корней для графа объектов и его-то GC собирать не будет, но ответ про «не умрет» все равно верный, так как, по сути, основной поток навсегда замирает в конструкторе объекта.
                                                • 0
                                                  Вы не говорили про GC. Вы говорили про релизную сборку. Работа компилятора по вырезанию лишнего хлама и работа GC — несколько разные вещи.
                                                  • 0
                                                    Простите, под смертью я подразумевал удаление посредством GC из-за отсутствия корня. В дебаге этого не происходит, а в релизе — обычное дело.
                                            • +2
                                              Не говоря уже о том, что в финализаторе нельзя обращаться к другим финализируемым объектам, так как порядок вызова финализаторов не определён: «Specifically, you must know that any code you call from within a Finalize method does not use any other object that could have already been finalized.» © Richter

                                              И вообще: «If you’re familiar with C++, you’ll notice that the special syntax C# requires for defining a Finalize method looks just like the syntax you’d use to define a C++ destructor. In fact, the C# Programming Language Specification calls this method a destructor. However, a Finalize method doesn’t work like an unmanaged C++ destructor at all, and this has caused a great deal of confusion for developers migrating from one language to another.
                                              The problem is that developers mistakenly believe that using the C# destructor syntax means that the type’s objects will be deterministically destructed, just as they would be in C++. However, the CLR doesn’t support deterministic destruction, preventing C# from providing this mechanism.» © Richter
                                          • +2
                                            А если сравнить ваш метод с асинхронными сокетами
                                            msdn.microsoft.com/en-us/library/fx6588te%28vs.80%29.aspx
                                            где меньше будет потребление ресурсов?

                                            • +1
                                              Можно уточнить что значит значок "~" перед именем Server?
                                              • +2
                                                • +17
                                                  Вот и выросло поколение людей, которые не знают, что такое "~"…
                                                  • +2
                                                    Я никого не защищаю, конечно, стыдно не знать, что такое деструктор (хотя, если человек никогда не был связан с программированием, то почему бы и нет?), но, судя по профилям, это «поколение» вас старше на 5 лет.
                                                    • +5
                                                      Да и степень стыдобы в данном случае — вопрос обсуждаемый. В дот нете часто лучше не знать, что ~ — это деструктор.
                                                      • 0
                                                        Ну если человек не связан с программированием, то что он забыл в топике, где по сути кроме кода и нету ничего? А то, что на 5 лет старше, так это вдвойне грустно.
                                                  • 0
                                                    Кому тема mini-серверов на .Net интересна, рекомендую посмотреть Cassini: en.wikipedia.org/wiki/Cassini_Web_Server Он не на много сложнее, но поддерживает ASP.Net (не только статика).
                                                    • +2
                                                      Вообще говоря многопоточные сервера — зло. Есть же дотнете асинхронщина для неблокирующей работы с сокетами.
                                                      • –1
                                                        Если делать простые вещи с упором на производительность — то да. Если что-то сложное — умаешься так писать.
                                                        • 0
                                                          Если вынести логику в схему «запрос-ответ» с сохранением состояний а ля сессия, а работу с пакетами перенести на отдельный уровень, то всё ок. Получаем событийную модель.
                                                          • 0
                                                            Останутся еще обращения к базе и прочим файлам, которые блокируют. Через это придется программу разрезать на куски, пихать куда-то временный стейт и все такое. Такой режим в ASP.net, кстати, есть. Но что-то более-менее сложное писать на этом умаешься.
                                                            • +1
                                                              С сишарповским-то синтаксисом замыканий? Наплодил колбэков и поехали. Тем более что сейчас соорудили ключевое слово async.
                                                      • +5
                                                        С точки зрения программирования: делать в конструкторе бесконечный цикл и закладывать логику в деструктор (это акутально для .NET) как-то нехорошо, мне кажется. Даже в рамках демонстрационного примера.
                                                        • +3
                                                          Сам интересный для меня вопрос: а что такого специфически c#-ного тут?

                                                          Это не «сервер на c#, это „сервер на clr“.

                                                          Ну и да, выше правильно пишут, в нем столько ошибок, что на нем можно делать демонстрации „как не надо делать“.
                                                          • +5
                                                                    // В бесконечном цикле
                                                                    while (true)


                                                            писать комментарии хорошо, но такие абсолютно бесполезны.
                                                            • 0
                                                              Пул потоков надо делать изначально расширяемым. Лучше медленно обслужить все запросы, чем вообще не обслужить половину.
                                                              • 0
                                                                Он расширяемый. Просто когда потоков больше лимита, они начинают создаваться не чаще чем раз в полсекунды вроде. Да и лимит там по-умолчанию вроде больше чем 8, что-то типа по 20 на ядро.
                                                              • 0
                                                                Называть локальные и приватные переменные с большой буквы — это некомильфо.
                                                                Обращаться к управляемым объектам в деструкторе — просто нельзя.
                                                                Если вы уже пишите комментарии, то для классов и методов лучше использовать xml-образные.
                                                                Вы извините, если это выглядит как то, что я придираюсь, просто при написании коммерческого кода Вас за это могут больно пнуть.
                                                                • +1
                                                                  Ощущение, что статья устарела лет эдак на N.
                                                                  Какие деструкторы (IDisposable!), какой Thread.Start (TPL!)…
                                                                  • –4
                                                                    Написать что ли статью «многопоточный веб-сервер на PHP»? :)
                                                                    • 0
                                                                      А почему Вы просто не воспользовались встроенным классом HttpListener?
                                                                      • 0
                                                                        Пропустил первый абзац =(
                                                                      • +1
                                                                        Как поделка для здачи преподу сгодится. А так нужно исспользовать WCF и не плодить велосипеды.
                                                                        • 0
                                                                          Не люблю:
                                                                          • 0
                                                                            — конкатенацию при помощи "+";
                                                                            — магические числа в коде (даже степени двойки);
                                                                            — "", когда надо писать string.Empty (или ничего не писать, если объявление, всё равно инициализация по умолчанию);

                                                                            Люблю:
                                                                            — следовать общепринятым стандартам наименования;
                                                                            — использовать Path для конкатенации путей;
                                                                            — использовать var
                                                                            — использовать using, чтобы не забыть закрыть stream

                                                                            Сугубо ихмо. Ну а статья в целом познавательна для незнакомых с темой.
                                                                            • 0
                                                                              С каких это пор default(string) == string.Empty? Всегда null было.
                                                                              • 0
                                                                                Не равно, конечно. Присваивать в начале string.Empty или нет нужно в зависимости от того, как дальше будет использоваться переменная.

                                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.