0f147887b0
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.
448 lines
14 KiB
Python
Executable File
448 lines
14 KiB
Python
Executable File
#!/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()
|