Решён
Как написать веб-сервер на Python с нуля?

Дмитрий Смирнов Python
3.1k
7

Хочу разобраться как устроены веб-серверы изнутри. Знаю Python на уровне скриптов и базовый HTTP. Пробовал читать про Flask и FastAPI, но там слишком много магии - хочу понять что реально происходит под капотом.

Задача: написать минимальный веб-сервер на Python без сторонних библиотек, только stdlib. Чтобы он принимал GET-запросы и возвращал HTML-страницу.

С чего начать? Какие модули из стандартной библиотеки использовать - http.server или сокеты напрямую? Есть ли нормальные туториалы именно по написанию с нуля, а не по использованию фреймворков?

Решение
74
Участник • 1 ответ

Два уровня погружения, выбирай по интересу:

Уровень 1 - http.server из stdlib:
Самый быстрый старт. Наследуешься от BaseHTTPRequestHandler, переопределяешь do_GET:

from http.server import HTTPServer, BaseHTTPRequestHandler

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(b'<h1>Hello</h1>')

    def log_message(self, format, *args):
        pass  # глушим логи

if __name__ == '__main__':
    server = HTTPServer(('0.0.0.0', 8080), Handler)
    print('Server running on :8080')
    server.serve_forever()

Запускаешь, открываешь http://localhost:8080 - работает.

Уровень 2 - сырые сокеты:
Понимаешь HTTP как текстовый протокол. Принимаешь TCP-соединение, читаешь строки запроса, пишешь ответ вручную:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8080))
server.listen(5)

while True:
    conn, addr = server.accept()
    data = conn.recv(4096).decode('utf-8')
    # data содержит сырой HTTP-запрос
    response = 'HTTP/1.1 200 OKrnContent-Type: text/htmlrnrn<h1>Hello</h1>'
    conn.sendall(response.encode('utf-8'))
    conn.close()

Для понимания протокола рекомендую второй путь. Когда поймешь что такое rnrn между заголовками и телом, магия фреймворков начнет рассеиваться.

Аватар Дмитрий Смирнов

Отличный пример с сокетами, именно это и искал. А как правильно парсить путь из запроса чтобы разные URL возвращали разный контент?

Аватар Ренат Хафизов

Первая строка сырого запроса выглядит как `GET /path HTTP/1.1`. Делаешь `request_line = data.split('rn')[0]`, потом `method, path, _ = request_line.split(' ')` - и `path` у тебя в кармане. Дальше обычный `if path == '/about':`.

8
Участник • 1 ответ

Если хочешь реально понять HTTP - прочитай RFC 7230 и RFC 7231. Скучно, но после этого любой фреймворк читается как открытая книга.

37
Участник • 1 ответ

Задача понятная, но я бы предложил немного другой путь.

Вместо того чтобы писать сервер с нуля через сокеты, напиши сначала простую WSGI-функцию и запусти ее через wsgiref.simple_server из stdlib. WSGI - это стандартный интерфейс между Python-приложением и веб-сервером, и понимание этого интерфейса даст больше практической пользы чем понимание TCP-уровня:

from wsgiref.simple_server import make_server

def app(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'<h1>Hello from WSGI</h1>']

with make_server('', 8080, app) as httpd:
    httpd.serve_forever()

environ - это словарь с данными запроса (метод, путь, заголовки). Вот что Flask и Django реально получают под капотом.

26
Эксперт • 1 ответ

Вопрос на самом деле про понимание, а не про результат. Для таких случаев хорошо работает подход "напиши игрушечный фреймворк": сначала сырые сокеты, потом добавляешь роутинг, потом парсинг query string, потом шаблоны. В итоге у тебя получается 200 строк кода которые делают 20% того что делает Flask, но ты понимаешь каждую из них.

29
Эксперт • 2 ответа

Asyncio + reader/writer вместо блокирующих сокетов. Иначе сервер будет обслуживать одного клиента за раз и зависать на каждом запросе:

import asyncio

async def handle(reader, writer):
    data = await reader.read(4096)
    response = b'HTTP/1.1 200 OKrnContent-Type: text/htmlrnrn<h1>Async hello</h1>'
    writer.write(response)
    await writer.drain()
    writer.close()

async def main():
    server = await asyncio.start_server(handle, '0.0.0.0', 8080)
    async with server:
        await server.serve_forever()

asyncio.run(main())

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

10
Эксперт • 1 ответ

Книга "Architecture Patterns with Python" от Percival и Gregory. Там есть хорошая глава про то как устроен HTTP-стек. Или Beej's Guide to Network Programming - это Си, но объясняет сокеты лучше любого Python-туториала.

3
Участник • 1 ответ

мне кажется можно через socketserver.TCPServer попробовать, там поменьше кода чем с чистыми сокетами... хотя не уверен что это то что ты ищешь

Написать ответ

Премодерация гостей

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

Будьте вежливы и соблюдайте правила платформы.