Поиск на сайте
Главная Журнал Форум Wiki DRKB Страны мира

Простой HTTP Server в C#

Введение

В этой статье рассматривается простой класс HTTP-сервера, который можно добавить в собственный проект, а так же дающий больше представления о протоколе HTTP. Для высоко-производительных сервисов обычно используются устойчивые устойчивые веб-сервера, такие как IIS, Apache или Tomcat. Но HTML настолько гибкий язык интерфейса, что бывает полезен практически в любом приложении или внутреннем (бэкэнд) сервере, где временнЫе затраты на конфигурацию и использование внешнего веб-сервера невыгодны. Всё что нужно для этого - простой HTTP-класс для обработки входящих веб-запросов, который можно легко встроить в приложение.

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

Сперва рассмотрим, как этот класс можно использовать, а затем разберёмся как он работает и покопаемся в некоторых его особенностях. Для начала унаследуем свой класс от HttpServer и реализуем два абстрактных метода handleGETRequest и handlePOSTRequest...

    public class MyHttpServer : HttpServer {
        public MyHttpServer(int port)
            : base(port) {
        }
        public override void handleGETRequest(HttpProcessor p) {
            Console.WriteLine("request: {0}", p.http_url);
            p.writeSuccess();
            p.outputStream.WriteLine("<html><body><h1>test server</h1>");
            p.outputStream.WriteLine("Current Time: " + DateTime.Now.ToString());
            p.outputStream.WriteLine("url : {0}", p.http_url);
 
            p.outputStream.WriteLine("<form method=post action=/form>");
            p.outputStream.WriteLine("<input type=text name=foo value=foovalue>");
            p.outputStream.WriteLine("<input type=submit name=bar value=barvalue>");
            p.outputStream.WriteLine("</form>");
        }
 
        public override void handlePOSTRequest(HttpProcessor p, StreamReader inputData) {
            Console.WriteLine("POST request: {0}", p.http_url);
            string data = inputData.ReadToEnd();
 
            p.outputStream.WriteLine("<html><body><h1>test server</h1>");
            p.outputStream.WriteLine("<a href=/test>return</a><p>");
            p.outputStream.WriteLine("postbody: <pre>{0}</pre>", data);
            
 
        }
    }

После того как простой обработчик запросов готов, необходимо создать экземпляр сервера на определённом порту и запустить основной листенер сервера в потоке.

 HttpServer httpServer = new MyHttpServer(8080);
 Thread thread = new Thread(new ThreadStart(httpServer.listen));
 thread.Start();

После компиляции и запуска этого примера, можно в веб-браузере открыть адрес http://localhost:8080 и увидеть простую html-страничку, сгенерированную нашим сервером. Теперь можно вкратце рассмотреть, что же происходит внутри.

Данный веб-сервер разбивается на две составляющие. Класс HttpServer открывает TcpListener на входящем порту и в цикле обрабатывает входящие TCP-запросы, используя AcceptTcpClient(). Это первый этап обработки входящих TCP-соединений. Входящий запрос поступает на наш порт и аксептящий процесс создает новую пару портов для сервера, по которой начнётся общение с клиентом. Эта новая пара портов и есть наша TcpClient-сессия. Такая операция позволяет освободить основной порт, на котором сервер продолжит принимать новые входящие подключения. Как видно из нижеприведённого кода, листенер каждый раз возвращает новый TcpClient, HttpServer создает новый HttpProcessor и запускается новый поток для его обработки. Этот класс также содержит абстрактные методы, которые наш унаследованный класс должен иметь, чтобы генерировать ответ.

  public abstract class HttpServer {
 
        protected int port;
        TcpListener listener;
        bool is_active = true;
       
        public HttpServer(int port) {
            this.port = port;
        }
 
        public void listen() {
            listener = new TcpListener(port);
            listener.Start();
            while (is_active) {                
                TcpClient s = listener.AcceptTcpClient();
                HttpProcessor processor = new HttpProcessor(s, this);
                Thread thread = new Thread(new ThreadStart(processor.process));
                thread.Start();
                Thread.Sleep(1);
            }
        }
 
        public abstract void handleGETRequest(HttpProcessor p);
        public abstract void handlePOSTRequest(HttpProcessor p, StreamReader inputData);
    }

В этом месте новое клиент-серверное TCP-соединение передаётся в HttpProcessor в его собственный поток. HttpProcessor разбирает HTTP-заголовки и передаёт управление абстрактному методу. Давайте рассмотрим несколько моментов обработки заголовков HTTP. В первой строке запроса обычно выглядит так:

GET /myurl HTTP/1.0 

После установки входного и выходного потоков в process(), наши HttpProcessor вызывает parseRequest(), где приведённый выше HTTP-запрос принимается и парсится.

public void parseRequest() {
    String request = inputStream.ReadLine();
    string[] tokens = request.Split(' ');
    if (tokens.Length != 3) {
        throw new Exception("invalid http request line");
    }
    http_method = tokens[0].ToUpper();
    http_url = tokens[1];
    http_protocol_versionstring = tokens[2];
 
    Console.WriteLine("starting: " + request);
} 

Строка HTTP-запроса всегда состоит из трёх частей, поэтому Мы просто вызываем string.Split(), чтобы разбить её на три части. Следующим шагом необходимо получить и проанализировать HTTP-заголовки от клиента. Каждая строка заголовка содержит пару в виде КЛЮЧ:Значение. Пустая строка означает конец заголовков. Код в readHeaders будет следующим:

 public void readHeaders() {
    Console.WriteLine("readHeaders()");
    String line;
    while ((line = inputStream.ReadLine()) != null) {
        if (line.Equals("")) {
            Console.WriteLine("got headers");
            return;
        }
                
        int separator = line.IndexOf(':');
        if (separator == -1) {
            throw new Exception("invalid http header line: " + line);
        }
        String name = line.Substring(0, separator);
        int pos = separator + 1;
        while ((pos < line.Length) && (line[pos] == ' ')) {
            pos++; // strip any spaces
        }
                    
        string value = line.Substring(pos, line.Length - pos);
        Console.WriteLine("header: {0}:{1}",name,value);
        httpHeaders[name] = value;
    }
}

В каждой строке ищем разделитель двоеточие (:), выцепляя строку до двоеточия как имя, а после - как значение. Когда мы доходим до пустой строки заголовка, возвращаем управление.

На данный момент этого достаточно, чтобы обрабатывать простые запросы GET и POST, поэтому мы передаём управление следующему обработчику. В случае POST-запроса есть некоторые тонкости, которые необходимо знать, чтобы принять данные. Один из заголовков запроса содержит длинну передаваемых данных (content-length). Чтобы наш handlePOSTRequest мог обрабатывать данные присланные методом POST, ему необходимо разрешить запрашивать количество байт (указанное в content-length) из потока, чтобы они не застряли во входном потоке. В данном примере сервера эта обработка сделана топорно, однако правильно было бы сперва считать все post-данные в MemoryStream, потом уже передавать управление POST-обработчику. Тем не менее по ряду причин и это решение не является идеальным. Во-первых, пост-данные могут быть большими. Это может быть файл, которые аплоадит клиент, и буферизация его в памяти может быть неэффективна или даже не возможна. В идеале необходимо создать некий тип поток-имитатора, который бы имел ограничение на длину content-length, а в остальном работал бы как обычный поток. Это позволит POST-обработчику извлекать данные из потока без накладных расходов при буфферизации в памяти. Однако это уже потребует намного больше кода. Пост-запросы во встроенных HTTP-серверах используются не так часто, поэтому мы просто ограничили входные данные 10 мегабайтами.

Еще одно упрощение нашего сервера заключается в типе возвращаемых клиенту данных (content-type). В протоколе HTTP, сервер всегда посылает браузеру MIME-тип (MIME-Type) данных, которые тот ожидает. В методе writeSuccess() видно, что наш сервер всегда посылает один и тот же тип text/html. Если необходимо возвращать другие типы содержимого, то потребуется доработка этого метода чтобы он посылал клиенту соответствующий тип перед началом отправки содержимого.

Заключение

Этот пример веб-сервера включает в себя обработку только самых основных возможностей протокола HTTP/1.0. Более продвинутые возможности HTTP включают в себя сжатие данных, сохранение сессий, частичная загрузка и многое другое. Тем не менее даже такой простой веб-сервера позволяет генерировать странички, которые понимаются современными браузерами.

Скачать SimpleHttpServer - 12 кб




Основные разделы сайта


 

Реклама Узнайте для чего нужен SSL-сертификат в статье на сайте Joomix.org.