В этой статье мы погрузимся в мир веб-протоколов, в частности HTTP, а также рассмотрим связь HTTP с TCP и UDP. Попутно, в качестве примеров, мы приведем примеры реализации на Python клиентов и серверов, использующих эти протоколы.
HTTP (Hypertext Transfer Protocol) является основой мира Интернета. Когда вы посещаете какой-нибудь сайт, ваш браузер посылает HTTP-запросы на сервер, который в ответ выдает веб-страницы. Это похоже на разговор между браузером и сервером. Например, если послать запрос с текстом “Joe”, то сервер может ответить “Hi there, Joe”.
Ниже приведен базовый пример работы такого HTTP-сервера в Python3. Мы будем использовать встроенные модули http.client и http.server. Это простой пример, поэтому он не строго соответствует стандартам HTTP и не рекомендуется для использования в производстве, поскольку реализует только базовые проверки безопасности. Но для наших целей это подойдет.
import http.server import http.client PORT = 8001 class Store: def __init__(self): self.requestBody = '' self.responseBody = '' store = Store() class MyHTTPRequestHandler(http.server.BaseHTTPRequestHandler): def do_POST(self): content_length = int(self.headers['Content-Length']) content = self.rfile.read(content_length).decode('utf-8') store.requestBody = content response_content = f'Hi there, {content}'.encode('utf-8') self.send_response(200) self.send_header('Content-Type', 'text/plain') self.send_header('Content-Length', len(response_content)) self.end_headers() self.wfile.write(response_content) def server_listen(): with http.server.HTTPServer(('localhost', PORT), MyHTTPRequestHandler) as server: print(f'HTTP server listening on {PORT}') http_request() def http_request(): conn = http.client.HTTPConnection('localhost', PORT) content = 'Joe' headers = { 'Content-Type': 'text/plain', 'Content-Length': str(len(content)) } conn.request('POST', '/greet', body=content, headers=headers) response = conn.getresponse() data = response.read().decode('utf-8') store.responseBody = data close_connections() def close_connections(): server.server_close() print(store.requestBody) # Joe print(store.responseBody) # Hi there, Joe server_listen()
TCP-соединение
Теперь познакомимся с TCP (Transmission Control Protocol). TCP является базовым протоколом, на котором построен HTTP, как видно из официальных спецификаций последнего. И хотя я уже об этом сказал, я попрошу вас сделать вид, что вы этого еще не знаете. Давайте докажем, что HTTP базируется на TCP!
В Python имеются встроенные модули threading и socket, которые помогают нам создавать TCP-клиенты и серверы.
Следует знать, что TCP отличается от HTTP по нескольким параметрам:
- Запросы не могут отправляться спонтанно. Сначала должно быть установлено соединение.
- После установки соединения сообщения могут передаваться в обоих направлениях.
- Установленное соединение должно быть закрыто вручную.
Ниже приведена простая реализация TCP-клиента, который желает получить приветствие от сервера:
import socket import threading PORT = 8001 MAXIMUM_BYTES_RECEIVABLE = 1024 class Store: def __init__(self): self.requestBody = '' self.responseBody = '' store = Store() def handle_client(client_socket): request_data = client_socket.recv(MAXIMUM_BYTES_RECEIVABLE).decode('utf-8') store.requestBody = request_data response_data = f'Hi there, {request_data}'.encode('utf-8') client_socket.send(response_data) response = client_socket.recv(MAXIMUM_BYTES_RECEIVABLE).decode('utf-8') store.responseBody = response client_socket.close() def server_listen(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # When the socket type is socket.SOCK_STREAM the protocol being used is TCP by default. server.bind(('0.0.0.0', PORT)) server.listen(5) print(f'TCP server listening on {PORT}') while True: client_socket, addr = server.accept() # Blocks execution and waits for an incoming connection. client_handler = threading.Thread(target=handle_client, args=(client_socket,)) client_handler.start() def http_request(): client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('localhost', PORT)) content = 'Joe' client.send(content.encode('utf-8')) client.shutdown(socket.SHUT_WR) response = client.recv(MAXIMUM_BYTES_RECEIVABLE).decode('utf-8') store.responseBody = response client.close() close_connections() def close_connections(): server.close() print(store.requestBody) # Joe print(store.responseBody) # Hi there, Joe if __name__ == '__main__': server_listen() http_request()
Теперь представьте, что у вас есть TCP-прокси, который может передавать сообщения между HTTP-клиентами и серверами. Даже если этот прокси не понимает HTTP, он все равно может передавать запросы и ответы.
Вот как будет выглядеть его реализация:
import socket import http.client import threading HTTP_PORT = 8001 PROXY_TCP_PORT = 8002 MAXIMUM_BYTES_RECEIVABLE = 1024 class Store: def __init__(self): self.requestBody = '' self.responseBody = '' store = Store() def proxy_handler(local_socket): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as remote_socket: remote_socket.connect(('localhost', HTTP_PORT)) def forward(src, dst): while True: data = src.recv(MAXIMUM_BYTES_RECEIVABLE) if not data: break dst.send(data) threading.Thread(target=forward, args=(local_socket, remote_socket)).start() threading.Thread(target=forward, args=(remote_socket, local_socket)).start() def http_server_handler(client_socket): data = client_socket.recv(MAXIMUM_BYTES_RECEIVABLE).decode('utf-8') store.requestBody = data response_data = f'Hi there, {data}'.encode('utf-8') client_socket.send(response_data) response = client_socket.recv(MAXIMUM_BYTES_RECEIVABLE).decode('utf-8') store.responseBody = response def http_server_listen(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: server.bind(('0.0.0.0', HTTP_PORT) server.listen(5) print(f'HTTP server listening on {HTTP_PORT}') while True: client_socket, addr = server.accept() threading.Thread(target=http_server_handler, args=(client_socket,)).start() def proxy_listen(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_server: proxy_server.bind(('0.0.0.0', PROXY_TCP_PORT)) proxy_server.listen(5) print(f'TCP proxy listening on {PROXY_TCP_PORT}') while True: local_socket, addr = proxy_server.accept() threading.Thread(target=proxy_handler, args=(local_socket,)).start() def http_request(): conn = http.client.HTTPConnection('localhost', PROXY_TCP_PORT) content = 'Joe' headers = { 'Content-Type': 'text/plain', 'Content-Length': str(len(content)) } conn.request('POST', '/greet', body=content, headers=headers) response = conn.getresponse() data = response.read().decode('utf-8') close_connections() def close_connections(): http_server_listen_thread.join() proxy_listen_thread.join() print(store.requestBody) # Joe print(store.responseBody) # Hi there, Joe if __name__ == '__main__': http_server_listen_thread = threading.Thread(target=http_server_listen) proxy_listen_thread = threading.Thread(target=proxy_listen) http_server_listen_thread.start() http_server_listen_thread.join() proxy_listen_thread.start() http_request()
Как уже говорилось, хотя TCP-прокси-сервер не знает, что такое HTTP, запросы и ответы полностью проходят через него.
Понимание особенностей TCP
Прежде чем мы продолжим, несколько фактов о TCP:
- Он надежен, т.е. обеспечивает подтверждение сообщений, их повторную передачу и тайминг при необходимости.
- Он гарантирует, что данные поступают в правильном порядке.
Неудивительно, что TCP так распространен, но… Вы же знали, что будет какое-то “но”, верно?
TCP может быть несколько тяжеловат. Чтобы сокетное соединение могло разрешить отправку данных, требуется установить три пакета. В мире HTTP это означает, что для выполнения параллельных запросов HTTP/1.1 требуется несколько TCP-соединений, что может потребовать значительных ресурсов.
HTTP/2 пытается улучшить эту ситуацию, обрабатывая параллельные запросы через одно соединение. Однако при этом возникают проблемы. Когда один пакет задерживается или приходит не по порядку, это приводит к остановке всех запросов.
А теперь представьте, что есть альтернатива TCP, позволяющая параллельные HTTP-сообщения без этих последствий. Звучит неплохо, не так ли? Эта альтернатива – UDP (User Datagram Protocol).
UDP-соединение
Начнем с того, чем UDP отличается от TCP:
- Здесь нет понятия соединения. Вы отправляете данные и надеетесь, что кто-то их получит.
- Вы можете передавать только небольшие фрагменты данных, которые не обязательно представляют собой целое сообщение (подробнее об этом можно почитать в статье Википедии), а встроенные разделители отсутствуют, если они не включены в явном виде.
- В результате создание даже базового механизма запрос/ответ становится более сложным (но все же возможным).
Давайте рассмотрим пример UDP-клиента, который хочет взаимодействовать с сервером. На этот раз мы определим наш сокет как SOCK_DGRAM:
import socket PORT = 8001 EOS = b'\0' # End of stream MAXIMUM_BYTES_RECEIVABLE = 1024 class Store: def __init__(self): self.requestBody = '' self.responseBody = '' store = Store() def slice_but_last(data, encoding='utf-8'): return data[:-1].decode(encoding) def server_listen(): sender = None with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server: server.bind(('0.0.0.0', PORT)) print(f'UDP server listening on {PORT}') while True: chunk, addr = server.recvfrom(MAXIMUM_BYTES_RECEIVABLE) sender = addr if sender is None else sender store.requestBody += slice_but_last(chunk) if chunk[-1:] == EOS: response_data = f'Hi there, {store.requestBody}'.encode('utf-8') + EOS server.sendto(response_data, sender) # Note: You can choose to close the server here if needed break def http_request(): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client: content = 'Joe'.encode('utf-8') + EOS client.sendto(content, ('localhost', PORT)) response_data, _ = client.recvfrom(MAXIMUM_BYTES_RECEIVABLE) store.responseBody = slice_but_last(response_data) close_connections() def close_connections(): print(store.requestBody) # Joe print(store.responseBody) # Hi there, Joe if __name__ == '__main__': server_listen() http_request()
Итак, учитывая, что у нас есть парсер HTTP (http-parser, например), вот как можно реализовать HTTP-решение через UDP:
import socket from http_parser.parser import HttpParser PORT = 8001 CRLF = '\r\n' MAXIMUM_BYTES_RECEIVABLE = 1024 class Store: def __init__(self): self.requestBody = '' self.responseBody = '' store = Store() def server_listen(): parser = HttpParser() def on_body(data): store.requestBody += data def on_message_complete(): content = f'Hi there, {store.requestBody}' response = f'HTTP/1.1 200 OK{CRLF}' \ f'Content-Type: text/plain{CRLF}' \ f'Content-Length: {len(content)}{CRLF}' \ f'{CRLF}' \ f'{content}' server.sendto(response.encode('utf-8'), sender) parser.on_body = on_body parser.on_message_complete = on_message_complete sender = None with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server: server.bind(('0.0.0.0', PORT)) print(f'UDP server listening on {PORT}') while True: chunk, sender = server.recvfrom(MAXIMUM_BYTES_RECEIVABLE) parser.execute(chunk) def http_request(): parser = HttpParser() def on_body(data): store.responseBody += data def on_message_complete(): close_connections() parser.on_body = on_body parser.on_message_complete = on_message_complete content = 'Joe' request = f'POST /greet HTTP/1.1{CRLF}' \ f'Content-Type: text/plain{CRLF}' \ f'Content-Length: {len(content)}{CRLF}' \ f'{CRLF}' \ f'{content}' client.sendto(request.encode('utf-8'), ('localhost', PORT)) def close_connections(): server.close() client.close() print(store.requestBody) # Joe print(store.responseBody) # Hi there, Joe if __name__ == '__main__': client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_listen() http_request()
Выглядит неплохо. У нас есть полноценная реализация с использованием UDP. Но пока не стоит слишком радоваться. UDP имеет ряд существенных недостатков:
- Ненадежность. Вы не можете быть уверены, что ваше сообщение дойдет до адресата.
- Пакеты приходят не по порядку, поэтому нельзя гарантировать порядок следования сообщений.
Возникновение QUIC и HTTP/3
Для устранения недостатков UDP был создан новый протокол – QUIC . Он построен на основе UDP и использует “умные” алгоритмы для его реализации. Отличительные особенности QUIC:
- Надежность
- Обеспечение упорядоченной доставки пакетов
- Легкость
Это приводит нас прямо к HTTP/3, который все еще является относительно новым и экспериментальным. В нем используется QUIC для устранения проблем, возникших в HTTP/2. В HTTP/3 нет соединений, поэтому сессии не влияют друг на друга.
HTTP/3 – перспективное направление развития веб-протоколов, использующее сильные стороны QUIC и UDP.
Хотя встроенная поддержка протокола QUIC отсутствует, можно воспользоваться модулем aioquic, который поддерживает реализацию как QUIC, так и HTTP/3.
Пример с использованием протокола QUIC
Рассмотрим простой пример сервера, использующего QUIC:
import asyncio import ssl from aioquic.asyncio import connect, connect_udp, Connection, serve from aioquic.asyncio.protocol import BaseProtocol, DatagramProtocol from aioquic.asyncio.protocol.stream import DataReceived class HTTPServerProtocol(BaseProtocol): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) async def data_received(self, data): await super().data_received(data) if isinstance(self._quic, Connection): for stream_id, buffer in self._quic._events[DataReceived]: data = buffer.read() response = f'HTTP/1.1 200 OK\r\nContent-Length: {len(data)}\r\n\r\n{data.decode("utf-8")}' self._quic.send_stream_data(stream_id, response.encode('utf-8')) async def main(): loop = asyncio.get_event_loop() # Create QUIC server context quic_server = await loop.create_server(HTTPServerProtocol, 'localhost', 8001) async with quic_server: await quic_server.serve_forever() if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main())
А это – клиент:
import asyncio import ssl from aioquic.asyncio import connect, connect_udp, Connection from aioquic.asyncio.protocol import BaseProtocol class HTTPClientProtocol(BaseProtocol): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.connected_event = asyncio.Event() def quic_event_received(self, event): super().quic_event_received(event) if event.matches('connected'): self.connected_event.set() async def request(self, path, data=None): stream_id = self._quic.get_next_available_stream_id() self._quic.send_stream_data(stream_id, data) await self.connected_event.wait() response = await self._quic.receive_data(stream_id) return response async def main(): loop = asyncio.get_event_loop() # Create QUIC client context quic = connect('localhost', 8001) async with quic as protocol: client_protocol = HTTPClientProtocol(quic, protocol._session_id, None) await client_protocol.connected_event.wait() data = 'Hello, Joe!' response = await client_protocol.request('/greet', data.encode('utf-8')) print(response.decode('utf-8')) if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main())
Пример с использованием протокола HTTP/3
А чтобы вы получили полное представление, приведем пример с использованием протокола HTTP/3 (с помощью модуля aioquic).
Сервер:
import asyncio from aioquic.asyncio.protocol import connect, connect_udp, serve, QuicProtocol from aioquic.asyncio.protocol.stream import DataReceived from h11 import Response, Connection from h11._events import Data class HTTP3ServerProtocol(QuicProtocol): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.conn = Connection() def quic_event_received(self, event): super().quic_event_received(event) if event.matches('handshake_completed'): self.conn.initiate_upgrade_for_http2() async def data_received(self, data): await super().data_received(data) if isinstance(self._quic, QuicProtocol): for stream_id, buffer in self._quic._events[DataReceived]: data = buffer.read() response = Response(status_code=200, headers=[('content-length', str(len(data)))], content=data) data = self.conn.send(response) self._quic.transmit_data(stream_id, data) async def main(): loop = asyncio.get_event_loop() # Create QUIC server context quic_server = await loop.create_server(HTTP3ServerProtocol, 'localhost', 8001) async with quic_server: await quic_server.serve_forever() if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main())
И клиент:
import asyncio from aioquic.asyncio.protocol import connect, connect_udp, QuicProtocol from h11 import Request, Response, Connection from h11._events import Data class HTTP3ClientProtocol(QuicProtocol): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.conn = Connection() async def request(self, path, data=None): stream_id = self._quic.get_next_available_stream_id() request = Request(method='POST', target=path, headers=[('content-length', str(len(data)))] if data else []) data = self.conn.send(request) self._quic.transmit_data(stream_id, data) while True: event = self.conn.next_event() if isinstance(event, Data): self._quic.transmit_data(stream_id, event.data) elif event == h11.EndOfMessage(): break response = await self._quic.receive_data(stream_id) return response async def main(): loop = asyncio.get_event_loop() # Create QUIC client context quic = connect('localhost', 8001) async with quic as protocol: client_protocol = HTTP3ClientProtocol(quic, protocol._session_id, None) data = 'Hello, Joe!' response = await client_protocol.request('/greet', data.encode('utf-8')) print(response.content.decode('utf-8')) if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main())
Итоги
Такая эволюция ставит вопрос о том, сможем ли мы в будущем распрощаться с TCP, но это уже тема для другой статьи и, возможно, для будущего.
На этом мы завершаем наше путешествие по HTTP, TCP и UDP в Python3! Тема может показаться сложной, но под поверхностью каждого посещаемого вами сайта скрывается увлекательный мир веб-коммуникаций, с которым стоит познакомиться.
Перевод статьи «Introduction to HTTP in Python3».