普通文本  |  126行  |  3.69 KB

#!/usr/bin/env python
# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed 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.

"""Simulate network characteristics directly in Python.

Allows running replay without dummynet.
"""

import logging
import platformsettings
import re
import time


TIMER = platformsettings.timer


class ProxyShaperError(Exception):
  """Module catch-all error."""
  pass

class BandwidthValueError(ProxyShaperError):
  """Raised for unexpected dummynet-style bandwidth value."""
  pass


class RateLimitedFile(object):
  """Wrap a file like object with rate limiting.

  TODO(slamm): Simulate slow-start.
      Each RateLimitedFile corresponds to one-direction of a
      bidirectional socket. Slow-start can be added here (algorithm needed).
      Will consider changing this class to take read and write files and
      corresponding bit rates for each.
  """
  BYTES_PER_WRITE = 1460

  def __init__(self, request_counter, f, bps):
    """Initialize a RateLimiter.

    Args:
      request_counter: callable to see how many requests share the limit.
      f: file-like object to wrap.
      bps: an integer of bits per second.
    """
    self.request_counter = request_counter
    self.original_file = f
    self.bps = bps

  def transfer_seconds(self, num_bytes):
    """Seconds to read/write |num_bytes| with |self.bps|."""
    return 8.0 * num_bytes / self.bps

  def write(self, data):
    num_bytes = len(data)
    num_sent_bytes = 0
    while num_sent_bytes < num_bytes:
      num_write_bytes = min(self.BYTES_PER_WRITE, num_bytes - num_sent_bytes)
      num_requests = self.request_counter()
      wait = self.transfer_seconds(num_write_bytes) * num_requests
      logging.debug('write sleep: %0.4fs (%d requests)', wait, num_requests)
      time.sleep(wait)

      self.original_file.write(
          data[num_sent_bytes:num_sent_bytes + num_write_bytes])
      num_sent_bytes += num_write_bytes

  def _read(self, read_func, size):
    start = TIMER()
    data = read_func(size)
    read_seconds = TIMER() - start
    num_bytes = len(data)
    num_requests = self.request_counter()
    wait = self.transfer_seconds(num_bytes) * num_requests - read_seconds
    if wait > 0:
      logging.debug('read sleep: %0.4fs %d requests)', wait, num_requests)
      time.sleep(wait)
    return data

  def readline(self, size=-1):
    return self._read(self.original_file.readline, size)

  def read(self, size=-1):
    return self._read(self.original_file.read, size)

  def __getattr__(self, name):
    """Forward any non-overriden calls."""
    return getattr(self.original_file, name)


def GetBitsPerSecond(bandwidth):
  """Return bits per second represented by dummynet bandwidth option.

  See ipfw/dummynet.c:read_bandwidth for how it is really done.

  Args:
    bandwidth: a dummynet-style bandwidth specification (e.g. "10Kbit/s")
  """
  if bandwidth == '0':
    return 0
  bw_re = r'^(\d+)(?:([KM])?(bit|Byte)/s)?$'
  match = re.match(bw_re, str(bandwidth))
  if not match:
    raise BandwidthValueError('Value, "%s", does not match regex: %s' % (
        bandwidth, bw_re))
  bw = int(match.group(1))
  if match.group(2) == 'K':
    bw *= 1000
  if match.group(2) == 'M':
    bw *= 1000000
  if match.group(3) == 'Byte':
    bw *= 8
  return bw