diff --git a/examples/hello_world_unix_domain.py b/examples/hello_world_unix_domain.py new file mode 100644 index 0000000..3ce6197 --- /dev/null +++ b/examples/hello_world_unix_domain.py @@ -0,0 +1,11 @@ +from socketify import App, AppListenOptions + +app = App() +app.get("/", lambda res, req: res.end("Hello World!")) +app.listen( + AppListenOptions(domain="/tmp/test.sock"), + lambda config: print("Listening on port %s http://localhost/ now\n" % config.domain), +) +app.run() + +# you can test with curl -GET --unix-socket /tmp/test.sock http://localhost/ \ No newline at end of file diff --git a/examples/static_files.py b/examples/static_files.py index 11ad95a..f971610 100644 --- a/examples/static_files.py +++ b/examples/static_files.py @@ -6,8 +6,8 @@ # using oha -c 400 -z 5s http://localhost:3000/ # nginx - try_files - 77630.15 req/s -# pypy3 - socketify static - 10245.82 req/s -# python3 - socketify static - 8273.71 req/s +# pypy3 - socketify static - 15839.22 req/s +# python3 - socketify static - 8294.96 req/s # node.js - @fastify/static - 5437.16 req/s # node.js - express.static - 4077.49 req/s # python3 - socketify static_aiofile - 2390.96 req/s @@ -21,8 +21,8 @@ # Conclusions: # With PyPy3 only static is really usable gunicorn/uvicorn, aiofiles and aiofile are realy slow on PyPy3 maybe this changes with HPy -# Python3 with any option will be faster than gunicorn/uvicorn but with PyPy3 with static we got 2x (or almost this in case of fastify) performance of node.js -# But even PyPy3 + socketify static is 7x+ slower than NGINX +# Python3 with any option will be faster than gunicorn/uvicorn but with PyPy3 with static we got almost 4x (or almost 3x this in case of fastify) performance of node.js +# But even PyPy3 + socketify static is 5x+ slower than NGINX # Anyway we really recommends using NGINX or similar + CDN for production like everybody else # Gunicorn production recommendations: https://docs.gunicorn.org/en/latest/deploy.html#deploying-gunicorn diff --git a/src/socketify/cli.py b/src/socketify/cli.py index b28d493..57d2605 100644 --- a/src/socketify/cli.py +++ b/src/socketify/cli.py @@ -34,7 +34,7 @@ Options: --req-res-factory-maxitems INT Pre allocated instances of Response and Request objects for socketify interface [default: 0] --ws-factory-maxitems INT Pre allocated instances of WebSockets objects for socketify interface [default: 0] - --uds TEXT Bind to a UNIX domain socket. + --uds TEXT Bind to a UNIX domain socket, this options disables --host or -h and --port or -p. --reload Enable auto-reload. This options also disable --workers or -w option. --reload-dir PATH Set reload directories explicitly, instead of using the current working directory. --reload-include TEXT Set extensions to include while watching for files. @@ -153,12 +153,16 @@ def execute(args): elif interface != "socketify": return print(f"{interface} interface is not supported yet") + auto_reload = options.get('--reload', False) workers = int(options.get("--workers", options.get("-w", os.environ.get('WEB_CONCURRENCY', 1)))) - if workers < 1: + if workers < 1 or auto_reload: workers = 1 port = int(options.get("--port", options.get("-p", 8000))) host = options.get("--host", options.get("-h", "127.0.0.1")) + uds = options.get('--uds', None) + + disable_listen_log = options.get("--disable-listen-log", False) websockets = options.get("--ws", "auto") @@ -197,7 +201,10 @@ def execute(args): def listen_log(config): if not disable_listen_log: - print(f"Listening on {'https' if ssl_options else 'http'}://{config.host if config.host and len(config.host) > 1 else '127.0.0.1' }:{config.port} now\n") + if uds: + print(f"Listening on {config.domain} {'https' if ssl_options else 'http'}://localhost now\n") + else: + print(f"Listening on {'https' if ssl_options else 'http'}://{config.host if config.host and len(config.host) > 1 else '127.0.0.1' }:{config.port} now\n") if websockets: websocket_options = { @@ -230,7 +237,10 @@ def execute(args): if websockets: # if socketify websockets are added using --ws in socketify interface we can set here websockets.update(websocket_options) # set websocket options fork_app.ws("/*", websockets) - fork_app.listen(AppListenOptions(port=port, host=host), listen_log) + if uds: + fork_app.listen(AppListenOptions(domain=uds), listen_log) + else: + fork_app.listen(AppListenOptions(port=port, host=host), listen_log) fork_app.run() # now we can start all over again @@ -251,5 +261,29 @@ def execute(args): for pid in pid_list: os.kill(pid, signal.SIGINT) else: - #Generic WSGI, ASGI, SSGI Interface - Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options).listen(AppListenOptions(port=port, host=host), listen_log).run(workers=workers) \ No newline at end of file + + # def on_change(): + # auto_reload + + def create_app(): + #Generic WSGI, ASGI, SSGI Interface + if uds: + app = Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options).listen(AppListenOptions(domain=uds), listen_log) + else: + app = Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options).listen(AppListenOptions(port=port, host=host), listen_log) + return app + + + if auto_reload: + force_reload = False + app = None + + while auto_reload: + app = create_app() + app.run() + if not force_reload: + auto_reload = False + + else: + app = create_app() + app.run(workers=workers) diff --git a/src/socketify/helpers.py b/src/socketify/helpers.py index a68ae4a..37bcbbe 100644 --- a/src/socketify/helpers.py +++ b/src/socketify/helpers.py @@ -9,6 +9,7 @@ mimetypes.init() # We have an version of this using aiofile and aiofiles # This is an sync version without any dependencies is normally much faster in CPython and PyPy3 # In production we highly recomend to use CDN like CloudFlare or/and NGINX or similar for static files +# TODO: this should be optimized entire in C++ async def sendfile(res, req, filename): # read headers before the first await if_modified_since = req.get_header("if-modified-since") @@ -26,7 +27,7 @@ async def sendfile(res, req, filename): exists = path.exists(filename) # not found if not exists: - return res.write_status(404).end(b"Not Found") + return res.cork(lambda res: res.write_status(404).end(b"Not Found")) # get size and last modified date stats = os.stat(filename) @@ -38,17 +39,13 @@ async def sendfile(res, req, filename): # check if modified since is provided if if_modified_since == last_modified: - return res.write_status(304).end_without_body() - # tells the broswer the last modified date - res.write_header(b"Last-Modified", last_modified) + return res.cork(lambda res: res.write_status(304).end_without_body()) # add content type (content_type, encoding) = mimetypes.guess_type(filename, strict=True) if content_type and encoding: - res.write_header(b"Content-Type", "%s; %s" % (content_type, encoding)) - elif content_type: - res.write_header(b"Content-Type", content_type) - + content_type = "%s; %s" % (content_type, encoding) + with open(filename, "rb") as fd: # check range and support it if start > 0 or not end == -1: @@ -57,17 +54,27 @@ async def sendfile(res, req, filename): size = end - start + 1 fd.seek(start) if start > total_size or size > total_size or size < 0 or start < 0: - return res.write_status(416).end_without_body() - res.write_status(206) + if content_type: + return res.cork(lambda res: res.write_header(b"Content-Type", content_type).write_status(416).end_without_body()) + return res.cork(lambda res: res.write_status(416).end_without_body()) + status = 206 else: end = size - 1 - res.write_status(200) + status = 200 - # tells the browser that we support range - res.write_header(b"Accept-Ranges", b"bytes") - res.write_header( - b"Content-Range", "bytes %d-%d/%d" % (start, end, total_size) - ) + def send_headers(res): + res.write_status(status) + # tells the broswer the last modified date + res.write_header(b"Last-Modified", last_modified) + + # tells the browser that we support range + if content_type: + res.write_header(b"Content-Type", content_type) + res.write_header(b"Accept-Ranges", b"bytes") + res.write_header( + b"Content-Range", "bytes %d-%d/%d" % (start, end, total_size) + ) + res.cork(send_headers) pending_size = size # keep sending until abort or done while not res.aborted: @@ -81,7 +88,7 @@ async def sendfile(res, req, filename): break except Exception as error: - res.write_status(500).end("Internal Error") + res.cork(lambda res: res.write_status(500).end("Internal Error")) def in_directory(file, directory): diff --git a/src/socketify/native.py b/src/socketify/native.py index 8703895..3c50a8c 100644 --- a/src/socketify/native.py +++ b/src/socketify/native.py @@ -148,6 +148,7 @@ typedef struct { } uws_try_end_result_t; typedef void (*uws_listen_handler)(struct us_listen_socket_t *listen_socket, uws_app_listen_config_t config, void *user_data); +typedef void (*uws_listen_domain_handler)(struct us_listen_socket_t *listen_socket, const char* domain, size_t domain_length, int options, void *user_data); typedef void (*uws_method_handler)(uws_res_t *response, uws_req_t *request, void *user_data); typedef void (*uws_filter_handler)(uws_res_t *response, int, void *user_data); typedef void (*uws_missing_server_handler)(const char *hostname, size_t hostname_length, void *user_data); @@ -171,6 +172,8 @@ void uws_app_run(int ssl, uws_app_t *); void uws_app_listen(int ssl, uws_app_t *app, int port, uws_listen_handler handler, void *user_data); void uws_app_listen_with_config(int ssl, uws_app_t *app, uws_app_listen_config_t config, uws_listen_handler handler, void *user_data); +void uws_app_listen_domain(int ssl, uws_app_t *app, const char *domain, size_t domain_length, uws_listen_domain_handler handler, void *user_data); +void uws_app_listen_domain_with_options(int ssl, uws_app_t *app, const char *domain,size_t domain_length, int options, uws_listen_domain_handler handler, void *user_data); bool uws_constructor_failed(int ssl, uws_app_t *app); unsigned int uws_num_subscribers(int ssl, uws_app_t *app, const char *topic, size_t topic_length); bool uws_publish(int ssl, uws_app_t *app, const char *topic, size_t topic_length, const char *message, size_t message_length, uws_opcode_t opcode, bool compress); diff --git a/src/socketify/socketify.py b/src/socketify/socketify.py index 8a2a79a..ac042c4 100644 --- a/src/socketify/socketify.py +++ b/src/socketify/socketify.py @@ -22,10 +22,6 @@ from dataclasses import dataclass mimetypes.init() -is_python = platform.python_implementation() == "CPython" - - - @ffi.callback("void(const char*, size_t, void*)") def uws_missing_server_name(hostname, hostname_length, user_data): if user_data != ffi.NULL: @@ -529,6 +525,25 @@ def uws_generic_method_handler(res, req, user_data): app.trigger_error(err, response, request) + + +@ffi.callback("void(struct us_listen_socket_t*, const char*, size_t,int, void*)") +def uws_generic_listen_domain_handler(listen_socket, domain, length, _options, user_data): + domain = ffi.unpack(domain, length).decode("utf8") + if listen_socket == ffi.NULL: + raise RuntimeError("Failed to listen on domain %s" % domain) + + if user_data != ffi.NULL: + + app = ffi.from_handle(user_data) + if hasattr(app, "_listen_handler") and hasattr(app._listen_handler, "__call__"): + app.socket = listen_socket + app._listen_handler( + AppListenOptions( + domain=domain, + options=int(_options) + ) + ) @ffi.callback("void(struct us_listen_socket_t*, uws_app_listen_config_t, void*)") def uws_generic_listen_handler(listen_socket, config, user_data): if listen_socket == ffi.NULL: @@ -1283,7 +1298,9 @@ class AppResponse: self._cork_handler = callback lib.uws_res_cork(self.SSL, self.res, uws_generic_cork_handler, self._ptr) - def set_cookie(self, name, value, options={}): + def set_cookie(self, name, value, options): + if options is None: + options = {} if self._write_jar is None: self._write_jar = cookies.SimpleCookie() self._write_jar[name] = quote_plus(value) @@ -2230,18 +2247,22 @@ class App: self.SSL, self.app, options, uws_generic_listen_handler, self._ptr ) else: - native_options = ffi.new("uws_app_listen_config_t *") - options = native_options[0] - options.port = ffi.cast("int", port_or_options.port) - options.host = ( - ffi.NULL - if port_or_options.host is None - else ffi.new("char[]", port_or_options.host.encode("utf-8")) - ) - options.options = ffi.cast("int", port_or_options.options) - self.native_options_listen = native_options # Keep alive native_options - lib.uws_app_listen_with_config( - self.SSL, self.app, options, uws_generic_listen_handler, self._ptr + if port_or_options.domain: + domain = port_or_options.domain.encode('utf8') + lib.uws_app_listen_domain_with_options(self.SSL, self.app, domain, len(domain), int(port_or_options.options), uws_generic_listen_domain_handler, self._ptr) + else: + native_options = ffi.new("uws_app_listen_config_t *") + options = native_options[0] + options.port = ffi.cast("int", port_or_options.port) + options.host = ( + ffi.NULL + if port_or_options.host is None + else ffi.new("char[]", port_or_options.host.encode("utf-8")) + ) + options.options = ffi.cast("int", port_or_options.options) + self.native_options_listen = native_options # Keep alive native_options + lib.uws_app_listen_with_config( + self.SSL, self.app, options, uws_generic_listen_handler, self._ptr ) return self @@ -2320,16 +2341,20 @@ class AppListenOptions: port: int = 0 host: str = None options: int = 0 + domain: str = None def __post_init__(self): if not isinstance(self.port, int): raise RuntimeError("port must be an int") if not isinstance(self.host, (type(None), str)): raise RuntimeError("host must be a str if specified") + if not isinstance(self.domain, (type(None), str)): + raise RuntimeError("domain must be a str if specified") if not isinstance(self.options, int): raise RuntimeError("options must be an int") - - + if self.domain and (self.host or self.port != 0): + raise RuntimeError("if domain is specified, host and port will be no effect") + @dataclass class AppOptions: key_file_name: str = None,