blob: 8eb67b8c0637adc0c2473438fb00f12bfb779835 [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 struct
from typing import Optional
from dubbo.compression import Compressor, Decompressor
from dubbo.protocol.triple.exceptions import RpcError
"""
gRPC Message Format Diagram (HTTP/2 Data Frame):
+----------------------+-------------------------+------------------+
| HTTP Header | gRPC Header | Business Data |
+----------------------+-------------------------+------------------+
| (variable length) | compressed-flag (1 byte)| data (variable) |
| | message length (4 byte) | |
+----------------------+-------------------------+------------------+
"""
__all__ = ["TriEncoder", "TriDecoder"]
HEADER: str = "HEADER"
PAYLOAD: str = "PAYLOAD"
# About HEADER
HEADER_LENGTH: int = 5
COMPRESSED_FLAG_MASK: int = 1
RESERVED_MASK = 0xFE
DEFAULT_MAX_MESSAGE_SIZE: int = 4194304 # 4MB
class TriEncoder:
"""
This class is responsible for encoding the gRPC message format, which is composed of a header and payload.
"""
__slots__ = ["_compressor"]
def __init__(self, compressor: Optional[Compressor]):
"""
Initialize the encoder.
:param compressor: The compression to use for compressing the payload.
:type compressor: Optional[Compressor]
"""
self._compressor = compressor
@property
def compressor(self) -> Optional[Compressor]:
"""
Get the compressor.
:return: The compressor.
:rtype: Optional[Compressor]
"""
return self._compressor
@compressor.setter
def compressor(self, value: Compressor) -> None:
"""
Set the compressor.
:param value: The compressor.
:type value: Compressor
"""
self._compressor = value
def encode(self, message: bytes, compress_flag: int) -> bytes:
"""
Encode the message into the gRPC message format.
:param message: The message to encode.
:type message: bytes
:param compress_flag: The compress flag. 0 for no compression, 1 for compression.
:type compress_flag: int
:return: The encoded message.
:rtype: bytes
"""
# check compress_flag
if compress_flag not in [0, 1]:
raise RpcError(f"compress_flag must be 0 or 1, but got {compress_flag}")
# check message size
if len(message) > DEFAULT_MAX_MESSAGE_SIZE:
raise RpcError(f"Message too large. Allowed maximum size is 4194304 bytes, but got {len(message)} bytes.")
# check compress_flag and compress the payload
if compress_flag == 1:
if not self._compressor:
raise RpcError("compression is required when compress_flag is 1")
message = self._compressor.compress(message)
# Create the gRPC header
# >: big-endian
# B: unsigned char(1 byte) -> compressed_flag
# I: unsigned int(4 bytes) -> message_length
header = struct.pack(">BI", compress_flag, len(message))
return header + message
class TriDecoder:
"""
This class is responsible for decoding the gRPC message format, which is composed of a header and payload.
"""
__slots__ = [
"_accumulate",
"_listener",
"_decompressor",
"_state",
"_required_length",
"_decoding",
"_compressed",
"_closing",
"_closed",
]
def __init__(
self,
listener: "TriDecoder.Listener",
decompressor: Optional[Decompressor],
):
"""
Initialize the decoder.
:param decompressor: The decompressor to use for decompressing the payload.
:type decompressor: Optional[Decompressor]
:param listener: The listener to deliver the decoded payload to when a message is received.
:type listener: TriDecoder.Listener
"""
self._listener = listener
# store data for decoding
self._accumulate = bytearray()
self._decompressor = decompressor
self._state = HEADER
self._required_length = HEADER_LENGTH
# decode state, if True, the decoder is currently processing a message
self._decoding = False
# whether the message is compressed
self._compressed = False
self._closing = False
self._closed = False
def decode(self, data: bytes) -> None:
"""
Process the incoming bytes, decoding the gRPC message and delivering the payload to the listener.
:param data: The data to decode.
:type data: bytes
"""
self._accumulate.extend(data)
self._do_decode()
def close(self) -> None:
"""
Close the decoder and listener.
"""
self._closing = True
self._do_decode()
def _do_decode(self) -> None:
"""
Deliver the accumulated bytes to the listener, processing the header and payload as necessary.
"""
if self._decoding:
return
self._decoding = True
try:
while self._has_enough_bytes():
if self._state == HEADER:
self._process_header()
elif self._state == PAYLOAD:
self._process_payload()
if self._closing:
if not self._closed:
self._closed = True
self._accumulate = None
self._listener.close()
finally:
self._decoding = False
def _has_enough_bytes(self) -> bool:
"""
Check if the accumulated bytes are enough to process the header or payload
:return: True if there are enough bytes, False otherwise.
:rtype: bool
"""
return len(self._accumulate) >= self._required_length
def _process_header(self) -> None:
"""
Processes the GRPC compression header which is composed of the compression flag and the outer frame length.
"""
header_bytes = self._accumulate[: self._required_length]
self._accumulate = self._accumulate[self._required_length :]
# Parse the header
compressed_flag = int(header_bytes[0])
if (compressed_flag & RESERVED_MASK) != 0:
raise RpcError("gRPC frame header malformed: reserved bits not zero")
else:
self._compressed = bool(compressed_flag & COMPRESSED_FLAG_MASK)
self._required_length = int.from_bytes(header_bytes[1:], byteorder="big")
# Continue to process the payload
self._state = PAYLOAD
def _process_payload(self) -> None:
"""
Processes the GRPC message body, which depending on frame header flags may be compressed.
"""
payload_bytes = self._accumulate[: self._required_length]
self._accumulate = self._accumulate[self._required_length :]
if self._compressed:
# Decompress the payload
payload_bytes = self._decompressor.decompress(payload_bytes)
self._listener.on_message(bytes(payload_bytes))
# Done with this frame, begin processing the next header.
self._required_length = HEADER_LENGTH
self._state = HEADER
class Listener(abc.ABC):
@abc.abstractmethod
def on_message(self, message: bytes):
"""
Called when a message is received.
:param message: The message received.
:type message: bytes
"""
raise NotImplementedError()
@abc.abstractmethod
def close(self):
"""
Called when the listener is closed.
"""
raise NotImplementedError()