- Пишем свой веб-сервер на Python: сокеты
- Пишем свой веб-сервер на Python: процессы, потоки и асинхронный I/O
- Пишем свой веб-сервер на Python: протокол HTTP
- 9001 способ создать веб-сервер на Python
Подпишись на обновления блог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()
- Пишем свой веб-сервер на Python: сокеты
- Пишем свой веб-сервер на Python: процессы, потоки и асинхронный I/O
- Пишем свой веб-сервер на Python: протокол HTTP
- 9001 способ создать веб-сервер на Python
Подпишись на обновления блогa, чтобы не пропустить следующий пост!