docs: def read_frames(self):
          '''
          Read frames from the transport and process them. Some transports may
          choose to do this in the background, in several threads, and so on.
          '''
          # It's possible in a concurrent environment that our transport handle
          # has gone away, so handle that cleanly.
          # TODO: Consider moving this block into Translator base class. In many
          # ways it belongs there. One of the problems though is that this is
          # essentially the read loop. Each Transport has different rules for
          # how to kick this off, and in the case of gevent, this is how a
          # blocking call to read from the socket is kicked off.
          if self._transport is None:
              return
  
          # Send a heartbeat (if needed)
          self._channels[0].send_heartbeat()
  
          data = self._transport.read(self._heartbeat)
          current_time = time.time()
  
          if data is None:
              # Wait for 2 heartbeat intervals before giving up. See AMQP 4.2.7:
              # "If a peer detects no incoming traffic (i.e. received octets) for two heartbeat intervals or longer,
              # it should close the connection"
              if self._heartbeat and (current_time-self._last_octet_time > 2*self._heartbeat):
                  msg = 'Heartbeats not received from %s for %d seconds' % (self._host, 2*self._heartbeat)
                  self.transport_closed(msg=msg)
                  raise ConnectionClosed('Connection is closed: ' + msg)
              return
          self._last_octet_time = current_time
          reader = Reader(data)
          p_channels = set()
  
          try:
              for frame in Frame.read_frames(reader):
                  if self._debug > 1:
                      self.logger.debug("READ: %s", frame)
                  self._frames_read += 1
                  ch = self.channel(frame.channel_id)
                  ch.buffer_frame(frame)
                  p_channels.add(ch)
          except Frame.FrameError as e:
              # Frame error in the peer, disconnect
              self.close(reply_code=501,
                         reply_text='frame error from %s : %s' % (
                             self._host, str(e)),
                         class_id=0, method_id=0, disconnect=True)
              raise ConnectionClosed("connection is closed: %s : %s" %
                                     (self._close_info['reply_code'],
                                      self._close_info['reply_text']))
  
          # NOTE: we process channels after buffering unused data in order to
          # preserve the integrity of the input stream in case a channel needs to
          # read input, such as when a channel framing error necessitates the use
          # of the synchronous channel.close method. See `Channel.process_frames`.
          #
          # HACK: read the buffer contents and re-buffer.  Would prefer to pass
          # buffer back, but there's no good way of asking the total size of the
          # buffer, comparing to tell(), and then re-buffering.  There's also no
          # ability to clear the buffer up to the current position. It would be
          # awesome if we could free that memory without a new allocation.
          if reader.tell() < len(data):
              self._transport.buffer(data[reader.tell():])
  
          self._transport.process_channels(p_channels)