Веб-сервер на C#

image

Доброго дня! Виникла в мене сьогодні ідея, яка полягає в тому, щоб викласти на цей блог веб-сервер, який я зі своєю командою робили в SoftServe Training Center.

Я не пропоную його застосовувати для підтримки вашого сайту, а просто хочу продемонструвати, як легко можна зробити веб-сервер за допомогою .NET Framework та C#.

Веб-сервер підтримує наступні можливості:

  1. Http 1.0, 1.1
  2. HTML/Text/JavaScript
  3. Завантаження та довантаження файлів
  4. Теги include, для вставки додаткового текстового файлу в html документ <!–#include file=”header.html” –>
  5. Сесії
  6. Cookies
  7. Логування в бд або в текстовий файл
  8. Розгалуження навантаження на кілька екземплярів серверу

Також застосовувались наступні технології:

  1. Windows Communication Foundation
  2. Пару класів з простору імен System.Net
  3. ADO .NET
  4. Різні типи з .NET Framework 4.0 для роботи з конфігураційними файлами, текстом, фізичними файлами на диску

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

image

В структурі проекту ServerLib є чітко вітділені підчастини, які відповідають за реалізацію певної функціональності:

image

Кожна назва папки відповідає за дію, яку вона надає, але всетаки я вирішив викласти загальний опис кожної:

  1. AppSetting – налаштування серверу
  2. Communication – зв’язок з сервером
  3. Debug – відлагодження серверу (логування)
  4. Handlers – аналіз запиту та формування відповіді (зчитування файлу з диска і т.д.)
  5. PostProcessing – обробка тегів <!–#include file=”header.html” –>
  6. RemoteManagement – управління іншими екземплярами цього серверу
  7. Request – оброка та формування екземпляру об’єктної моделі запитів
  8. Response – формування екземпляру об’єктної моделі відповіді

Всі дії починаються з запиту клієнта до сервера, коли клієнт шле Http запит у вигляді тексту

image

Опрацьовує ці запити клас Communicator, який є сінглтоном, і має такі головні методи та властивості та події:

image

В коді робота комунікатора виглядає наступним чином:

public void Start()
{
    ThreadPool.SetMaxThreads(Configurator.Singleton.MaxPoolThreads , 20000);
    if (!isRunning)
    {
        communicatorThread = new Thread(Communicator.Instance.CommunicatorThreadStart);
        communicatorThread.Name = &quot;Communicator Thread&quot;;
        tcpListener = new TcpListener( IPAddress.Any, Configurator.Singleton.ServerPort);
        try
        {
            tcpListener.Start();
        }
        catch (SocketException)
        {
            System.Windows.Forms.MessageBox.Show(String.Format(&quot;Port {0} is busy&quot;, Configurator.Singleton.ServerPort), &quot;Error&quot;, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
            return;
        }
        communicatorThread.Start();
        isRunning = true;
        if (Started != null) Started(this, new ServerEventArgs(&quot;Server started&quot;, DateTime.Now.ToString()));
    }
   else
   {
        if (Error!=null) Error(this, new ServerEventArgs(&quot;Attempt to start Communicator&quot;,&quot;Communicator is already running&quot;));
        throw new Exception(&quot;Communicator is already running&quot;);
   }
}

За допомогою цього методу робляться налаштування максимальної кількості потоків в пулі, а потім запускається сам веб-сервер, за умовою, що він на даний момент ще не є запущений.

Спочатку створюється додатковий поток, який буде приймати всі запити та передавати їх в чергу на обробку, потім ініціалізується TcpListener з портом, який заданий в конфігураційному файлі, і в кінці запускається поток, який був створений на початку цього методу.

Сама реалізація прослуховування запитів та відправки їх на оброку виглядає ось так:

private void CommunicatorThreadStart()
{
    Socket socket;
    // ThreadPool.SetMaxThreads(1, 20000);
    while (true)
    {
         try
         {
             socket = tcpListener.AcceptSocket();
             socket.ReceiveTimeout = socketReceiveTimeout;
             Configurator.Singleton.IncreaseClientsCount();
             ThreadPool.QueueUserWorkItem(RequestWorkItem, socket);
             if (StartedPoolThread != null) StartedPoolThread(this, new ServerEventArgs(&quot;New PoolThread started&quot;, &quot;&quot;));
         }
         catch (Exception e)
         {
             if (Error!=null) Error(this, new ServerEventArgs(e.Message,&quot;&quot; ));
             break;
         }
   }
} //end of method CommunicatorThreadStart

В цьому методі, який постійно запущений відбувається прослуховування клієнтських запитів, та передавання цих запитів в чергу потоків.

Деталі методів для роботи з пулом потоків я в цій темі описувати не буду, так як для цього потрібно окрему тему, але що стосується веб-сервера, то код оброки запиту, який знаходиться в комунікаторі виглядає так:

private void RequestWorkItem(object state) //callBack for ThreadPool
{
     Socket socket = (Socket)state;
     HttpRequest request = null;
byte[] buffer = new byte[socket.ReceiveBufferSize];
int count; //count of received bytes from socket
do
{
try
{
count = socket.Receive(buffer);
ASCIIEncoding enc = new ASCIIEncoding();
Console.WriteLine(enc.GetString(buffer, 0, count));
          }
          catch (SocketException e)
          {
               if (Error != null) Error(this, new ServerEventArgs(e.Message, &quot;&quot;));
               break;
          }
          try
          {
                request = RequestBuilder.ParseRequest(buffer, socket);// request Parsing
          }
          catch (Exception) //Invalid request
          {
                if (StartedPoolThread != null) StartedPoolThread(this, new ServerEventArgs(&quot;Invalid request&quot;, &quot;&quot;));
                break;
          }
          if (ReceivedRequest != null) ReceivedRequest(this, new ServerEventArgs(&quot;Request received&quot;, request.ToString()));
          HandleRequest(request);
      }
      while (request.Connection == HttpConnectionAction.KeepAlive); //don't close socket when in request is header &quot;Connection: Keep-Alive&quot;
      socket.Close();
      Configurator.Singleton.DecreaseClientsCount();
      if (StartedPoolThread != null) FinishedPoolThread(this, new ServerEventArgs(&quot;New PoolThread finished&quot;, &quot;&quot;));
}// end of void RequestWorkItem(object state)

Простіше кажучи, в цьому методі отримується з’єднання з клієнтом (Socket), після чого розбирається запит за допомогою методу RequestBuilder.ParseRequest, який повертає екземпляр класу Request, який містить всі необхідні дані, які відносяться до запиту:

image

Екземпляр класу Request передається в метод HandleRequest, в якому ініціалізуються різні типи класу Handler, за допомогою яких відбувається обробка запиту:

private void HandleRequest(HttpRequest request) //creating handlers and request handling
{
    //*********** creating handlers and chain of respronsibility*******
TextHandler textHandler = new TextHandler();
BinaryHandler binaryHandler = new BinaryHandler();
ErrorHandler errorHandler = new ErrorHandler();
RedirectHandler redirectHandler = new RedirectHandler();
PostHandler postHandler = new PostHandler();
redirectHandler.SetupHandler(textHandler);
textHandler.SetupHandler(binaryHandler);
binaryHandler.SetupHandler(postHandler);
postHandler.SetupHandler(errorHandler);
//******************************************************************
Console.WriteLine(request.URI);
redirectHandler.Handle(request);
}// end of void HandleRequest(byte[] data)

В класі Handler реалізований паттер Chain Of Responsibility, тобто запит буде оброблятись в такій послідовності: RedirectHandler->TextHandler->BinaryHandler->PostHandler->ErrorHandler.

Подившиь в папки RemoteManagement та AppSettings можна взяти за ідею, реалізацію веб-серверу з розгалуженням навантаження.

Для ознайомлення з веб-сервером цього буде достатньо, тому взявши за приклад цей веб сервер, ви зможете реалізувати свій, або використати частини готової функіональності з цієї реалізації.

P.S. Автори проекту:

imageimageimageimage

Веб-сервер ви можете скачати тут.

Дякую за ваше відвудування цього блогу!

Advertisements

, , , , , ,

  1. #1 by Edward on July 13, 2011 - 20:36

    :))) Памятаю також писав подібне

    Правда у нас пункт 8 ( Розгалуження навантаження на кілька екземплярів серверу ) криво працював трохи, але зате ми змогли похвалитись тим що сервер витримав тест на 2100 запитів 😉

  2. #2 by Serhiy Shumakov on July 13, 2011 - 21:00

    Маєш сорси того сервера?))

  3. #3 by Edward on July 14, 2011 - 09:15

    Десь були у папці Recycle.bin ;)))
    А так то десь на харді валяються серед іншого сміття, правда не знаю якої версії

  4. #4 by romko on October 24, 2011 - 00:27

    Сорі, а можеш перезалити на рапідшару свій сервак або скинути на мило

  5. #5 by Serhiy Shumakov on October 25, 2011 - 22:08

    Привіт, вибач за незручності. Веб сервер вже залив на skydrive :), пробуй знову.

  6. #6 by romko on October 26, 2011 - 13:16

    thnx!!!!

  7. #7 by Andriy on February 16, 2012 - 21:04

    а ви повністю все самі реалізовували чи використовували якісь “ісходніки”? Просто дивлюсь на обєм роботи і розумію що це реально круто:)

    • #8 by Serhiy Shumakov on February 17, 2012 - 08:15

      Так, ми реалізовували цей сервер самі 😉

  8. #9 by Andriy on February 24, 2012 - 19:44

    привіт, в мене виникло запитання, чи можна перекодувати запит типу
    int count = Client.GetStream().Read(buffer, 0, 1024);
    request += Encoding.UTF8.GetString(buffer, 0, count);
    але так як вище не працює, всерівно викидає аскі, буде дуже вдячний якщо Ви мені допоможете, хочаб з напрямком вирішення даної проблеми

    • #10 by Serhiy Shumakov on February 24, 2012 - 23:50

      Привіт, можете скинути більш детальніший опис помилки?

  9. #11 by Andriy on February 25, 2012 - 12:06

    Привіт, по-перше дякую за намагання допомогти:), і так, справа в тому що я також пишу веб сервер, він містить діректорі хендлер який обробляє вміст папок, я хочу зробити тоак, щоб можна було відображати кирилицю, це я зробив, але коли користувач переходить по такій ссилці наприклад /Фільми тоді в метод GET() записуюється символи в кодуванні аскі, тобто щось на подобі d%d%f%g%g%(ці символи не мають під собою ніякого вмісту), очевидно проблеми виникаэ на етапы формуваня метода GET() шттп протоколу, буду дуже вдячний якщо Ви мені допожете.

    • #12 by Serhiy Shumakov on February 26, 2012 - 11:13

      Тобто Ви не можете отримати нормальну назву через метод GET того, що замість кирилиці отримуєте символи закодовані в ASCII?

      Якщо так, то спробуйте їх декодувати таким чином:

      byte[] encodedData = GetEncodedDataFromSomewhere();
      string decoded = Encoding.ASCII.GetString(encodedData);

      Я правильно зрозумів питання?

  10. #13 by Andriy on February 26, 2012 - 20:46

    Все розыбрався, фышка в тому що гет кодує юрл в утф 8, розкодувати назад в теж саме утф 8 виявилось не просто, вирішив проблему наступним чином, якзо це гавнокод – вибачте,
    int count = Client.GetStream().Read(buffer, 0, 1024);
    request += Encoding.UTF8.GetString(buffer, 0, count);
    string temp= Uri.UnescapeDataString(request);
    Дякую за допомогу

  11. #14 by Yaroslav on August 9, 2012 - 11:44

    Привіт. Наскільки я зрозумів у відповідному хендлері будується Response і грубо кажучи на цьому і все закінчується. Тобто хендлери повинні будуватися тоді, коли практично написана половина проекту ? я все правильно зрозумів чи ні ?

    • #15 by Serhiy Shumakov on August 9, 2012 - 16:01

      Так, хендлери це типи які відповідають за формування відповіді, тому, їх варто починати тоді коли частина програми, яка відповповідає за комунікацію з клієнтом готова.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: