Пишем свой веб-сервер на python: сокеты

Сравнение

В данном разделе, посвященном сравнению веб-серверов для приложений Python, речь пойдет о некоторых популярных доступных серверах. Цель данного раздела – дать читателю четкое представление о серверах и помочь подобрать соответствующий требованиям приложения сервер. В связи с огромным количеством веб-серверов, которое невозможно охватить в рамках одной статьи, далее будут описаны только «выдающиеся» серверы (т.е., наиболее популярные, наиболее надежные, или же обладающие более мощными функциями по сравнению с остальными машинами).

Примечание: рекомендуется относиться с осторожностью к предвзятым и обманчивым тестам, не стремящимся отразить условия реальной среды производства. К сожалению, подобные статьи не помогут в выборе веб-сервера для производства, а, напротив, только собьют с толку

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

Улучшаем интерфейс нашего сервиса

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

Вместо id задачи мы вернем полный URI, через который будет осуществляться выполнение всех действий с задачей. Для этого мы напишем маленькую функцию-хелпер, которая будет генерировать «публичную» версию задачи, отправляемую клиенту:

Все что мы делаем здесь это берем задачу из нашей базы данных и создаем новую задачу в которой все поля идентичны, за исключением поля , которое заменено полем , сгенерированным функцией предоставляемой Flask.

Когда мы возвращаем список задач мы прогоняем все задачи через эту функцию, прежде чем отослать клиенту:

Теперь клиент получает вот такой список задач:

Применив эту технику к остальным функциям мы сможем гарантировать, что клиент всегда получит URI, вместо id.

HTTP GET¶

To add support for an HTTP method in a request handler class,
implement the method , replacing with the
name of the HTTP method (e.g., , ,
etc.). For consistency, the request handler methods take no
arguments. All of the parameters for the request are parsed by
and stored as instance attributes of
the request instance.

This example request handler illustrates how to return a response to the
client, and some of the local attributes that can be useful in building the
response.

http_server_GET.py

from http.server import BaseHTTPRequestHandler
from urllib import parse


class GetHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        parsed_path = parse.urlparse(self.path)
        message_parts = 
            'CLIENT VALUES:',
            'client_address={} ({})'.format(
                self.client_address,
                self.address_string()),
            'command={}'.format(self.command),
            'path={}'.format(self.path),
            'real path={}'.format(parsed_path.path),
            'query={}'.format(parsed_path.query),
            'request_version={}'.format(self.request_version),
            '',
            'SERVER VALUES:',
            'server_version={}'.format(self.server_version),
            'sys_version={}'.format(self.sys_version),
            'protocol_version={}'.format(self.protocol_version),
            '',
            'HEADERS RECEIVED:',
        
        for name, value in sorted(self.headers.items()):
            message_parts.append(
                '{}={}'.format(name, value.rstrip())
            )
        message_parts.append('')
        message = '\r\n'.join(message_parts)
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()
        self.wfile.write(message.encode('utf-8'))


if __name__ == '__main__'
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), GetHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

The message text is assembled and then written to , the
file handle wrapping the response socket. Each response needs a
response code, set via . If an error code is used
(404, 501, etc.), an appropriate default error message is included in
the header, or a message can be passed with the error code.

To run the request handler in a server, pass it to the constructor of
, as in the processing portion of the
sample script.

Then start the server:

$ python3 http_server_GET.py

Starting server, use <Ctrl-C> to stop

In a separate terminal, use to access it:

$ curl -v -i http://127.0.0.1:8080/?foo=bar

*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /?foo=bar HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf-8
Server: BaseHTTP/0.6 Python/3.5.2
Date: Thu, 06 Oct 2016 20:44:11 GMT

CLIENT VALUES:
client_address=('127.0.0.1', 52934) (127.0.0.1)
command=GET
path=/?foo=bar
real path=/
query=foo=bar
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept=*/*
Host=127.0.0.1:8080
User-Agent=curl/7.43.0
* Connection #0 to host 127.0.0.1 left intact

Возможные улучшения

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

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

Запрос , который возвращает список задач, может быть расширен несколькими способами. Для начала это запрос может иметь опциональные агрументы, такие как количество задач на страницу. Другой путь сделать функцию более удобной это добавить критерии фильтрации. Например клиент может запросить только выполненые задачии или задачи, заголовок которых начинается с определенной буквы. Все эти элементы могут быть добавлены в URL как аргументы.

HTTP POST¶

Supporting POST requests is a little more work, because the base class
does not parse the form data automatically. The module
provides the class which knows how to parse the
form, if it is given the correct inputs.

http_server_POST.py

import cgi
from http.server import BaseHTTPRequestHandler
import io


class PostHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        # Parse the form data posted
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={
                'REQUEST_METHOD' 'POST',
                'CONTENT_TYPE' self.headers'Content-Type'],
            }
        )

        # Begin the response
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()

        out = io.TextIOWrapper(
            self.wfile,
            encoding='utf-8',
            line_buffering=False,
            write_through=True,
        )

        out.write('Client: {}\n'.format(self.client_address))
        out.write('User-agent: {}\n'.format(
            self.headers'user-agent']))
        out.write('Path: {}\n'.format(self.path))
        out.write('Form data:\n')

        # Echo back information about what was posted in the form
        for field in form.keys():
            field_item = formfield
            if field_item.filename
                # The field contains an uploaded file
                file_data = field_item.file.read()
                file_len = len(file_data)
                del file_data
                out.write(
                    '\tUploaded {} as {!r} ({} bytes)\n'.format(
                        field, field_item.filename, file_len)
                )
            else
                # Regular form value
                out.write('\t{}={}\n'.format(
                    field, formfield.value))

        # Disconnect our encoding wrapper from the underlying
        # buffer so that deleting the wrapper doesn't close
        # the socket, which is still being used by the server.
        out.detach()


if __name__ == '__main__'
    from http.server import HTTPServer
    server = HTTPServer(('localhost', 8080), PostHandler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

Run the server in one window:

$ python3 http_server_POST.py

Starting server, use <Ctrl-C> to stop

The arguments to can include form data to be posted to
the server by using the option. The last argument, , posts the contents of the file
to illustrate reading file data from the
form.

Проектируем простой веб-сервис

При проектировании веб-сервиса или API нужно определить ресурсы, которые будут доступны и запросы, с помощью которых эти данные будут доступны, согласно правил REST.

Допустим мы хотим написать приложение To Do List и мы должны спроектировать веб-сервис для него. Первое что мы должны сделать, это придумать кореневой URL для доступа к этому сервису. Например мы могли бы придумать в качестве корневого URL что-то типа:

Здесь я решил включить в URL имя приложения и версию API. Добавление имени приложения в URL это хороший способ разделить между собой сервисы запущенные на одном сервере. Добавление версии API в URL может помочь, если вы захотите сделать обновление в будущем и внедрить в новой версии несовместимые функции и не хотите ломать работающие приложения которые работают на старом API.

Следующим шагом мы должны выбрать ресурсы, которые будут доступны через наш сервис. У нас очень простое приложение, у нас есть только задачи, поэтому нашими ресурсами могут быть только задачи из нашего ToDo листа.

Для доступа к ресурсам будем использовать следующие методы HTTP:

Метод HTTP URI Действие
GET http:///todo/api/v1.0/tasks Получить список задач
GET http:///todo/api/v1.0/tasks/ Получить задачу
POST http:///todo/api/v1.0/tasks Создать новую задачу
PUT http:///todo/api/v1.0/tasks/ Обновить существующую задачу
DELETE http:///todo/api/v1.0/tasks/ Удалить задачу

Наша задача будет иметь следующие поля:

  • id: уникальный идентификатор задачи. Тип Numeric.
  • title: Краткое описание задачи. Тип String.
  • description: подробное описание задачи. Тип Text.
  • done: отметка о выполнении. Тип Boolean.

На этом мы заканчиваем часть посвященную дизайну нашего сервиса. Осталось только реализовать это!

What Is Web Server

Overview

  • A web server is actually a network application, running on some machine, listening on some port.
  • Web server is a computer where web contents are stored.
  • A web server serves web pages to clients across the internet or an intranet .
  • It hosts the pages, scripts, programs and multimedia files and serve them using HTTP, a protocol designed to send files to web browsers.
  • Apache web server, IIS web server, Nginx web server, Light Speed web server etc are the common examples of web servers.

Types

There are two types of web servers –

  • Dedicated web servers : In this, one web server is dedicated to a single user.This is suitable for websites with more web traffics.
  • Shared web servers : In this, a web server is assigned to many clients and it shared among all the clients.

Защита RESTful веб-сервиса

Вы думали мы уже закончили? Конечно, мы закончили с функциональностью нашего сервиса, но у нас есть проблема. Наш сервис открыт для всех, а это не очень хорошо.

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

Многие руководства для начинающих игнорируют безопасность и заканчиваются здесь. По-моему это серьезная проблема, которая всегда должна быть решена.

Простой путь защитить наш веб-сервис это пускать клиентов после авторизации по логину и паролю. В обычном веб-приложении вы должны сделать форму логина, которая отправляет данные авторизации, сервер обрабатывает их и делает новую сессию, а браузер пользователя получает куки с идентификатором сессии. К сожаление здесь мы такое сделать не можем, stateless — одно из правил построения REST веб-сервисов и мы должны просить клиентов отправлять свои регистрационные данные при каждом запросе.

С REST мы всегда стараемся придерживаться протокола HTTP настолько, насколько сможем. Сейчас нам нужно реализовать аутентификацию пользователя в контексте HTTP, который предоставляет нам 2 варианта — Basic и Digest.

Существует маленькое расширение Flask написанное вашим покорным слугой. Давайте установим Flask-HTTPAuth:

Теперь скажем нашего веб-сервису отдавать данные только пользователю с логином и паролем . Для начала настроим Basic HTTP authentication как показано ниже:

Функция будет по имени пользователя возвращать пароль. В более сложных системах такая функцию должна будет лезть в базу, но для одного пользователя это не обязательно.

Функция будет использоваться чтобы отправить ошибку авторизации, при неправильных данных. Так же как мы поступили с другими ошибками мы должны настроить функцию на отправку JSON, вместо HTML.

После настройки системы аутентификаци, осталось только добавить декоратор для всех функций, которые должны быть защищены. Например:

Если мы попробуем запросить эту функцию с помощью мы получим примерно следующее:

Для того, чтобы вызвать эту функцию, мы должны подтвердить наши полномочия:

Расширение с аутентификацией дает нам свободу выбирать какие функции будут в общем доступе, а какие защищены.

Для защиты регистрационной информации наш веб-сервис должен быть доступен через HTTP Secure server ( …) который шифрует траффик между клиентом и сервером и предотвращает получение конфиденциальной информаци третьей стороной.

К сожалению веб-браузеры имеют дурную привычку показывать страшное диалоговое окно, когда запрос возвращается с ошибкой 401. Это происходит даже для фоновых запросов, так что если бы мы реализовали клиента для веб-браузера, нам пришлось бы прыгать через обручи, чтобы не давать браузеру показывать свои окна.

Простой путь обмануть браузер — возвращать любой другой код, вместо 401. Любимая всеми альтернатива это код 403, который означает ошибку «Forbidden». Хоть это достаточно близкая по смыслу ошибка, это нарушает стандарт HTTP, так что это неправильно. В частности будет хорошим решением не использовать веб-браузер в качестве клиентского приложения. Но в случаях, когда сервер и клиент разрабатываются совместно это спасает от многих неприятностей. Чтобы провернуть этот трюк нам нужно просто заменить код ошибки с 401 на 403:

В клиентском приложении нужно тоже отлавливать ошибку 403.

Асинхронность

Как объяснялось ранее, requests полностью синхронен. Он блокирует приложение в ожидании ответа сервера, замедляя работу программы. Создание HTTP-запросов в потоках является одним из решений, но потоки имеют свои собственные накладные расходы, и это подразумевает параллелизм, который не всегда каждый рад видеть в программе.

Начиная с версии 3.5, Python предлагает асинхронность внутри своего ядра, используя aiohttp. Библиотека aiohttp предоставляет асинхронный HTTP-клиент, построенный поверх asyncio. Эта библиотека позволяет отправлять запросы последовательно, но не дожидаясь первого ответа, прежде чем отправлять новый. В отличие от конвейерной передачи HTTP, aiohttp отправляет запросы по нескольким соединениям параллельно, избегая проблемы, описанной ранее.

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

import aiohttp
import asyncio

async def get(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return response

loop = asyncio.get_event_loop()

coroutines = [get("http://example.com") for _ in range(8)]

results = loop.run_until_complete(asyncio.gather(*coroutines))

print("Results: %s" % results)

Все эти решения (с использованием Session, thread, futures или asyncio) предлагают разные подходы к ускорению работы HTTP-клиентов. Но какая между ними разница с точки зрения производительности?

Threading and Forking¶

is a simple subclass of
, and does not use multiple threads or
processes to handle requests. To add threading or forking, create a
new class using the appropriate mix-in from .

http_server_threads.py

from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading


class Handler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        self.end_headers()
        message = threading.currentThread().getName()
        self.wfile.write(message.encode('utf-8'))
        self.wfile.write(b'\n')


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Handle requests in a separate thread."""


if __name__ == '__main__'
    server = ThreadedHTTPServer(('localhost', 8080), Handler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

Run the server in the same way as the other examples.

$ python3 http_server_threads.py

Starting server, use <Ctrl-C> to stop

Each time the server receives a request, it starts a new thread or
process to handle it:

$ curl http://127.0.0.1:8080/

Thread-1

$ curl http://127.0.0.1:8080/

Thread-2

$ curl http://127.0.0.1:8080/

Thread-3

Что такое REST?

  • Клиент-Сервер: Должно быть разделение между сервером, который предлагает сервис и клиентом, который использует ее.
  • Stateless: Каждый запрос от клиента должен содержать всю информацию, необходимую серверу для выполнения запроса. Другими словами, сервер не обязан сохранять информацию о состоянии клиента.
  • Кэширование: В каждом запросе клиента должно явно содержаться указание о возможности кэширования ответа и получения ответа из существующего кэша.
  • Уровневая система: Клиент может взаимодействовать не напрямую с сервером, а с произвольным количеством промежуточных узлов. При этом клиент может не знать о существовании промежуточных узлов, за исключением случаев передачи конфиденциальной информации.
  • Унификация: Унифицированный программный интерфейс сервера.
  • Код по запросу: Сервера могут поставлять исполняемый код или скрипты для выполнения их на стороне клиентов.
Добавить комментарий

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