В этой статье мы погрузимся в мир веб-протоколов, в частности 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'{$content}' # 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 = 'rn'
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 OKrnContent-Length: {len(data)}rnrn{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».
Сообщение Введение в HTTP в Python3 появились сначала на pythonturbo.
Source: pythonturbo.ru