Обучающие Серии

Подпишись на обновления блогa, чтобы не пропустить следующий пост!

Дополняемый список способов запустить веб-сервер на Python:

This blog is sponsored by - an open source solution for multi-cluster Kubernetes monitoring.

Robusta is based on Prometheus and uses webhooks to add context to each alert. For example, CrashLoopBackOffs arrive in your Slack with relevant logs, so you don't need to open the terminal and run kubectl logs.

Make your Slack alerts rock by installing Robusta - Kubernetes monitoring that just works.

Модуль socket и прямые руки

Берем модуль socket из стандартной библиотеки, создаем серверный сокет, принимаем входящие подключения, вычитываем и обрабатываем запросы и отправляем ответы. Все своими руками. Подробнее можно почитать тут.

Синхронный TCP сервер

Возможна обработка только одного клиента в один момент времени, весь код выполяется в одном процессе и одном (главном) потоке.

тыц чтобы смотреть код
# python3

import socket
import sys
import time

def run_server(port=53210):
  serv_sock = create_serv_sock(port)
  cid = 0
  while True:
    client_sock = accept_client_conn(serv_sock, cid)
    serve_client(client_sock, cid)
    cid += 1

def serve_client(client_sock, cid):
  request = read_request(client_sock)
  if request is None:
    print(f'Client #{cid} unexpectedly disconnected')
  else:
    response = handle_request(request)
    write_response(client_sock, response, cid)


def create_serv_sock(serv_port):
  serv_sock = socket.socket(socket.AF_INET,
                            socket.SOCK_STREAM,
                            proto=0)
  serv_sock.bind(('', serv_port))
  serv_sock.listen()
  return serv_sock

def accept_client_conn(serv_sock, cid):
    client_sock, client_addr = serv_sock.accept()
    print(f'Client #{cid} connected '
          f'{client_addr[0]}:{client_addr[1]}')
    return client_sock

def read_request(client_sock, delimiter=b'!'):
  request = bytearray()
  try:
    while True:
      chunk = client_sock.recv(4)
      if not chunk:
        # Клиент преждевременно отключился.
        return None

      request += chunk
      if delimiter in request:
        return request

  except ConnectionResetError:
    # Соединение было неожиданно разорвано.
    return None
  except:
    raise

def handle_request(request):
  time.sleep(5)
  return request[::-1]

def write_response(client_sock, response, cid):
  client_sock.sendall(response)
  client_sock.close()
  print(f'Client #{cid} has been served')


if __name__ == '__main__':
  run_server(port=int(sys.argv[1]))

Многопоточный TCP сервер

Очевидный недостаток синхронного подхода - отсутствие какой бы то ни было конкурентной обработки запросов. Берем модуль threading и начинаем обрабатывать каждый запрос в отдельном потоке.

тыц чтобы смотреть код
# python3

import socket
import sys
import time
import threading

def run_server(port=53210):
  serv_sock = create_serv_sock(port)
  cid = 0
  while True:
    client_sock = accept_client_conn(serv_sock, cid)
    t = threading.Thread(target=serve_client,
                         args=(client_sock, cid))
    t.start()
    cid += 1

def serve_client(client_sock, cid):
  request = read_request(client_sock)
  if request is None:
    print(f'Client #{cid} unexpectedly disconnected')
  else:
    response = handle_request(request)
    write_response(client_sock, response, cid)

def create_serv_sock(serv_port):
  serv_sock = socket.socket(socket.AF_INET,
                            socket.SOCK_STREAM,
                            proto=0)
  serv_sock.bind(('', serv_port))
  serv_sock.listen()
  return serv_sock

def accept_client_conn(serv_sock, cid):
  client_sock, client_addr = serv_sock.accept()
  print(f'Client #{cid} connected '
        f'{client_addr[0]}:{client_addr[1]}')
  return client_sock

def read_request(client_sock, delimiter=b'!'):
  request = bytearray()
  try:
    while True:
      chunk = client_sock.recv(4)
      if not chunk:
        # Клиент преждевременно отключился.
        return None

      request += chunk
      if delimiter in request:
        return request

  except ConnectionResetError:
    # Соединение было неожиданно разорвано.
    return None
  except:
    raise

def handle_request(request):
  time.sleep(5)
  return request[::-1]

def write_response(client_sock, response, cid):
  client_sock.sendall(response)
  client_sock.close()
  print(f'Client #{cid} has been served')


if __name__ == '__main__':
  run_server(port=int(sys.argv[1]))

Пул потоков-обработчиков

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

# TODO: implement me

Многопроцессный TCP сервер

Альтернативный (и исторически более ранний) взгляд на обработку - каждый запрос в отдельном процессе. Берем низкоуровневый os.fork() и клонируем главный процесс сервера на каждый входящий запрос.

тыц чтобы смотреть код
# python3

import os
import socket
import sys
import time

def run_server(port=53210):
  serv_sock = create_serv_sock(port)
  active_children = set()
  cid = 0
  while True:
    client_sock = accept_client_conn(serv_sock, cid)
    child_pid = serve_client(client_sock, cid)
    active_children.add(child_pid)
    reap_children(active_children)
    cid += 1

def serve_client(client_sock, cid):
  child_pid = os.fork()
  if child_pid:
    # Родительский процесс, не делаем ничего
    client_sock.close()
    return child_pid

  # Дочерний процесс:
  #  - читаем запрос
  #  - обрабатываем
  #  - записываем ответ
  #  - закрываем сокет
  #  - завершаем процесс (os._exit())
  request = read_request(client_sock)
  if request is None:
    print(f'Client #{cid} unexpectedly disconnected')
  else:
    response = handle_request(request)
    write_response(client_sock, response, cid)
  os._exit(0)

def reap_children(active_children):
  for child_pid in active_children.copy():
    child_pid, _ = os.waitpid(child_pid, os.WNOHANG)
    if child_pid:
      active_children.discard(child_pid)

def create_serv_sock(serv_port):
  serv_sock = socket.socket(socket.AF_INET,
                            socket.SOCK_STREAM,
                            proto=0)
  serv_sock.bind(('', serv_port))
  serv_sock.listen()
  return serv_sock

def accept_client_conn(serv_sock, cid):
  client_sock, client_addr = serv_sock.accept()
  print(f'Client #{cid} connected '
        f'{client_addr[0]}:{client_addr[1]}')
  return client_sock

def read_request(client_sock, delimiter=b'!'):
  request = bytearray()
  try:
    while True:
      chunk = client_sock.recv(4)
      if not chunk:
        # Клиент преждевременно отключился.
        return None

      request += chunk
      if delimiter in request:
        return request

  except ConnectionResetError:
    # Соединение было неожиданно разорвано.
    return None
  except:
    raise

def handle_request(request):
  time.sleep(5)
  return request[::-1]

def write_response(client_sock, response, cid):
  client_sock.sendall(response)
  client_sock.close()
  print(f'Client #{cid} has been served')


if __name__ == '__main__':
  run_server(port=int(sys.argv[1]))

Модуль socketserver

Модуль стандартной библиотеки socketserver предназначен для сокращения рутинных действий при написании серверов на Python. Фактически, он предоставляет те же возможности, что и сервера в разделе Модуль socket и прямые руки, но... Просто сравните количество кода.

Класс TCPServer

Базовый, но не абстрактный класс для создания TCP сервера. Аналогичен синхронному TCP серверу. Только один клиент в один момент времени. Предоставляет функциональность для запуска сервера, приема входящих соединений и минимальной обработки ошибок. Обработка же запросов вынесена в класс BaseRequestHandler. Для создания обработчика необходимо отнаследоваться от BaseRequestHandler и определить как минимум метод handle().

# python3

import sys
from socketserver import BaseRequestHandler, TCPServer

class MyTCPHandler(BaseRequestHandler):
  def handle(self):
    data = self.request.recv(1024).strip()
    self.request.sendall(data[::-1])


if __name__ == '__main__':
  host = 'localhost'
  port = int(sys.argv[1])

  with TCPServer((host, port), MyTCPHandler) as srv:
    srv.serve_forever()

ThreadingMixIn + TCPServer

С синхронным TCPServer мы опять не можем обрабатывать входящие запросы конкурентно. Берем ThreadingMixIn и получаем многопоточный TCP сервер:

# python3

from socketserver import BaseRequestHandler, ThreadingMixIn, TCPServer

class MyTCPHandler(BaseRequestHandler):
  def handle(self):
    data = self.request.recv(1024)
    self.request.sendall(data[::-1])

# Важная строчка! Эквивалент socketserver.ThreadingTCPServer
# https://github.com/python/cpython/blob/43fc3bb7cf0278735eb0010d7b3043775a120cb5/Lib/socketserver.py#L682
class ThreadedTCPServer(ThreadingMixIn, TCPServer): pass


if __name__ == '__main__':
  host = 'localhost'
  port = int(sys.argv[1])

  with ThreadedTCPServer((host, port), MyTCPHandler) as srv:
    srv.serve_forever()

ForkingMixIn + TCPServer

Аналогично многопоточному серверу можно создать многопроцессный сервер, используя ForkingMixIn.

# Эквивалент socketserver.ForkingTCPServer
# https://github.com/python/cpython/blob/43fc3bb7cf0278735eb0010d7b3043775a120cb5/Lib/socketserver.py#L679
class ForkedTCPServer(ForkingMixIn, TCPServer): pass

Поточная обработка

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

# python3

import sys
from socketserver import StreamRequestHandler, TCPServer

class MyTCPHandler(StreamRequestHandler):
  def handle(self):
    while True:
      chunk = self.rfile.readline()
      if not chunk:
        break
      self.wfile.write(chunk[::-1])


if __name__ == '__main__':
  host = 'localhost'
  port = int(sys.argv[1])

  with TCPServer((host, port), MyTCPHandler) as srv:
    srv.serve_forever()

Модуль asyncio

Синхронная обработка запросов - непозволительная роскошь неэффективная неэффективность. Процессы и потоки - ограниченная конкурентная обработка, подходящая для десятков или сотен одновременных (и быстрых) запросов. Но что делать, если запросов тысячи? Или если каждое соединение может длиться минуты или даже часы (привет, WebSocket-ы)? Всех спасет asyncio - модуль стандартной библиотеки для написания высокопроизводительных сетевых приложений на основе асинхронного ввода-вывода. В частности, asyncio предоставляет реализацию асинхронного TCP сервера:

тыц чтобы смотреть код
# python3

import asyncio
import sys

counter = 0

async def run_server(host, port):
  server = await asyncio.start_server(serve_client, host, port)
  await server.serve_forever()

async def serve_client(reader, writer):
  global counter
  cid = counter
  counter += 1
  print(f'Client #{cid} connected')

  request = await read_request(reader)
  if request is None:
    print(f'Client #{cid} unexpectedly disconnected')
  else:
    response = await handle_request(request)
    await write_response(writer, response, cid)

async def read_request(reader, delimiter=b'!'):
  request = bytearray()
  while True:
    chunk = await reader.read(4)
    if not chunk:
      # Клиент преждевременно отключился.
      break

    request += chunk
    if delimiter in request:
      return request

  return None

async def handle_request(request):
  await asyncio.sleep(5)
  return request[::-1]

async def write_response(writer, response, cid):
  writer.write(response)
  await writer.drain()
  writer.close()
  print(f'Client #{cid} has been served')

if __name__ == '__main__':
  asyncio.run(run_server('127.0.0.1', int(sys.argv[1])))

Модуль http.server

Когда сетевого и транспортного уровня не достаточно и хочется работать на 7 небе уровне модели ISO OSI, поможет модуль стандартной библиотеки http.server. Модуль определяет классы для реализации HTTP серверов. В их числе HTTPServer, являющийся наследником socketserver.TCPServer, и ThreadingHTTPServer на основе ThreadingMixIn.

Этот модуль не для боевого применения! Его использование может привести к вызову произвольного кода на сервере!

В то же время, это удобная и полезная штука, позволяющая с помощью Python и ничего более, запускать HTTP сервер на локальной машине, например, чтобы раздавать статику.

python -m http.server 8000 --bind 127.0.0.1

Пример кастомизации HTTP сервера: отдаем специальный Content-Type заголовок для *.wasm файлов:

# python3

import sys
import http.server
from socketserver import TCPServer

class Handler(http.server.SimpleHTTPRequestHandler):
  pass

Handler.extensions_map['.wasm'] = 'application/wasm'

if __name__ == '__main__':
  host = 'localhost'
  port = int(sys.argv[1])

  with TCPServer((host, port), Handler) as httpd:
    httpd.serve_forever()

Модуль wsgiref

Модуль http.server хоть и предоставляет более высокоуровневую чем TCPServer абстракцию, все же с трудом может быть использован для написания кастомных веб-приложений напрямую. HTTP запросы необходимо парсить и передавать клиентскому приложению уже высокоуровневые примитивы (например, HTTP заголовки удобно представлять в виде dict). Аналогично, получаемые от приложения объекты, представляющие собой HTTP ответы, необходимо сериализовывать перед тем, как отправлять их по сети клиентам. Эта логика может быть непосредственно частью приложения. Но в таком случае ее придется дублировать при написании каждого следующего веб-приложения. Если же единожды договориться о формате взаимодействия веб-сервера и приложения, можно иметь общий интерфейс для всех клиентских приложений, занимающихся обработкой HTTP запросов. Тогда приложения будут отвечать непосредственно за обработку, а между приложением и веб-сервером должен располагаться слой, отвечающий за совместимость.

Web Server Gateway Interface (WSGI) - это стандартный (для Python) интерфейс для взаимодействия веб-сервера и питоно-приложения. Стандартная библиотека предоставляет эталонную реализацию WSGI. В частности, WSGI-совместимый HTTP сервер wsgiref.simple_server на основе уже знакомого нам http.server.


# python3

import sys
from wsgiref.simple_server import make_server

def my_app(environ, start_response):
  status = '200 OK'
  headers = [('Content-type', 'text/plain; charset=utf-8')]
  start_response(status, headers)

  return [b'Make code, not war!']


if __name__ == '__main__':
  host = 'localhost'
  port = int(sys.argv[1])

  with make_server(host, port, my_app) as srv:
    srv.serve_forever()
Обучающие Серии

Подпишись на обновления блогa, чтобы не пропустить следующий пост!