wiz 01.02.2011 11:05

PythonВеб-сервер своими руками

Многие уже писали себе «сайты» или планируют ими заняться, но при этом плохо представляют себе что же это такое - сайт. Серия постов посвящена построению с нуля учебного (а не супер-быстрого или супер-фичастого) сервера, на примере которого, постараемся разобрать разные практические вопросы из жизни разработчика. Первая, вступительная, часть публичная т.к. возможно многим будет интересно «как это всё устроено». Код на питоне, но это практически псевдокод на английском, поэтому ни у кого тут сложностей возникнуть не должно. Остальные части скорее всего будут показываться только для участников питоноблога.

Сайт это такой многослойный торт, напичканый самыми разными видами крема кода. Давайте посмотрим, что происходит, когда пользователь набирает в браузере http://example.com/ и зачем.

Протокол HTTP работает поверх другого протокола - TCP, в котором никаких example.com нет, а есть 2.50.203.49, поэтому шаг 0 — используя службы DNS браузер получает IP-адрес хоста. Сервер об этом ничего не знает, поэтому и никакого кода этому этапу не соответствует.

Теперь можно установить соединение до сервера. IP нам сообщили, а порт берётся из схемы или задаётся вручную. Обычно это 80. Зная адрес и порт клиент создаёт сокет и открывает соединение, которое сервер принимает.

Для организации канала необходимо две стороны, два сокета. Клиент будет подключаться, а сервер находиться в состоянии ожидания:
1
2
3
4
5
6
import socket  # Всё, что нам нужно для этого лежит в модуле socket.

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # параметры для создания TCP-сокета
sock.bind(("", 8000)) # слушать на всех адресах, на порту 8000. Чтобы создать сервер на порту ниже 1024 нужен рут.
sock.listen(1) # перевести сокет в режим ожидания входящих соединений
conn, addr = sock.accept() # ожидать установки соединения


(Если любопытно, можно заходить на сервер через telnet или netcat или, собственно, браузером и смотреть что происходит)

Когда клиент делает connect, а сервер accept они получают объект-соединение из которго можно читать и писать. Фактически это пара FIFO каналов.

Канал организован, теперь можно сообщить другой стороне о своих намерениях согласно протоколу HTTP.

Первым делом посылается запрос:
1
GET /some/stuff HTTP/1.1\r\n



Потом идут заголовки:
1
Host: example.com\r\n



Запрос оканчивается пустой строкой:
1
\r\n



Клиент своё дело сделал, дальше в игру вступает сервер. От него потребуется разобрать запрос на сегменты: собственно запрос, заголовки и тело.

 1
2
3
4
5
6
7
8
9
10
11
12
data = conn.recv(1024).split("\r\n")        # считываем запрос и бьём на строки
method, url, proto = data<0>.split(" ", 2) # обрабатываем первую строку

headers = {}
for pos, line in enumerate(data<1:>): # проходим по строкам и заодно запоминаем позицию
if not line.strip(): # пустая строка = конец заголовков, начало тела**
break
key, value = line.split(": ", 1) # разбираем строку с заголовком
headers<key.upper> = value # приводим ключ к "нормальному" виду чтобы обращение было регистронезависимым

# всё остальное - тело** запроса
body = "\r\n".join(data<pos>)</pos></key.upper>



Имея на руках URL, адрес ресурса**, канал для ответа и остальные запчасти мы можем ответить. Ответ сервера клиенту состоит из тех же трёх частей:

1
2
3
4
5
6
conn.send("HTTP/1.0 200 OK\r\n")           # мы не умеем никаких фишечек версии 1.1, поэтому будем сразу честны
conn.send("Server: OwnHands/0.1\r\n") # Поехали заголовки...
conn.send("Content-Type: text/plain\r\n") # разметка нам тут пока не нужна, поэтому говорим клиенту показывать всё как есть
conn.send("\r\n") # Кончились заголовки, всё остальное - тело ответа
conn.send("Hi there!") # привет мир
conn.close() # сбрасываем буфера, закрываем соединение



Устроив такое «короткое замыкание» браузеру и прогулявшись по стеку протоколов обеспечивающих транспорт, мы теперь можем разобрать саму начинку сервера. Тут уже пойдёт варенье вместо крема и мы вольны писать что угодно, не заглядывая в RFC 1945.

Сервер, в том виде, как сейчас обрабатывает всего один запрос и выходит. У такого поведения есть определённая полезность, но Настоящие Сервера так не делают. Продолжим.

Во время и после того, как соединение было принято, обработано и закрыто, с оригинальным сокетом ничего не произошло, он так и оставался в готовности принять новые соединения. И возможно даже уже что-то принял. Код, который будет повторяться завернём в вечный while:

1
2
3
4
while True:
conn, addr = socket.accept()
... # код работы с соединением
conn.close()



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

Итого. От сервера требуется:
создать сокет (socket.socket)
настроить его (bind, listen)
принять соединение (accept)
считать и распарсить запрос
придумать ответ
завернуть ответ в протокол
выдать его и закрыть соединение


Больше всего времени он проводит на этапе 5, и по большому счёту, это вообще не его дело, что там будет происходить. А происходить там может много чего. Например:
Тупо ответ как в примере какой-нибудь захардкоденой фигни. Малоприменимо, но в качестве упражнения сойдёт. Реальное применение - всякие empty_gif; у nginx.
Отдача файла с диска. Не барское это дело. Такими вещами должны заниматься очень сильно оптимизированые сервера типа того же nginx. Но мы всё равно попробуем, заодно разберём вопросы безопасности.
Обратное проксирование (сервер сам делает запрос и передаёт результат клиенту). Тоже работа для nginx, но делается просто, поэтому возьмём всё равно.
Выполнение другого скрипта (CGI). Морально устаревший метод вызова на каждый чих подпроцесса. Как-нибудь потом.
Передача обработчику (в стиле modphp, asp). Конёк Apache - в сервер вкорячивается интерпретатор чего угодно, который на каждый урл запускает скрипты. Попытка починить CGI, который «починить» невозможно т.к. он предназначен для другого.
Обработка запроса сервером приложений. Любимое дело Java серверов. Весь код 5го пункта живёт как часть сервера и запускается только один раз, всасывает всё в кэши, после чего как турбовеник только раскидывает запросы.


Кроме того, можно сразу выделить несколько интересных применений протокола, помимо «сайтиков». Фактичски, сайт это API для употребления человеком. Есть ещё несколько классов задач, которые часто встречаются на практике:
RPC, «удалённый вызов процедур». HTTP, благодаря своей минималистичности очень хорошо подходит в качестве транспорта для реализации службы доступа к удалённым объектам. Немного class-ной питонской магии и whatever.you_wish(to="do") начинает выполнятся на сервере, а может даже и сразу на нескольких.
Обёртка вокруг сложных протоколов. Если есть какая-нибудь хитрая библиотека, которая там как-то сложно работает, а её нужно вызывать из кучи языков и сред то, передав в адресе все необходимые параметры, сервер выполнит всю грязную работу и не надо будет переписывать библиотеку для очередного недоязычка. HTTP-клиенты обычно есть везде.
Виртуальные файловые системы. Используя протокол WebDAV (расширение HTTP) можно подключить в виде сетевого диска что угодно - базу данных, поисковый индекс, список рассылки или форум. Что-то типа кроссплатформенного FUSE.


В следующем заходе приведём код в порядок и сделаем большой задел для реализации возможностей в пункте 5.


Тэги: http ownhands python socket tutorial web велосипед
+ 23 -
Похожие Поделиться

klen 01.02.2011 11:22 #
Отличная статья!
nikebl 01.02.2011 11:50 #
Пока все тривиально, интересно будет понаблюдать за развитием велосипеда. Буду ждать продолжения.
wiz 01.02.2011 11:54 #
Уже пишу (=
knicefire 01.02.2011 13:04 #
Круто!.... Очень хороший и понятный ход изложения, как раз для таких самоучек как я :)
zz 01.02.2011 14:16 #
Спасибо за статью, очень интересно! Ждем продолжения.
le087 01.02.2011 16:50 #
wiz! Спасибо спасибо спасибо. От чистого сердца благодарен. Мне это очень-очень нужно.
wiz 01.02.2011 17:41 #
Добил вторую часть. Осторожно, там вынос мозга. Мало HTTP и много питона.
sol13 02.02.2011 00:31 #
Спасибо большое. хорошая статья, уже и вторую прочел, взял на заметку.