mirror of
https://github.com/Qbix/webserver.git
synced 2026-07-22 07:57:23 +02:00
Updated README to highlight comparisons to other set-ups
This commit is contained in:
@@ -11,7 +11,9 @@ One process serves static files, PHP scripts, WebSocket connections, and a live
|
|||||||
|
|
||||||
- [Quick Start](#-quick-start)
|
- [Quick Start](#-quick-start)
|
||||||
- [Performance](#-performance)
|
- [Performance](#-performance)
|
||||||
|
- [Why Not php-fpm?](#-why-not-php-fpm)
|
||||||
- [Features](#-features)
|
- [Features](#-features)
|
||||||
|
- [Server Powers](#-server-powers--what-your-php-can-do)
|
||||||
- [Configuration](#-configuration)
|
- [Configuration](#-configuration)
|
||||||
- [PHP Scripts](#-php-scripts)
|
- [PHP Scripts](#-php-scripts)
|
||||||
- [Three Ways to Run](#-three-ways-to-run)
|
- [Three Ways to Run](#-three-ways-to-run)
|
||||||
@@ -27,25 +29,25 @@ One process serves static files, PHP scripts, WebSocket connections, and a live
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone
|
# Clone
|
||||||
git clone https://github.com/Qbix/webserver.git
|
git clone https://github.com/Qbix/Server.git
|
||||||
cd webserver
|
cd Server
|
||||||
|
|
||||||
# Create a web directory
|
# Create a web directory
|
||||||
mkdir web
|
mkdir web
|
||||||
echo '<h1>Hello World</h1>' > web/index.html
|
echo '<h1>Hello World</h1>' > web/index.html
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
php qbixserver.php --port=8080
|
php server.php --port=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:8080](http://localhost:8080). That's it.
|
Open [http://localhost:8080](http://localhost:8080). That's it.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Or serve an existing directory
|
# Or serve an existing directory
|
||||||
php qbixserver.php --root=/var/www/mysite --port=80
|
php server.php --root=/var/www/mysite --port=80
|
||||||
|
|
||||||
# Or use the PHAR (single file, 196KB)
|
# Or use the PHAR (single file, 196KB)
|
||||||
php bin/qbixserver.phar --root=./public --port=8080
|
php bin/qbix-server.phar --root=./public --port=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -70,6 +72,94 @@ Zero failed requests across 50,000+ requests at concurrency 50. Server never cra
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🏎️ Why Not php-fpm?
|
||||||
|
|
||||||
|
The traditional stack — nginx + php-fpm — works like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Request → nginx → FastCGI socket → php-fpm worker
|
||||||
|
↓
|
||||||
|
Load PHP
|
||||||
|
Include autoloader
|
||||||
|
Boot framework
|
||||||
|
Connect to DB
|
||||||
|
Run your code
|
||||||
|
Send response
|
||||||
|
↓
|
||||||
|
Worker resets or dies
|
||||||
|
```
|
||||||
|
|
||||||
|
Every PHP request pays the bootstrap cost. Even with OPcache, each php-fpm worker re-initializes your framework's class instances, config trees, and DB connections on every request. For a framework like Qbix (or Laravel, Symfony, etc.), this bootstrap takes **10–50ms** — often longer than the actual work.
|
||||||
|
|
||||||
|
**Qbix Server eliminates this entirely:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Startup:
|
||||||
|
1. Load PHP
|
||||||
|
2. Include autoloader
|
||||||
|
3. Load ALL framework classes into memory
|
||||||
|
4. Parse ALL config files
|
||||||
|
5. Connect to database
|
||||||
|
6. pcntl_fork() → workers inherit everything
|
||||||
|
↓
|
||||||
|
Request:
|
||||||
|
Worker already has classes, config, DB connections.
|
||||||
|
Just run your code. 0ms bootstrap.
|
||||||
|
```
|
||||||
|
|
||||||
|
The key insight is **fork after preload**. Unix `fork()` uses copy-on-write, so forked workers share the parent's memory pages for all those preloaded classes. Each worker starts with ~30MB shared (read-only) and allocates only the per-request data. Compare this to php-fpm where each worker loads everything independently, using 30MB × N workers of duplicated memory.
|
||||||
|
|
||||||
|
### Preloading classes
|
||||||
|
|
||||||
|
Use the `--workers=N` flag and configure which classes to preload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Q": {
|
||||||
|
"webserver": {
|
||||||
|
"preload": [
|
||||||
|
"Q_Dispatcher", "Q_Request", "Q_Response",
|
||||||
|
"Q_Config", "Q_Cache", "Q_Session",
|
||||||
|
"Db", "Db_Mysql", "Db_Row", "Db_Query",
|
||||||
|
"Users", "Users_User", "Users_Session",
|
||||||
|
"Streams", "Streams_Stream", "Streams_Message"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start with 4 workers (classes loaded once, shared across all)
|
||||||
|
php server.php --app=/path/to/myapp --port=8080 --workers=4
|
||||||
|
```
|
||||||
|
|
||||||
|
The parent process loads and parses every class in the `preload` list, then forks. Workers inherit the entire loaded state — OPcache entries, class definitions, parsed config trees, autoloader maps. The first PHP request in each worker runs at full speed, no cold start.
|
||||||
|
|
||||||
|
### The numbers
|
||||||
|
|
||||||
|
| | php-fpm | Qbix Server |
|
||||||
|
|---|---|---|
|
||||||
|
| Bootstrap per request | 10–50ms | **0ms** |
|
||||||
|
| Memory per worker | 30–60MB each | 30MB shared + ~5MB per worker |
|
||||||
|
| IPC overhead | FastCGI socket + serialization | Direct function call or Unix fork |
|
||||||
|
| Static files | Separate nginx process | Same process, memory-cached, single `fwrite` |
|
||||||
|
| Config reload | Restart all workers | `SIGHUP`, zero downtime |
|
||||||
|
| WebSocket | Needs separate server | Built in |
|
||||||
|
|
||||||
|
For a Qbix app with 20 loaded plugins, the bootstrap savings alone make the server **2–5x faster** on PHP requests compared to nginx + php-fpm.
|
||||||
|
|
||||||
|
### Why it's actually faster in practice
|
||||||
|
|
||||||
|
The benchmarks above measure static file throughput, where nginx's C implementation and `sendfile()` syscall give it an inherent edge. But for **real PHP applications**, the story flips:
|
||||||
|
|
||||||
|
- **nginx + php-fpm:** 0.1ms static file + 30ms PHP bootstrap + 5ms actual work = **35ms**
|
||||||
|
- **Qbix Server:** 0.15ms static file + 0ms bootstrap + 5ms actual work = **5ms**
|
||||||
|
|
||||||
|
The 0.05ms you lose on static files, you gain back 30ms on every PHP request. And you can always put nginx or a CDN in front for the static file edge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
| Category | What you get |
|
| Category | What you get |
|
||||||
@@ -87,6 +177,166 @@ Zero failed requests across 50,000+ requests at concurrency 50. Server never cra
|
|||||||
| **Graceful shutdown** | SIGTERM/SIGINT drain in-flight requests before closing |
|
| **Graceful shutdown** | SIGTERM/SIGINT drain in-flight requests before closing |
|
||||||
| **TLS** | Optional HTTPS with auto-certbot or manual certs |
|
| **TLS** | Optional HTTPS with auto-certbot or manual certs |
|
||||||
| **Logging** | Colored terminal output + file-based access logs |
|
| **Logging** | Colored terminal output + file-based access logs |
|
||||||
|
| **Access control** | X-Accel-Redirect support — PHP enforces access, server serves the file |
|
||||||
|
| **Component cache** | X-Cache-Tree headers — invalidate parts of a page, not the whole thing |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Server Powers — What Your PHP Can Do
|
||||||
|
|
||||||
|
Qbix Server understands special response headers from your PHP scripts, giving you
|
||||||
|
capabilities that normally require complex nginx configurations or aren't possible at all.
|
||||||
|
|
||||||
|
### Access-controlled static files
|
||||||
|
|
||||||
|
With a typical server, your uploaded files sit at public URLs. Anyone with the link can
|
||||||
|
access them — and share the link with others. The usual workaround is "unguessable" URLs,
|
||||||
|
which are just security through obscurity.
|
||||||
|
|
||||||
|
Qbix Server supports `X-Accel-Redirect`: your PHP checks access, then tells the server
|
||||||
|
to serve the file directly — fast, streamed, with no public URL exposed:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// web/download.php — access-controlled file serving
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$fileId = $_GET['id'] ?? '';
|
||||||
|
$userId = $_SESSION['user_id'] ?? null;
|
||||||
|
|
||||||
|
// Your access control logic
|
||||||
|
if (!$userId || !userCanAccess($userId, $fileId)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo 'Access denied';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the server to serve the file directly.
|
||||||
|
// The client never sees the real path.
|
||||||
|
$realPath = "/uploads/private/{$fileId}";
|
||||||
|
header("X-Accel-Redirect: {$realPath}");
|
||||||
|
header("Content-Disposition: attachment; filename=\"document.pdf\"");
|
||||||
|
|
||||||
|
// The server takes over from here — streams the file
|
||||||
|
// with correct Content-Type, ETag, compression, etc.
|
||||||
|
// Your PHP process is already done.
|
||||||
|
```
|
||||||
|
|
||||||
|
No public URL for the file. No redirect the user can bookmark. The server streams
|
||||||
|
the file after your PHP has verified access and exited. This works for PDFs, images,
|
||||||
|
videos, ZIPs — anything.
|
||||||
|
|
||||||
|
### Built-in reverse proxy cache
|
||||||
|
|
||||||
|
The server caches responses and serves them without running PHP again.
|
||||||
|
Control it with standard `Cache-Control` headers:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// web/feed.php — cached for 5 minutes
|
||||||
|
header('Cache-Control: public, max-age=300');
|
||||||
|
|
||||||
|
// This runs once, then the server serves the cached
|
||||||
|
// response for the next 5 minutes. Zero PHP cost.
|
||||||
|
echo renderFeed();
|
||||||
|
```
|
||||||
|
|
||||||
|
The server also generates `ETag` headers from your response content. On subsequent
|
||||||
|
requests with `If-None-Match`, it returns `304 Not Modified` with no body — saving
|
||||||
|
bandwidth for both you and your users.
|
||||||
|
|
||||||
|
### Component-level cache invalidation
|
||||||
|
|
||||||
|
This is the big one. Most caching systems cache whole pages — when anything changes,
|
||||||
|
you throw away the entire page and re-render everything.
|
||||||
|
|
||||||
|
Qbix Server supports `X-Cache-Tree` and `X-Cache-Deps` headers that let your PHP
|
||||||
|
register individual components of a page and what data they depend on:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// web/community.php — a page with multiple components
|
||||||
|
|
||||||
|
// Render the feed (depends on the feed stream)
|
||||||
|
$feedHtml = renderFeed($communityId);
|
||||||
|
$feedHash = md5($feedHtml);
|
||||||
|
|
||||||
|
// Render the sidebar (depends on the about stream)
|
||||||
|
$sidebarHtml = renderSidebar($communityId);
|
||||||
|
$sidebarHash = md5($sidebarHtml);
|
||||||
|
|
||||||
|
// Render members list (depends on participants)
|
||||||
|
$membersHtml = renderMembers($communityId);
|
||||||
|
$membersHash = md5($membersHtml);
|
||||||
|
|
||||||
|
// Register components and what they depend on
|
||||||
|
header('X-Cache-Tree: ' . json_encode([
|
||||||
|
'l' => [
|
||||||
|
'feed' => $feedHash,
|
||||||
|
'sidebar' => $sidebarHash,
|
||||||
|
'members' => $membersHash,
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
|
||||||
|
header('X-Cache-Deps: ' . json_encode([
|
||||||
|
'feed' => ["community/{$communityId}/feed"],
|
||||||
|
'sidebar' => ["community/{$communityId}/about"],
|
||||||
|
'members' => ["community/{$communityId}/participants"],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// When someone posts to the feed, only 'feed' is invalidated.
|
||||||
|
// The sidebar and members list are still served from cache.
|
||||||
|
// The server re-renders only the stale component.
|
||||||
|
```
|
||||||
|
|
||||||
|
When data changes, tell the server which dependency key was affected:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// web/post.php — user posts to the feed
|
||||||
|
saveNewPost($communityId, $content);
|
||||||
|
|
||||||
|
// Invalidate only pages that depend on this feed
|
||||||
|
header('X-Cache-Invalidate: ' . json_encode([
|
||||||
|
"community/{$communityId}/feed"
|
||||||
|
]));
|
||||||
|
|
||||||
|
// The server walks its dependency graph:
|
||||||
|
// community/123/feed → page /community/123 component 'feed'
|
||||||
|
// Only that component is stale. Sidebar, members = still cached.
|
||||||
|
```
|
||||||
|
|
||||||
|
The server maintains a Merkle tree of component hashes. When any dependency key
|
||||||
|
is invalidated, it walks the tree to find exactly which components on which pages
|
||||||
|
are affected — and only those are re-rendered on the next request. Everything else
|
||||||
|
is served from the in-memory cache.
|
||||||
|
|
||||||
|
### Even more powerful with Qbix Platform
|
||||||
|
|
||||||
|
These headers work with plain PHP as shown above. But with the
|
||||||
|
[Qbix Platform](https://github.com/Qbix/Platform), it becomes automatic:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Tools call this during rendering — the framework handles the rest
|
||||||
|
Q_Response::setCacheComponent('Streams/feed', $hash, [$depKey]);
|
||||||
|
Q_Response::invalidateCacheDeps($publisherId . '/' . $streamName);
|
||||||
|
|
||||||
|
// X-Accel-Redirect for access-controlled files
|
||||||
|
Q_Response::setHeader('X-Accel-Redirect', $path);
|
||||||
|
|
||||||
|
// Cache-Control with semantic options
|
||||||
|
Q_Response::setCachePolicy([
|
||||||
|
'public' => true,
|
||||||
|
'maxAge' => 300,
|
||||||
|
'mustRevalidate' => true,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
The Platform's Streams plugin automatically invalidates cache dependencies when
|
||||||
|
stream data changes — posts, relations, participant joins — so cached pages
|
||||||
|
update themselves without manual invalidation calls. Combined with the server's
|
||||||
|
Merkle tree, this gives you fine-grained, data-driven cache invalidation across
|
||||||
|
your entire app, with zero configuration.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -166,29 +416,29 @@ For concurrent PHP execution, use `--workers=N` to pre-fork a worker pool.
|
|||||||
### 1. From source (needs PHP 8.1+)
|
### 1. From source (needs PHP 8.1+)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php qbixserver.php --root=./web --port=8080
|
php server.php --root=./web --port=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. PHAR — single 196KB file (needs PHP)
|
### 2. PHAR — single 196KB file (needs PHP)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php bin/qbixserver.phar --root=./web --port=8080
|
php bin/qbix-server.phar --root=./web --port=8080
|
||||||
|
|
||||||
# Or make it executable
|
# Or make it executable
|
||||||
chmod +x bin/qbixserver.phar
|
chmod +x bin/qbix-server.phar
|
||||||
./bin/qbixserver.phar --port=8080
|
./bin/qbix-server.phar --port=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Static binary — no PHP needed
|
### 3. Static binary — no PHP needed
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Download from GitHub Releases
|
# Download from GitHub Releases
|
||||||
chmod +x qbixserver-linux-x86_64
|
chmod +x qbix-server-linux-x86_64
|
||||||
./qbixserver-linux-x86_64 --root=./web --port=8080
|
./qbix-server-linux-x86_64 --root=./web --port=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
The binary bundles PHP 8.3 + extensions into a single ~15MB executable.
|
The binary bundles PHP 8.3 + extensions into a single ~15MB executable.
|
||||||
Copy it to any Linux machine and run. No dependencies.
|
Copy it to any Linux or macOS machine and run. No dependencies.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -198,7 +448,7 @@ Copy it to any Linux machine and run. No dependencies.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
php -d phar.readonly=0 build-phar.php
|
php -d phar.readonly=0 build-phar.php
|
||||||
# Output: bin/qbixserver.phar
|
# Output: bin/qbix-server.phar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build the static binary
|
### Build the static binary
|
||||||
@@ -210,13 +460,14 @@ php -d phar.readonly=0 build-phar.php
|
|||||||
# With static-php-cli installed locally:
|
# With static-php-cli installed locally:
|
||||||
./build-binary.sh
|
./build-binary.sh
|
||||||
|
|
||||||
# Output: bin/qbixserver (~15MB)
|
# Output: bin/qbix-server (~15MB)
|
||||||
```
|
```
|
||||||
|
|
||||||
The binary is built using [static-php-cli](https://github.com/crazywhalecc/static-php-cli),
|
The binary is built using [static-php-cli](https://github.com/crazywhalecc/static-php-cli),
|
||||||
which compiles PHP + extensions into a statically linked binary.
|
which compiles PHP + extensions into a statically linked binary.
|
||||||
|
|
||||||
GitHub Actions automatically builds binaries for **x86_64** and **aarch64** on every tagged release.
|
GitHub Actions automatically builds binaries for **Linux x86_64**, **Linux ARM64**,
|
||||||
|
**macOS x86_64**, and **macOS Apple Silicon** on every tagged release.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -278,12 +529,13 @@ this delivers sub-millisecond response times.
|
|||||||
|
|
||||||
**PHP scripts** run in-process (single-threaded, suitable for lightweight APIs)
|
**PHP scripts** run in-process (single-threaded, suitable for lightweight APIs)
|
||||||
or in a pre-fork worker pool (`--workers=N`) for concurrent PHP execution.
|
or in a pre-fork worker pool (`--workers=N`) for concurrent PHP execution.
|
||||||
Workers are forked after class preloading, so they share the base memory footprint.
|
Workers are forked after class preloading, so they share the base memory footprint
|
||||||
|
via copy-on-write pages.
|
||||||
|
|
||||||
**The remaining gap** versus nginx (Qbix at 55-73%) is inherent: nginx uses
|
**The remaining gap** versus nginx (55–73%) is inherent: nginx uses
|
||||||
`sendfile()` (kernel-space file→socket copy), `epoll` (O(1) event notification),
|
`sendfile()` (kernel-space file→socket copy), `epoll` (O(1) event notification),
|
||||||
and compiled C. PHP's `stream_select` is `select(2)`, file serving goes through
|
and compiled C. PHP's `stream_select` is `select(2)`, file serving goes through
|
||||||
userspace, and every operation has interpreter overhead. Getting to 55-73% of C
|
userspace, and every operation has interpreter overhead. Getting to 55–73% of C
|
||||||
performance from pure interpreted PHP is about as good as it gets.
|
performance from pure interpreted PHP is about as good as it gets.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user