Перевод статьи http://www.codeproject.com/Articles/376754/Building-a-Socket-WebServer-using-Csharp

Скачать: [download id=»14″]

Скачать: [download id=»15″]

Эта статья показывает, как можно написать простой web сервер, который поддерживает GZIP сжатие, приложения и сессии. Проект реализован на C# с .NET 4.0 и Visual Studio 2010. Два демонстрационных приложения включены в проект. Сперва запустите «server tester», проверьте чтобы порт 8080 был не занят, потом нажмите «Старт» и введите в адресной строке браузера следующие варианты:

Важно: адрес надо вводить со знаком / в конце запроса, иначе ошибка

1.http://localhost:8080/chat/ — для тестирования приложения ChatServer (чат). Нужно открыть два браузера (вкладки) для проверки чата

loginpage_small

 

roompage_small

2. http://localhost:8080/desk/ — для тестирования приложения DesktopViewer, которое отображает рабочий стол Вашего сервера в браузере

dektopviewer_small

 

Итак, то же такое Web сервер? В нескольких словах — это программа, которая слушает запросы пользователя на определенном порту, по протоколу HTTP. Запрос обычно направлен на получение ресурсов сервера, как статических (css, png, jpg и т.д.) так и динамических (HTML, HTM, DHTML…) По умолчанию, статические ресурсы отправляются клиенту автоматически, но мы можем изменить это поведение, как например в примере Desktop.

РЕЗЮМЕ

Решение состоит из семи проектов: SocketEngine, ComunicationLayer, Service, и BizApplication, общая библиотека, и два демонстрационных приложения для тестирования:

  •  SocketEngine -этот компонент заботится о приеме всех подключений. Для повышения производительности я использовал классический TCP listener из репозитория MSDN.
  • ComunicationLayer — Содержит Socketcomunicator, который абстрагирует SocketEngine.
  • BizApplication — Это слой бизнес-логики, который реализует взаимодействие пользователя (его запросов) с серверными приложениями. Когда запрос получен сервером,то сервер его проверяет на корректность, создает экземпляр необходимо приложения (в соответствии с запросом) и сформированный приложением ответ отправляется обратно пользователю. Если ресурса не существует формируется ответ с ошибкой 404. Детали BizApplication далее в статье.
  • Service — это тот компонент в котором мы соединяем все компоненты в одно целое.В итоге код выглядит следующим образом:
Server server=new Server();
server.AddService<HttpService<myLogger>>(8080);

 Использование

Во-первых давайте посмотрим как настроить сервер. Параметры находятся в файле app.config в TesterProject и включают в себя root директорию, страницу ошибки, путь к файлу application.xml, в котором содержится информация о классах установленных web приложений. Я создания нового приложения, нужно описать следующее: путь к DLL, полное имя (namespace + classname) класса web приложения и полное имя класса настроек приложения. Например так:

<Application>
    <Name>Chat</Name>
    <Assembly>Demos.dll</Assembly>
    <ApplicationSettingsClass>Chat.HttpApplication.ChatServerConfiguration</ApplicationSettingsClass>
    <ApplicationClass>Chat.HttpApplication.ChatServer</ApplicationClass>
</Application>

Теперь, прежде чем объяснить, как запрос от socketa переходит в ответ в web-приложения, полезно узнать, как все части связаны друг с другом. Структура сервера описана в классе Service.

///Server/Service.sc

public class Service<PROVIDER,OUTPUTCHANNEL> : IServiceBase
        where OUTPUTCHANNEL : IServerOutput
        where PROVIDER : IServerService
{

    OUTPUTCHANNEL sender;
    PROVIDER provider;

    public Service(SENDER sender, PROVIDER provider)
    {
        this.sender = sender;
        this.provider = provider;            
    }

    public void ParseNewRequest(RawRequest e)
    {
        sender.SendResponse(provider.GetResponse(e));
    }

    public void Dispose()
    {
     //Dispose resources
    }
}

Класс Service работает по схеме запрос/ответ, связываю Клиента и Поставщика (приложения) в простом шаблоне  sender.SendResponse(provider.GetResponse(request)); Теперь нам нужен компонент, который использует интерфейс IServiceBase и определяет метод ParseNewRequest. В нашем примере запросы пользователя мы получаем через сокет, по этому используем такой шаблон: HttpService в качестве поставщика и SocketCommunicator как канал для ввода вывода. Теперь мы готовы к созданию службы Http:

///Server/WebServer.sc

SocketComunicator In_Out_channel=new SocketComunicator();
Service<HttpService, SocketComunicator> service = 
    new Service <HttpService,SocketComunicator>(In_Out_channel, service);
In_Out_channel.SetServiceHandler(servicehost);
In_Out_channel.StartListen(port);

Теперь  давайте посмотрим на детали реализации.SocketComunicator класс используется для получения и отправки данных через TCP / IP.

///ComunicationLayer/SocketComunicator.cs
...
public void SetServiceHandler(IServiceBase wb)
{
    this.serviceInterface = wb;
}

void Socket_OnNewDataReceived(object sender, SocketConnection e, byte[] dataRef)
{
    if (this.serviceInterface != null)  //
        this.serviceInterface.ParseNewRequest(new RawRequest() { Connection = e, RawData = dataRef });
}
...

Можете убедиться, что каждый Socket_OnNewDataReceived вызывается полностью асинхронно, потому что SocketAsycnEventArgs класс использует модель  I/O Completion Ports. Это является большим преимуществом для работы с асинхронными сокетами.

Давайте взглянем на то, как запрос обрабатывается провайдером.Запрос достигает функции GetResponse класса HttpService, и первое, что делает этот метод — это проверка на соответствие запроса протоколу HTTP. Если проверка не пройдена, то соединение закрывается, иначе создается новый HttpRequest, который содержит все данные запроса, как HTTP заголовки, пути и QueryStrings.

public RawResponse GetResponse(RawRequest req)
{
    RawResponse service_output = null;
    HttpRequest httpreq = null;
    if (HttpApplicationManager.TryValidate(req, out httpreq))
    {
        ///
        /// ManageHttpRequest method deals corrects http requests
        ///                       
        this.tracer.trace("New request: " + httpreq.CompleteRequest);
        service_output = ManageHttpRequest(httpreq);
    }
    else
    {
        this.tracer.trace("Invalid request.");
        service_output = new RawResponse(req) { Action = ResponseAction.Disconnect };
    }
    return service_output;
}

Я полагаю, что каждый запрос из браузера относится к конкретному приложению на сервере. Первую часть адреса (url) я называю MainPath — он указывает имя вызываемого приложения. После он, следующих частей непосредственно решен посредством применения. Например, следующий запрос: HTTP / / локальный: 8080/chat/roompage2.html  — запрашивает страницу под названием «roompage2.html в приложении под названием Chat. Для передачи запроса  приложению, мы должны его сначала создать, таким образом это означает, что каждый запрос генерирует новый экземпляр приложения на сервере? Для того чтобы не создавать новые экземпляры приложения на каждые запросы реализованы сессии. Использование типа сессий в приложении задается в  ApplicationSettings. В этом проекте реализовано три способа обработки сессий: ApplicationSessionMode {SingletonSession, IpSession, BrowserSession}. Например, приложение ChatServer  использует режим BrowserSession. Компонент HttpApplicationManager  заботится о сессиях, проверяя входящие HTTP запросы с существующими сессиями. Если сессии  не существует, он создает новую, а также проверяет, сколько истечение сеансов по времени, например, после 30 секунд бездействия (см. в описании класса ApplicationSettings).

///MainBiz/Http/HttpApplicationManager.cs

public bool TryGetApplicationInstance(HttpRequest e, 
       out ApplicationInstanceBase application)
{
    application = null;
    string[] paths = e.Path.Split(new string[] { "/" }, 
             StringSplitOptions.RemoveEmptyEntries);

    if (paths.Length == 0) return false;

    string mainPath = paths[0];
    ///
    /// Проверка на существующее имя приложения
    ///
    if (!applications.ContainsKey(mainPath)) return false;
    //Ok the applicatoin Exist!

    SessionManager sessionMgr = applications[mainPath];

    ApplicationSettings settings = sessionMgr.Info;
    string sessionKey = string.Empty;

    switch (settings.SessionType)
    {
        case ApplicationSessionMode.SingletonSession:
            ///
            /// SingletonSession 
            ///
            application = sessionMgr.GetOrCreateSingletonInstance();
            return true;
        case ApplicationSessionMode.BrowserSession:
            ///
            /// IP + userAgent
            ///                    
            sessionKey = e.Rawrequest.Connection.IP + "@" + e.Requests["User-Agent"];
            break;
        case ApplicationSessionMode.IpSession:
            ///
            /// Идентификация сессии по IP адресу
            ///                    
            sessionKey = e.Rawrequest.Connection.IP.ToString();
            break;
    }

    application = sessionMgr.GetOrCreateInstanceBySessionKey(sessionKey);

    return true;

Код прост и очевиден. Если TryGetApplicationInstance(reqhttp, из сессии) успешен, то  HTTP-запрос формирует новую сессию. Перед возвратом ответа проверяем, вдруг  приложению требуется, чтобы ответ был направлен также в другие сессии. Эта функция полезна, когда серверу приложений необходимо знать о других сессиях. Если что-то пойдет не так важно показать в браузере подробные сведения об ошибке. Каждое пойманное исключение генерирует  ответ с  404 кодом ошибки.

///Server/Services/HttpServices.cs

public RawResponse ManageHttpRequest(HttpRequest reqhttp)
{
   ApplicationResponse output = null;
    try
    {
        ApplicationInstanceBase session = null;
        if (this.appManager.TryGetApplicationInstance(reqhttp, out session))
        {
            output = session.ProcessRequest(reqhttp);

            if (output == null)
                ///
                /// application error
                ///
                throw new InvalidOperationException("Application " + reqhttp.Path + " not respond.");
            if (reqhttp.Type == HttpRequestType.HttpPage)
            {

                switch (session.Info.ResponseMode)
                {
                    case ApplicationResponseBehavior.ShareAndSend:
                        ///
                        ///  раздаем ответ всем сесииям
                        ///
                        this.appManager.ShareApplicationOutput(session, output, reqhttp);
                        break;
                }
            }
        }
        else
        {
            switch (reqhttp.Type)
            {
                case HttpRequestType.HttpPage:
                   if (reqhttp.Paths.Count > 0)
                    {
                        throw new InvalidOperationException("Application " + reqhttp.Path + " not exist");
                    }
                    else
                    {
                        ///  htpp://localhost:port:/
                        output = HttpHelper.Generate404Page(reqhttp, "","Welcome :)","Server Home Page");
                    }
                    break;
                case HttpRequestType.HttpStaticRequest:
                     ///
                     /// Не найдено такое приложение, но это может быть запрос на статический ресурс(css, js, png ecc.. ) 
                     /// пробуем выдать этот ресурс из  root директорию 
                     /// Если не найден ресурс выдаем 404 ошибку.
                     ///
                    output = this.appManager.ResponseStaticResource(reqhttp);
                    break;
            }
        }
    }
    catch (Exception ex)
    {     
        this.tracer.trace("ERROR" + ex.Message + "::" + ex.StackTrace);
        output = HttpHelper.Generate404Page(reqhttp, ""+ex.Message+"::"+ex.StackTrace, 
                 "Error occured parsing " + reqhttp.Path);
    }
    return output;
}

После того как мы выяснили, ка работает Http сервис, давайте разберем, как создать приложение, на примере приложения Chat. Мы начинаем с наследования HttpApplicationBase и обеспечением реализации двух абстрактных методов PageLoad и ApplicationDirectory. ApplicationDirectory возвращает физический путь к папке приложения, где можно найти его ресурсы такие как: html, css, js.

protected override void PageLoad(HttpRequest req)
{
    ///
    /// The ChatServer application is strictly relate with the javascript implementation,
    /// so if the request contains query url parameters means
    /// that is invoked by the XMLHttpRequest object (ajax pattern).
    /// You can choose a different way to recognize an pageload event 
    /// (called by Asp.net postback) instead of ajax event.
    ///
    ///  The query url request has this pattern:
    ///   ?op=<operation>&<par1>=<value1>&<par2>=<value2>
    ///   where op stands for 'operation'.
    ///
    string page = Request.Paths[Request.Paths.Count - 1];

    if (req.UrlParameters.Count > 0)
    {
        SharedChatMessage sharedresponse = null;
        ChatMessage message = null;
        string operation = req.GetQueryStringValue("op");
        switch (operation)
        {
            case "login":
                ///
                /// Login operation process steps:
                ///    1) Check username and password , if one of these is empty we respond with an alert
                ///    2) Validation of the user
                ///    3) Respond with a redirect
                ///
                string username = req.GetQueryStringValue("username");
                string password = req.GetQueryStringValue("password");
                if (String.IsNullOrEmpty(username) || String.IsNullOrEmpty(password))
                {
                    BuildChatResponse(new ChatMessage() { MessageCode = 
                       (int)MessageType.alert, Value = "Login request error." }, true);
                    return;
                }
                currentUsername = username;
                currentPassowrd = password;
                isValidUser = true;
                BuildChatResponse(new ChatMessage() { MessageCode = 
                  (int)MessageType.eval, Value = "window.location=\"" + roomPage + "\"" }, true);
                return;
            case "listen":
                ///
                /// When the room page is loaded start sending 'listen' operation request in loop,
                /// at every listen request we respond with a chatmessage getting
                /// from the queue or,if is empty ,with a skip action message.
                /// But firstable we sent adduser action message
                /// with SharedChatMessage envelop for notify the new user.
                ///
                if (!sendAdduser)
                {
                    sendAdduser = true;
                    message = new ChatMessage() { MessageCode = (int)MessageType.adduser, User = currentUsername, };
                    BuildChatResponse(message, false);
                    this.response = sharedresponse = 
                      new SharedChatMessage(Response.ResponseData, Response.AppRequest, message);
                    return;
                }
                if (localqueuemessages.Count == 0)
                {
                    System.Threading.Thread.Sleep(500);
                    BuildChatResponse(new ChatMessage() { MessageCode = (int)MessageType.skip, Value = "" }, false);
                }
                else
                {
                    ChatMessage msg = null;
                    if (localqueuemessages.TryDequeue(out msg))
                        BuildChatResponse(msg, false);
                }
                return;
            case "message":
                ///
                /// A chat message has been sent by the user,
                ///     firstable we build ChatMessage packet replacing the response with SharedChatMessage envelop,
                ///     that is why SharedChatMessage is visible to the other session (see OnNewShareResponse).
                ///
                ///

                string value = req.GetQueryStringValue("value");
                message = new ChatMessage() { MessageCode = (int)MessageType.chatmessage, 
                          Value = value, User = currentUsername };
                BuildChatResponse(message, false);
                sharedresponse = new SharedChatMessage(Response.ResponseData, Response.AppRequest, message);
                Response = sharedresponse;
                return;

            default:
                throw new InvalidOperationException("Invalid request");
        }
    }
    if (page == roomPage)
    {
        ///
        /// if the user not perform the login the roomPage
        /// will not be loaded, will be sent a login page
        ///
        if (!isValidUser)
        {
            BuildResponseFile(ApplicationDirectory() + "\\" + loginPage,MimeType.text_html);
            return;
        }
        else
        {
            byte[] room = Helper.GetFile(ApplicationDirectory() + "\\" + roomPage);
            string msg = new string(Encoding.UTF8.GetChars(room));
            msg = msg.Replace("", this.currentUsername);
            BuildResponse(msg);
        }
    }
}

Можно много чего усовершенствовать в этом проекте. В этой статье я сосредоточился на архитектуре сервера, но так и не объяснили детали того, как запрос HTTP анализируется  и как строится HTTP ответ. Это можно сделать многими путями, я просто реализовал один из них. Если вы используете SslStream, а не низкий уровень сокетов (которые не обладают встроенной поддержкой SSL шифрования) вы можете использовать  HTTPS подключения в браузере.

Создание web сервера на c#. С приложениями.
Метки:

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

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

Нажимая на кнопку "Отправить комментарий", я даю согласие на обработку персональных данных и соглашаюсь c политикой конфиденциальности *