| Главная | Журнал | Форум | 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 кб |
Основные разделы сайта
|
|
|