Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.DS_Store
dist
picohttp.egg-info
pyproject.toml
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The HttpServer class is used to instantiate an HTTP server object.
**`class HttpServer(port=80, handler=staticResource, args=(), threads=True, start=True, block=True)`**

port
The port to listen on.
The port to listen on. An int or list of int values.

handler
The function defined to be the request handler.
Expand All @@ -28,14 +28,23 @@ The HttpServer class is used to instantiate an HTTP server object.
threads
If True handle each request in a separate thread, otherwise process them sequentially.

reuse
If True allow the reuse of the server address, otherwise the server will not start if the address is already in use.

start
If True immediately start the server, otherwise the start() function must be called.

block
If True the server will block indefinitely when it is started, otherwise the function starting it will return.

start()
Start the server if it was instantiated with start=False.
one_request
If True, shutdown the server after processing one request.

accept_period
The time in seconds to wait for a connection before checking if the server should be shutdown.

accept_queue_size
The maximum number of connections to queue before refusing new connections.

When a request is received it is parsed by the HTTP server and an HttpRequest object containing the request elements is passed to the request handler.

Expand Down Expand Up @@ -105,6 +114,8 @@ A helper function that facilitates the serving of static resources my be called
The HTTP mime type will be placed into a Content-Type header based on the file extension of the resource. This is the default that should be used if it can't be determined.

### Examples

<!-- TODO add more examples showing the use of the most recent ctor flags -->
This is a minimal HTTP server. It listens on port 80 and resources contained in the directory where the application is run from.
```
import picohttp
Expand Down
97 changes: 86 additions & 11 deletions picohttp/httpServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,63 @@
from rutifu import *
from .httpClasses import *
from .staticResource import *
from typing import Union


class HttpServer(object):
def __init__(self, port=80, handler=staticResource, args=(), threads=True, reuse=True, block=True, start=True):
def __init__(self, port: Union[int, list] = 80, handler=staticResource, args=(), threads=True, reuse=True,
block=True, start=True, one_request=False, accept_period: float = 3.0, accept_queue_size: int = 5):
"""
:param port: int or list of ints - ports to try to use. First successful bind() is utilized.
:param handler: function to handle requests. The handler should take a request and response object as arguments.
AND if the handler returns True AND one_request, the server's shutdown flag will be set to True.
:param args: Optional tuple of handler specific arguments. Last member in self.args is the server object. So the
handler can access the server object if needed to set shutdown = True
:param threads: True -> each request is handled in a separate thread. There is always a dedicated thread created
to listen for incoming connects. If not threads, the listener thread will also handle the requests.
:param reuse: If True, the server will set the SO_REUSEADDR socket option. to get around TIME_WAIT issues.
:param block: True -> server will block indefinitely waiting for incoming connections. False -> the invoker must
use its own strategy to block (e.g. sleep(n) or wait for a signal), settings the shutdown flag to True.
:param start: True -> server will start immediately. False -> server will not start until the start() method is
invoked.
:param one_request: True -> server will shut down after processing one request. Regardless of the return value
of the handler. If false, then the handler's return value of True will set the server's shutdown flag to True.
:param accept_period: time to wait for a connection (via accept) before checking if we should shut down.
:param accept_queue_size: size of the accept queue for incoming connections. Make this number large enough to
reduce the likelihood that an incoming connection that may be used to terminate the server is blocked waiting
'too' long.
"""
self.ports = listize(port)
self.port = 0
self.handler = handler
self.args = args
self.args = (*args, self) # TODO ? make the HttpServer ref the first or last in the arg tuple? as the expansion
# of this tuple when invoking the handler provides positional args to the handler rather than named args. Which
# is a strong argument for making the server object the first arg in the tuple. But most handlers won't need it
# which makes a strong argument for making it the last arg in the tuple. And this is consistent with my
# understanding of the usage of this lean, mean HttpServer. Simple, easy to use.
self.threads = threads
self.reuse = reuse
self.block = block
self.socket = None
self.one_request_only = one_request # we shut down after one request is processed.
self.accept_period = accept_period
self.accept_queue_size = accept_queue_size # in blocking mode, if an incoming connection is used to trigger a
# shutdown, we need to ensure that the accept() call is not blocked waiting for that connection. Definitely an
# issue when using tightly timed clients in tests. Note that I'm not bothering to use a mutex (threading.Lock)
# to protect the shutdown flag. This is because the shutdown flag is only set to True and never reset to False.
# simple semaphore usage, IMHO.
self.shutdown = False
if start:
self.start()

def start(self):
"""
Start the server by opening a socket on the specified port and listening for incoming connections. The
listening is either in a blocking mode or non-blocking. If non-blocking, the invoker must block using its
own strategy (e.g. sleep(n) or wait for a signal).
:return: 0 if no port was successfully used to listen on (bind failed for all our ports), else the port number
that is being listened on. (useful to convey to clients as this is the port to be used).
"""
debug("debugHttpServer", "httpServer", "starting")
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.reuse:
Expand All @@ -37,10 +79,12 @@ def start(self):
except OSError:
pass
if self.port:
self.socket.listen(1)
startThread("httpserver", self.getRequests)
self.socket.listen(self.accept_queue_size)
startThread(f"httpserver_{self.port}", self.getRequests)
# at this point, we now have another thread that is listening for incoming connections with a request
# so we might need to block (if not, our caller will need to block using its own strategy)
if self.block:
block()
self._block()
return self.port
else:
self.socket.close()
Expand All @@ -50,28 +94,49 @@ def start(self):
# wait for requests
def getRequests(self):
debug("debugHttpServer", "waiting for request")
# we might be notified to shut down after we start waiting so use the accept_period max wait time
self.socket.settimeout(self.accept_period) # never block forever if there is a possible shutdown needed
while True:
(client, addr) = self.socket.accept()
if self.threads:
startThread("httpserver_"+str(addr[0])+"_"+str(addr[1]), self.handleConnection, args=(client, addr,))
else:
self.handleConnection(client, addr)
# wait for a connection unless we've already been set to shut down
if self.shutdown:
self.socket.close()
return
try:
(client, addr) = self.socket.accept()
if self.threads:
startThread("httpserver_"+str(addr[0])+"_"+str(addr[1]), self.handleConnection,
args=(client, addr,))
else:
self.handleConnection(client, addr)
if self.one_request_only:
self.shutdown = True
self.socket.close()
return # our thread runs-down after one request
except socket.timeout:
if self.shutdown:
self.socket.close()
return
# otherwise we just keep looping, waiting for a connection.

def handleConnection(self, client, addr):
stop = False
request = HttpRequest()
self.parseRequest(client, addr, request)
debugRequest("debugHttpServer", addr, request)
# send it to the request handler
response = HttpResponse("HTTP/1.0", 200, {}, None)
try:
self.handler(request, response, *self.args)
if self.handler(request, response, *self.args):
stop = True
except Exception as ex:
logException("exception in request handler", ex)
response.status = 500
response.data = str(ex)+"\n"
self.sendResponse(client, addr, response)
debugResponse("debugHttpServer", addr, response)
client.close()
if stop and self.one_request_only:
self.shutdown = True

def parseRequest(self, client, addr, request):
clientFile = client.makefile()
Expand Down Expand Up @@ -121,3 +186,13 @@ def sendResponse(self, client, addr, response):
except BrokenPipeError: # can't do anything about this
log("sendResponse", "broken pipe", addr[0])
return

def _block(self):
"""
Block the calling thread indefinitely until we are set to shut down
:return: None
"""
while True:
if self.shutdown:
return
time.sleep(1)
63 changes: 63 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[build-system]
requires = [
"setuptools>=42",
"wheel",
"packaging>=24.2" # see https://github.com/pypa/twine/issues/1216 for why (and this is not sufficient to overcome the issue)
# see this issue as well: https://github.com/pypa/setuptools/issues/4759
]
build-backend = "setuptools.build_meta"

[project]
name="picohttp"
version="0.5.1"
requires-python= ">= 3.7"
authors = [
{ name= "Joe Deuhl", email="joe@thebuehls.com" },
{ name= "Steve Hespelt", email= "shespelt@stevens.edu"},
]
description = "An HTTP 1.0 server framework that is useful for supporting applications where the behavior is constrained and relatively simple"
dependencies = [
"rutifu"
]
readme = "README.md"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
# license-files key added in Dec 2024, not recognized by twine validation - why the workarounds (so I can publish this)
# license-files = ["LICENSE"]
#license.file = 'LICENSE' # "GPL-3.0-only"

[project.optional-dependencies]
# stuff that can be included via pkg-main[option1]

[project.scripts]
# python script that can get wrapper applied, so script invoked as command

[project.urls]
Repository="https://github.com/jbuehl/picohttp"

[tool.setuptools]
license-files = [] # see https://github.com/pypa/twine/issues/1216, https://github.com/pypa/setuptools/issues/4759 for why
# upgrading packaging to the latest (24.2) does not fix the issue

[tool.setuptools.packages.find]
where = ["./"]
include = ["picohttp"]
exclude = ["./tests**"]


[tool.poetry]

[tool.mypy]
mypy_path = 'picohttp/'

[tool.coverage.run]
source = [
"picohttp/**"
]
omit = [
"tests/skip*.py"
]
Loading