Пишем свой вэб сервер на C#

Перевод статьи на http://www.codeproject.com/Articles/452052/Build-Your-Own-Web-Server

Скачать: [Загрузка не найдена] Скачать: [Загрузка не найдена]

simpleWebServer

Вступление

Мы будем писать свой простой WebServer, который сможет отправлять ответы на наиболее известные методы HTTP (GET и POST).

HTTP протокол
HTTP протокол связи между сервером и клиентом. Использует в качестве транспорта для отправки и получения протокол TCP/IP.
У HTTP есть несколько методов, но мы реализуем только два из них: это GET и POST.

GET
Что происходит, когда мы нажимаем в адресной строке браузера Enter? (Обычно мы не указываем порт через знак :, хотя для TCP/IP он необходим, так как он задан для HTTP по умолчанию, это порт 80. Если он отличен от 80 то его необходимо указывать через двоеточия)

GET / HTTP/1.1\r\n
Host: atasoyweb.net\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/14.0.1\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: tr-tr,tr;q=0.8,en-us;q=0.5,en;q=0.3\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n\r\n

 

Это GET запрос, который посылается от нашего браузера к серверу через TCP/IP. Это означает, что браузер запрашивает у сервер содержимое «/» корневой папки «atasoyweb.net».
Мы сами (или браузер) можем добавлять дополнительные заголовки. Но самый упрощенный вариант запроса выглядит так:

GET / HTTP/1.1\r\n
Host: atasoyweb.net\r\n\r\n

 

POST
POST запросы аналогичны запросам GET. В GET запрос, переменные добавляются к URL-адресов используется знак ?. А в POST запрос, переменные добавляются в в тело HTTP-запроса, а размер передаваемых данных в байтах указывается в заголовке Content-Length:

POST /index.html HTTP/1.1\r\n
Host: atasoyweb.net\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20100101 Firefox/15.0.1\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: tr-tr,tr;q=0.8,en-us;q=0.5,en;q=0.3\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
Referer: http://atasoyweb.net/\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 35\r\n\r\n
variable1=value1&variable2=value2

 

Упрощенный вариант POST запроса:

POST /index.html HTTP/1.1\r\n
Host: atasoyweb.net\r\n
Content-Length: 35\r\n\r\n
variable1=value1&variable2=value2

 

Ответы

Когда запрос поступает на сервер он обрабатывается и возвращается ответ с кодом состояния:

HTTP/1.1 200 OK\r\n
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)\r\n
Content-Length: {content_length}\r\n
Connection: close\r\n
Content-Type: text/html; charset=UTF-8\r\n\r\n
the content of which length is equal to {content_length}

 

Это заголовок ответа. «200 OK» означает, что все в порядке, запрашиваемый контент, будет возвращен. Есть много кодов состояний, но мы будем использовать только 200, 501 и 404:

  • «501 Not Implemented — «Не реализовано 501»: метод не реализован. Мы реализуем только GET и POST. Для всех других методов будем выдавать это состояние
  • «404 Not Found»: запрашиваемый контент не найден.

Типы контента

Сервер в своем ответе должен указать тип передаваемого контента. Есть много типов контента, их также называют MIME типы (многоцелевые расширения почты интернета).
Вот типы содержимого, которые мы будем использовать в нашем сервере: (Вы можете изменить код и добавить новые типы):

  • text/html
  • text/xml
  • text/plain
  • text/css
  • image/png
  • image/gif
  • image/jpg
  • image/jpeg
  • application/zip

Если сервер укажет неверный тип контента, то содержимое будет неправильно истолковано. Например, если сервер посылает простой текст с помощью «image/png», то клиент попытается показать текст в виде изображения.

Многопоточность

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

Реализация

Теперь мы готовы к реализации нашего простого вэб сервера. Сперва наперво определим переменные которые будем использовать:

        public bool running = false; //Запущено ли?

        private int timeout = 8; // Лиммт времени на приём данных.
        private Encoding charEncoder = Encoding.UTF8; // Кодировка
        private Socket serverSocket; // Нащ сокет
        private string contentPath; // Корневая папка для контента

        // Поодерживаемый контент нашим сервером
        // Вы можете добавить больше
        // Смотреть здесь: http://www.webmaster-toolkit.com/mime-types.shtml
        private Dictionary<string, string> extensions = new Dictionary<string, string>()
        { 
            //{ "extension", "content type" }
            { "htm", "text/html" },
            { "html", "text/html" },
            { "xml", "text/xml" },
            { "txt", "text/plain" },
            { "css", "text/css" },
            { "png", "image/png" },
            { "gif", "image/gif" },
            { "jpg", "image/jpg" },
            { "jpeg", "image/jpeg" },
            { "zip", "application/zip"}
        };

 

Метод Start, запускающий наш сервер

        public bool start(IPAddress ipAddress, int port, int maxNOfCon, string contentPath)
        {
            if (running) return false; // Если уже запущено, то выходим

            try
            {
                // tcp/ip сокет (ipv4)
                serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                serverSocket.Bind(new IPEndPoint(ipAddress, port));
                serverSocket.Listen(maxNOfCon);
                serverSocket.ReceiveTimeout = timeout;
                serverSocket.SendTimeout = timeout;
                running = true;
                this.contentPath = contentPath;
            }
            catch { return false; }

            // Наш поток ждет новые подключения и создает новые потоки.
            Thread requestListenerT = new Thread(() =>
            {
                while (running)
                {
                    Socket clientSocket;
                    try
                    {
                        clientSocket = serverSocket.Accept();
                        // Создаем новый поток для нового клиента и продолжаем слушать сокет.
                        Thread requestHandler = new Thread(() =>
                        {
                            clientSocket.ReceiveTimeout = timeout;
                            clientSocket.SendTimeout = timeout;
                            try { handleTheRequest(clientSocket); }
                            catch
                            {
                                try { clientSocket.Close(); } catch { }
                            }
                        });
                        requestHandler.Start();
                    }
                    catch{}
                }
            });
            requestListenerT.Start();

            return true;
        }

 

Метод Stop, останавливающий сервер

public void stop()
{
    if (running)
    {
        running = false;
        try { serverSocket.Close(); }
        catch { }
        serverSocket = null;
    }
}

 

Наиболее важная часть кода:

        private void handleTheRequest(Socket clientSocket)
        {
            byte[] buffer = new byte[10240]; // 10 kb, just in case
            int receivedBCount = clientSocket.Receive(buffer); // Получаем запрос
            string strReceived = charEncoder.GetString(buffer, 0, receivedBCount);

            // Парсим запрос
            string httpMethod = strReceived.Substring(0, strReceived.IndexOf(" "));

            int start = strReceived.IndexOf(httpMethod) + httpMethod.Length + 1;
            int length = strReceived.LastIndexOf("HTTP") - start - 1;
            string requestedUrl = strReceived.Substring(start, length);

            string requestedFile;
            if (httpMethod.Equals("GET") || httpMethod.Equals("POST"))
                requestedFile = requestedUrl.Split('?')[0];
            else // Вы можете реализовать другие методы
            {
                notImplemented(clientSocket);
                return;
            }

            requestedFile = requestedFile.Replace("/", "\\").Replace("\\..", ""); // Not to go back
            start = requestedFile.LastIndexOf('.') + 1;
            if (start > 0)
            {
                length = requestedFile.Length - start;
                string extension = requestedFile.Substring(start, length);
                if (extensions.ContainsKey(extension)) // Мы поддерживаем это расширение?
                    if (File.Exists(contentPath + requestedFile)) // Если да
                        // ТО отсылаем запрашиваемы контент:
                        sendOkResponse(clientSocket, File.ReadAllBytes(contentPath + requestedFile), extensions[extension]);
                    else
                        notFound(clientSocket); // Мы не поддерживаем данный контент.
            }
            else
            {
                // Если файл не указан, пробуем послать index.html
                // Вы можете добавить больше(например "default.html")
                if (requestedFile.Substring(length - 1, 1) != "\\")
                    requestedFile += "\\";
                if (File.Exists(contentPath + requestedFile + "index.htm"))
                    sendOkResponse(clientSocket, File.ReadAllBytes(contentPath + requestedFile + "\\index.htm"), "text/html");
                else if (File.Exists(contentPath + requestedFile + "index.html"))
                    sendOkResponse(clientSocket, File.ReadAllBytes(contentPath + requestedFile + "\\index.html"), "text/html");
                else
                    notFound(clientSocket);
            }
        }

 

Ответы на коды статусов:

private void notImplemented(Socket clientSocket)
{

    sendResponse(clientSocket, "<html><head><meta 
        http-equiv=\"Content-Type\" content=\"text/html; 
        charset=utf-8\">
        </head><body><h2>Atasoy Simple Web 
        Server</h2><div>501 - Method Not 
        Implemented</div></body></html>", 
        "501 Not Implemented", "text/html");

}

private void notFound(Socket clientSocket)
{

    sendResponse(clientSocket, "<html><head><meta 
        http-equiv=\"Content-Type\" content=\"text/html; 
        charset=utf-8\"></head><body><h2>Atasoy Simple Web 
        Server</h2><div>404 - Not 
        Found</div></body></html>", 
        "404 Not Found", "text/html");
}

private void sendOkResponse(Socket clientSocket, byte[] bContent, string contentType)
{
    sendResponse(clientSocket, bContent, "200 OK", contentType);
}

 

Метод, который будет отправлять ответы клиентам:

// For strings
private void sendResponse(Socket clientSocket, string strContent, string responseCode,
                          string contentType)
{
    byte[] bContent = charEncoder.GetBytes(strContent);
    sendResponse(clientSocket, bContent, responseCode, contentType);
}

// For byte arrays
private void sendResponse(Socket clientSocket, byte[] bContent, string responseCode,
                          string contentType)
{
    try
    {
        byte[] bHeader = charEncoder.GetBytes(
                            "HTTP/1.1 " + responseCode + "\r\n"
                          + "Server: Atasoy Simple Web Server\r\n"
                          + "Content-Length: " + bContent.Length.ToString() + "\r\n"
                          + "Connection: close\r\n"
                          + "Content-Type: " + contentType + "\r\n\r\n");
        clientSocket.Send(bHeader);
        clientSocket.Send(bContent);
        clientSocket.Close();
    }
    catch { }
}

 

Как использовать:

// Создаем
Server server = new Server();
// Запускаем
server.start(ipAddress, port, maxconnections, contentpath);
// останавливаем
server.stop();

 

One Reply to “Пишем свой вэб сервер на C#”

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

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