blob: a762d0a260bd8c646b801180c4486f72bb2520d0 [file]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
import asyncio
import struct
import time
from typing import Optional
from h2.config import H2Configuration
from h2.connection import H2Connection
from dubbo.constants import common_constants
from dubbo.loggers import loggerFactory
from dubbo.remoting.aio import ConnectionStateListener, EmptyConnectionStateListener, constants as h2_constants
from dubbo.remoting.aio.exceptions import ProtocolError
from dubbo.remoting.aio.http2.stream_handler import StreamServerMultiplexHandler
from dubbo.remoting.aio.http2.controllers import RemoteFlowController
from dubbo.remoting.aio.http2.frames import (
DataFrame,
HeadersFrame,
Http2Frame,
PingFrame,
RstStreamFrame,
UserActionFrames,
WindowUpdateFrame,
)
from dubbo.remoting.aio.http2.registries import Http2FrameType
from dubbo.remoting.aio.http2.stream import Http2Stream
from dubbo.remoting.aio.http2.utils import Http2EventUtils
from dubbo.url import URL
from dubbo.utils import EventHelper, FutureHelper
__all__ = ["AbstractHttp2Protocol", "Http2ClientProtocol", "Http2ServerProtocol"]
_LOGGER = loggerFactory.get_logger()
class AbstractHttp2Protocol(asyncio.Protocol, abc.ABC):
"""
HTTP/2 protocol implementation.
"""
DEFAULT_PING_DATA = struct.pack(">Q", 0) # 8 bytes of 0
__slots__ = [
"_url",
"_loop",
"_h2_connection",
"_transport",
"_flow_controller",
"_stream_handler",
"_last_read",
"_last_write",
]
def __init__(self, url: URL, h2_config: H2Configuration):
self._url = url
self._loop = asyncio.get_running_loop()
# Create the H2 state machine
self._h2_connection = H2Connection(h2_config)
# The transport instance
self._transport: Optional[asyncio.Transport] = None
self._flow_controller: Optional[RemoteFlowController] = None
if self._url.attributes[common_constants.PROTOCOL_KEY] == Http2ServerProtocol:
listener_factory = self._url.attributes[h2_constants.LISTENER_FACTORY_KEY]
self._stream_handler = StreamServerMultiplexHandler(listener_factory)
else:
self._stream_handler = self._url.attributes[h2_constants.STREAM_HANDLER_KEY]
# last time of receiving data
self._last_read = time.time()
# last time of sending data
self._last_write = time.time()
@property
def last_read(self) -> float:
"""
Get the last time of receiving data.
"""
return self._last_read
def _update_last_read(self) -> None:
"""
Update the last time of receiving data.
"""
self._last_read = time.time()
@property
def last_write(self) -> float:
"""
Get the last time of sending data.
"""
return self._last_write
def _update_last_write(self) -> None:
"""
Update the last time of sending data.
"""
self._last_write = time.time()
def connection_made(self, transport: asyncio.Transport):
"""
Called when the connection is first established. We complete the following actions:
1. Save the transport.
2. Initialize the H2 connection.
3. Create and start the follow controller.
4. Initialize the stream handler.
"""
self._transport = transport
self._h2_connection.initiate_connection()
self._flush()
# Create and start the follow controller
self._flow_controller = RemoteFlowController(self._h2_connection, self._transport, self._loop)
# Initialize the stream handler
self._stream_handler.do_init(self._loop, self)
def get_next_stream_id(self, future) -> None:
"""
Create a new stream.(thread-safe)
:param future: The future to set the stream identifier.
"""
def _inner_operation(_future):
stream_id = self._h2_connection.get_next_available_stream_id()
FutureHelper.set_result(_future, stream_id)
self._loop.call_soon_threadsafe(_inner_operation, future)
def send_frame(
self,
frame: UserActionFrames,
stream: Http2Stream,
event: Optional[asyncio.Event] = None,
) -> None:
"""
Send the HTTP/2 frame.(thread-unsafe)
:param frame: The frame to send.
:type frame: UserActionFrames
:param stream: The stream.
:type stream: Http2Stream
:param event: The event to be set after sending the frame.
:type event: Optional[asyncio.Event]
"""
frame_type = frame.frame_type
if frame_type == Http2FrameType.HEADERS:
self._send_headers_frame(frame, stream, event)
elif frame_type == Http2FrameType.DATA:
self._flow_controller.write_data(stream, frame, event)
elif frame_type == Http2FrameType.RST_STREAM:
self._send_reset_frame(frame.stream_id, frame.error_code.value, event)
else:
_LOGGER.warning("Unhandled frame: %s", frame)
def _send_headers_frame(
self,
frame: HeadersFrame,
stream: Http2Stream,
event: Optional[asyncio.Event] = None,
) -> None:
"""
Send the HTTP/2 headers frame.(thread-unsafe)
:param frame: The frame to send.
:type frame: HeadersFrame
:param stream: The stream.
:type stream: Http2Stream
:param event: The event to be set after sending the frame.
"""
if stream.id == -1:
stream.id = self._h2_connection.get_next_available_stream_id()
self._stream_handler.put_stream(stream.id, stream)
self._h2_connection.send_headers(stream.id, frame.headers.to_list(), end_stream=frame.end_stream)
self._flush()
EventHelper.set(event)
def _send_reset_frame(self, stream_id: int, error_code: int, event: Optional[asyncio.Event] = None) -> None:
"""
Send the HTTP/2 reset frame.(thread-unsafe)
:param stream_id: The stream identifier.
:type stream_id: int
:param error_code: The error code.
:type error_code: int
:param event: The event to be set after sending the frame.
:type event: Optional[asyncio.Event]
"""
self._h2_connection.reset_stream(stream_id, error_code)
self._flush()
EventHelper.set(event)
def _send_ping_frame(self, data: bytes = DEFAULT_PING_DATA) -> None:
"""
Send the HTTP/2 ping frame.(thread-unsafe)
:param data: The data to send. The length of the data must be 8 bytes.
:type data: bytes
"""
self._h2_connection.ping(data)
self._flush()
def _flush(self) -> None:
"""
Flush the data to the transport.
"""
outbound_data = self._h2_connection.data_to_send()
if outbound_data != b"":
self._transport.write(outbound_data)
# Update the last write time
self._update_last_write()
def data_received(self, data):
"""
Called when some data is received from the transport.
:param data: The data received.
:type data: bytes
"""
# Update the last read time
self._update_last_read()
# Process the event
events = self._h2_connection.receive_data(data)
try:
for event in events:
frame = Http2EventUtils.convert_to_frame(event)
# If frame is None, there are two possible cases:
# 1. Events that are handled automatically by the H2 library (e.g. RemoteSettingsChanged, PingReceived).
# -> We just need to send it.
# 2. Events that are not implemented or do not require attention. -> We'll ignore it for now.
if frame is not None:
if isinstance(frame, WindowUpdateFrame):
# Because flow control may be at the connection level, it is handled here
self._flow_controller.release_flow_control(frame)
elif isinstance(frame, (HeadersFrame, DataFrame, RstStreamFrame)):
# Handle the frame by the stream handler
self._stream_handler.handle_frame(frame)
else:
# Try handling other frames
self._do_other_frame(frame)
# Flush the data
self._flush()
except Exception as e:
raise ProtocolError("Failed to process the Http/2 event.") from e
def _do_other_frame(self, frame: Http2Frame):
"""
This is a scalable approach to handle other frames. Subclasses can override this method to handle other frames.
:param frame: The frame to handle.
:type frame: Http2Frame
"""
pass
def ack_received_data(self, stream_id: int, ack_length: int) -> None:
"""
Acknowledge the received data.
:param stream_id: The stream identifier.
:type stream_id: int
:param ack_length: The length of the data to acknowledge.
:type ack_length: int
"""
self._h2_connection.acknowledge_received_data(ack_length, stream_id)
self._flush()
def close(self):
"""
Close the connection.
"""
self._h2_connection.close_connection()
self._flush()
self._transport.close()
def connection_lost(self, exc):
"""
Called when the connection is lost.
"""
self._flow_controller.close()
class Http2ClientProtocol(AbstractHttp2Protocol):
"""
HTTP/2 client protocol implementation.
"""
def __init__(
self,
url: URL,
connection_listener: ConnectionStateListener = None,
):
super().__init__(url, H2Configuration(client_side=True, header_encoding="utf-8"))
self._connection_listener = connection_listener or EmptyConnectionStateListener()
# get heartbeat interval -> default 60s
self._heartbeat_interval = url.parameters.get(h2_constants.HEARTBEAT_KEY, h2_constants.DEFAULT_HEARTBEAT)
self._ping_ack_future: Optional[asyncio.Future] = None
self._heartbeat_task: Optional[asyncio.Task] = None
def connection_made(self, transport: asyncio.Transport):
super().connection_made(transport)
# Start the heartbeat task
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
# Notify the connection is established
asyncio.create_task(self._connection_listener.connection_made())
def _do_other_frame(self, frame: Http2Frame):
# Handle the ping frame
if isinstance(frame, PingFrame) and frame.ack:
FutureHelper.set_result(self._ping_ack_future, None)
async def _heartbeat_loop(self):
"""
Heartbeat loop. It is used to check the connection status.
"""
while True:
await asyncio.sleep(self._heartbeat_interval)
# check last read time
now = time.time()
if now - self.last_read < self._heartbeat_interval:
# the connection is normal
continue
# try to send ping frame to check the connection
self._ping_ack_future = asyncio.Future()
self._send_ping_frame()
try:
# wait for the ping ack
await asyncio.wait_for(self._ping_ack_future, timeout=5)
except asyncio.TimeoutError:
# close the connection
self.close()
break
def connection_lost(self, exc):
super().connection_lost(exc)
# Notify the connection is lost
asyncio.create_task(self._connection_listener.connection_lost(exc))
class Http2ServerProtocol(AbstractHttp2Protocol):
"""
HTTP/2 server protocol implementation.
"""
def __init__(self, url: URL):
super().__init__(url, H2Configuration(client_side=False, header_encoding="utf-8"))