curl/tests/http_pipe.py

448 lines
14 KiB
Python
Raw Normal View History

Multiple pipelines and limiting the number of connections. Introducing a number of options to the multi interface that allows for multiple pipelines to the same host, in order to optimize the balance between the penalty for opening new connections and the potential pipelining latency. Two new options for limiting the number of connections: CURLMOPT_MAX_HOST_CONNECTIONS - Limits the number of running connections to the same host. When adding a handle that exceeds this limit, that handle will be put in a pending state until another handle is finished, so we can reuse the connection. CURLMOPT_MAX_TOTAL_CONNECTIONS - Limits the number of connections in total. When adding a handle that exceeds this limit, that handle will be put in a pending state until another handle is finished. The free connection will then be reused, if possible, or closed if the pending handle can't reuse it. Several new options for pipelining: CURLMOPT_MAX_PIPELINE_LENGTH - Limits the pipeling length. If a pipeline is "full" when a connection is to be reused, a new connection will be opened if the CURLMOPT_MAX_xxx_CONNECTIONS limits allow it. If not, the handle will be put in a pending state until a connection is ready (either free or a pipe got shorter). CURLMOPT_CONTENT_LENGTH_PENALTY_SIZE - A pipelined connection will not be reused if it is currently processing a transfer with a content length that is larger than this. CURLMOPT_CHUNK_LENGTH_PENALTY_SIZE - A pipelined connection will not be reused if it is currently processing a chunk larger than this. CURLMOPT_PIPELINING_SITE_BL - A blacklist of hosts that don't allow pipelining. CURLMOPT_PIPELINING_SERVER_BL - A blacklist of server types that don't allow pipelining. See the curl_multi_setopt() man page for details.
2013-02-15 18:50:45 +08:00
#!/usr/bin/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.
#
# Modified by Linus Nielsen Feltzing for inclusion in the libcurl test
# framework
#
import SocketServer
import argparse
import re
import select
import socket
import time
import pprint
import os
INFO_MESSAGE = '''
This is a test server to test the libcurl pipelining functionality.
It is a modified version if Google's HTTP pipelining test server. More
information can be found here:
http://dev.chromium.org/developers/design-documents/network-stack/http-pipelining
Source code can be found here:
http://code.google.com/p/http-pipelining-test/
'''
MAX_REQUEST_SIZE = 1024 # bytes
MIN_POLL_TIME = 0.01 # seconds. Minimum time to poll, in order to prevent
# excessive looping because Python refuses to poll for
# small timeouts.
SEND_BUFFER_TIME = 0.5 # seconds
TIMEOUT = 30 # seconds
class Error(Exception):
pass
class RequestTooLargeError(Error):
pass
class ServeIndexError(Error):
pass
class UnexpectedMethodError(Error):
pass
class RequestParser(object):
"""Parses an input buffer looking for HTTP GET requests."""
global logfile
LOOKING_FOR_GET = 1
READING_HEADERS = 2
HEADER_RE = re.compile('([^:]+):(.*)\n')
REQUEST_RE = re.compile('([^ ]+) ([^ ]+) HTTP/(\d+)\.(\d+)\n')
def __init__(self):
"""Initializer."""
self._buffer = ""
self._pending_headers = {}
self._pending_request = ""
self._state = self.LOOKING_FOR_GET
self._were_all_requests_http_1_1 = True
self._valid_requests = []
def ParseAdditionalData(self, data):
"""Finds HTTP requests in |data|.
Args:
data: (String) Newly received input data from the socket.
Returns:
(List of Tuples)
(String) The request path.
(Map of String to String) The header name and value.
Raises:
RequestTooLargeError: If the request exceeds MAX_REQUEST_SIZE.
UnexpectedMethodError: On a non-GET method.
Error: On a programming error.
"""
logfile = open('log/server.input', 'a')
logfile.write(data)
logfile.close()
self._buffer += data.replace('\r', '')
should_continue_parsing = True
while should_continue_parsing:
if self._state == self.LOOKING_FOR_GET:
should_continue_parsing = self._DoLookForGet()
elif self._state == self.READING_HEADERS:
should_continue_parsing = self._DoReadHeader()
else:
raise Error('Unexpected state: ' + self._state)
if len(self._buffer) > MAX_REQUEST_SIZE:
raise RequestTooLargeError(
'Request is at least %d bytes' % len(self._buffer))
valid_requests = self._valid_requests
self._valid_requests = []
return valid_requests
@property
def were_all_requests_http_1_1(self):
return self._were_all_requests_http_1_1
def _DoLookForGet(self):
"""Tries to parse an HTTTP request line.
Returns:
(Boolean) True if a request was found.
Raises:
UnexpectedMethodError: On a non-GET method.
"""
m = self.REQUEST_RE.match(self._buffer)
if not m:
return False
method, path, http_major, http_minor = m.groups()
if method != 'GET':
raise UnexpectedMethodError('Unexpected method: ' + method)
if path in ['/', '/index.htm', '/index.html']:
raise ServeIndexError()
if http_major != '1' or http_minor != '1':
self._were_all_requests_http_1_1 = False
# print method, path
self._pending_request = path
self._buffer = self._buffer[m.end():]
self._state = self.READING_HEADERS
return True
def _DoReadHeader(self):
"""Tries to parse a HTTP header.
Returns:
(Boolean) True if it found the end of the request or a HTTP header.
"""
if self._buffer.startswith('\n'):
self._buffer = self._buffer[1:]
self._state = self.LOOKING_FOR_GET
self._valid_requests.append((self._pending_request,
self._pending_headers))
self._pending_headers = {}
self._pending_request = ""
return True
m = self.HEADER_RE.match(self._buffer)
if not m:
return False
header = m.group(1).lower()
value = m.group(2).strip().lower()
if header not in self._pending_headers:
self._pending_headers[header] = value
self._buffer = self._buffer[m.end():]
return True
class ResponseBuilder(object):
"""Builds HTTP responses for a list of accumulated requests."""
def __init__(self):
"""Initializer."""
self._max_pipeline_depth = 0
self._requested_paths = []
self._processed_end = False
self._were_all_requests_http_1_1 = True
def QueueRequests(self, requested_paths, were_all_requests_http_1_1):
"""Adds requests to the queue of requests.
Args:
requested_paths: (List of Strings) Requested paths.
"""
self._requested_paths.extend(requested_paths)
self._were_all_requests_http_1_1 = were_all_requests_http_1_1
def Chunkify(self, data, chunksize):
""" Divides a string into chunks
"""
return [hex(chunksize)[2:] + "\r\n" + data[i:i+chunksize] + "\r\n" for i in range(0, len(data), chunksize)]
def BuildResponses(self):
"""Converts the queue of requests into responses.
Returns:
(String) Buffer containing all of the responses.
"""
result = ""
self._max_pipeline_depth = max(self._max_pipeline_depth,
len(self._requested_paths))
for path, headers in self._requested_paths:
if path == '/verifiedserver':
body = "WE ROOLZ: {}\r\n".format(os.getpid());
result += self._BuildResponse(
'200 OK', ['Server: Apache',
'Content-Length: {}'.format(len(body)),
'Cache-Control: no-store'], body)
elif path == '/alphabet.txt':
body = 'abcdefghijklmnopqrstuvwxyz'
result += self._BuildResponse(
'200 OK', ['Server: Apache',
'Content-Length: 26',
'Cache-Control: no-store'], body)
elif path == '/reverse.txt':
body = 'zyxwvutsrqponmlkjihgfedcba'
result += self._BuildResponse(
'200 OK', ['Content-Length: 26', 'Cache-Control: no-store'], body)
elif path == '/chunked.txt':
body = ('7\r\nchunked\r\n'
'8\r\nencoding\r\n'
'2\r\nis\r\n'
'3\r\nfun\r\n'
'0\r\n\r\n')
result += self._BuildResponse(
'200 OK', ['Transfer-Encoding: chunked', 'Cache-Control: no-store'],
body)
elif path == '/cached.txt':
body = 'azbycxdwevfugthsirjqkplomn'
result += self._BuildResponse(
'200 OK', ['Content-Length: 26', 'Cache-Control: max-age=60'], body)
elif path == '/connection_close.txt':
body = 'azbycxdwevfugthsirjqkplomn'
result += self._BuildResponse(
'200 OK', ['Content-Length: 26', 'Cache-Control: max-age=60', 'Connection: close'], body)
self._processed_end = True
elif path == '/1k.txt':
str = '0123456789abcdef'
body = ''.join([str for num in xrange(64)])
result += self._BuildResponse(
'200 OK', ['Server: Apache',
'Content-Length: 1024',
'Cache-Control: max-age=60'], body)
elif path == '/10k.txt':
str = '0123456789abcdef'
body = ''.join([str for num in xrange(640)])
result += self._BuildResponse(
'200 OK', ['Server: Apache',
'Content-Length: 10240',
'Cache-Control: max-age=60'], body)
elif path == '/100k.txt':
str = '0123456789abcdef'
body = ''.join([str for num in xrange(6400)])
result += self._BuildResponse(
'200 OK',
['Server: Apache',
'Content-Length: 102400',
'Cache-Control: max-age=60'],
body)
elif path == '/100k_chunked.txt':
str = '0123456789abcdef'
moo = ''.join([str for num in xrange(6400)])
body = self.Chunkify(moo, 20480)
body.append('0\r\n\r\n')
body = ''.join(body)
result += self._BuildResponse(
'200 OK', ['Transfer-Encoding: chunked', 'Cache-Control: no-store'], body)
elif path == '/stats.txt':
results = {
'max_pipeline_depth': self._max_pipeline_depth,
'were_all_requests_http_1_1': int(self._were_all_requests_http_1_1),
}
body = ','.join(['%s:%s' % (k, v) for k, v in results.items()])
result += self._BuildResponse(
'200 OK',
['Content-Length: %s' % len(body), 'Cache-Control: no-store'], body)
self._processed_end = True
else:
result += self._BuildResponse('404 Not Found', ['Content-Length: 7'], 'Go away')
if self._processed_end:
break
self._requested_paths = []
return result
def WriteError(self, status, error):
"""Returns an HTTP response for the specified error.
Args:
status: (String) Response code and descrtion (e.g. "404 Not Found")
Returns:
(String) Text of HTTP response.
"""
return self._BuildResponse(
status, ['Connection: close', 'Content-Type: text/plain'], error)
@property
def processed_end(self):
return self._processed_end
def _BuildResponse(self, status, headers, body):
"""Builds an HTTP response.
Args:
status: (String) Response code and descrtion (e.g. "200 OK")
headers: (List of Strings) Headers (e.g. "Connection: close")
body: (String) Response body.
Returns:
(String) Text of HTTP response.
"""
return ('HTTP/1.1 %s\r\n'
'%s\r\n'
'\r\n'
'%s' % (status, '\r\n'.join(headers), body))
class PipelineRequestHandler(SocketServer.BaseRequestHandler):
"""Called on an incoming TCP connection."""
def _GetTimeUntilTimeout(self):
return self._start_time + TIMEOUT - time.time()
def _GetTimeUntilNextSend(self):
if not self._last_queued_time:
return TIMEOUT
return self._last_queued_time + SEND_BUFFER_TIME - time.time()
def handle(self):
self._request_parser = RequestParser()
self._response_builder = ResponseBuilder()
self._last_queued_time = 0
self._num_queued = 0
self._num_written = 0
self._send_buffer = ""
self._start_time = time.time()
try:
poller = select.epoll(sizehint=1)
poller.register(self.request.fileno(), select.EPOLLIN)
while not self._response_builder.processed_end or self._send_buffer:
time_left = self._GetTimeUntilTimeout()
time_until_next_send = self._GetTimeUntilNextSend()
max_poll_time = min(time_left, time_until_next_send) + MIN_POLL_TIME
events = None
if max_poll_time > 0:
if self._send_buffer:
poller.modify(self.request.fileno(),
select.EPOLLIN | select.EPOLLOUT)
else:
poller.modify(self.request.fileno(), select.EPOLLIN)
events = poller.poll(timeout=max_poll_time)
if self._GetTimeUntilTimeout() <= 0:
return
if self._GetTimeUntilNextSend() <= 0:
self._send_buffer += self._response_builder.BuildResponses()
self._num_written = self._num_queued
self._last_queued_time = 0
for fd, mode in events:
if mode & select.EPOLLIN:
new_data = self.request.recv(MAX_REQUEST_SIZE, socket.MSG_DONTWAIT)
if not new_data:
return
new_requests = self._request_parser.ParseAdditionalData(new_data)
self._response_builder.QueueRequests(
new_requests, self._request_parser.were_all_requests_http_1_1)
self._num_queued += len(new_requests)
self._last_queued_time = time.time()
elif mode & select.EPOLLOUT:
num_bytes_sent = self.request.send(self._send_buffer[0:4096])
self._send_buffer = self._send_buffer[num_bytes_sent:]
time.sleep(0.05)
else:
return
except RequestTooLargeError as e:
self.request.send(self._response_builder.WriteError(
'413 Request Entity Too Large', e))
raise
except UnexpectedMethodError as e:
self.request.send(self._response_builder.WriteError(
'405 Method Not Allowed', e))
raise
except ServeIndexError:
self.request.send(self._response_builder.WriteError(
'200 OK', INFO_MESSAGE))
except Exception as e:
print e
self.request.close()
class PipelineServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
pass
parser = argparse.ArgumentParser()
parser.add_argument("--port", action="store", default=0,
type=int, help="port to listen on")
parser.add_argument("--verbose", action="store", default=0,
type=int, help="verbose output")
parser.add_argument("--pidfile", action="store", default=0,
help="file name for the PID")
parser.add_argument("--logfile", action="store", default=0,
help="file name for the log")
parser.add_argument("--srcdir", action="store", default=0,
help="test directory")
parser.add_argument("--id", action="store", default=0,
help="server ID")
parser.add_argument("--ipv4", action="store_true", default=0,
help="IPv4 flag")
args = parser.parse_args()
if args.pidfile:
pid = os.getpid()
f = open(args.pidfile, 'w')
f.write('{}'.format(pid))
f.close()
server = PipelineServer(('0.0.0.0', args.port), PipelineRequestHandler)
server.allow_reuse_address = True
server.serve_forever()