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

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

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

Модуль 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'Hello world!']


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

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

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