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)
|
||||
- [Performance](#-performance)
|
||||
- [Why Not php-fpm?](#-why-not-php-fpm)
|
||||
- [Features](#-features)
|
||||
- [Server Powers](#-server-powers--what-your-php-can-do)
|
||||
- [Configuration](#-configuration)
|
||||
- [PHP Scripts](#-php-scripts)
|
||||
- [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
|
||||
# Clone
|
||||
git clone https://github.com/Qbix/webserver.git
|
||||
cd webserver
|
||||
git clone https://github.com/Qbix/Server.git
|
||||
cd Server
|
||||
|
||||
# Create a web directory
|
||||
mkdir web
|
||||
echo '<h1>Hello World</h1>' > web/index.html
|
||||
|
||||
# Run
|
||||
php qbixserver.php --port=8080
|
||||
php server.php --port=8080
|
||||
```
|
||||
|
||||
Open [http://localhost:8080](http://localhost:8080). That's it.
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
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
|
||||
|
||||
| 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 |
|
||||
| **TLS** | Optional HTTPS with auto-certbot or manual certs |
|
||||
| **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+)
|
||||
|
||||
```bash
|
||||
php qbixserver.php --root=./web --port=8080
|
||||
php server.php --root=./web --port=8080
|
||||
```
|
||||
|
||||
### 2. PHAR — single 196KB file (needs PHP)
|
||||
|
||||
```bash
|
||||
php bin/qbixserver.phar --root=./web --port=8080
|
||||
php bin/qbix-server.phar --root=./web --port=8080
|
||||
|
||||
# Or make it executable
|
||||
chmod +x bin/qbixserver.phar
|
||||
./bin/qbixserver.phar --port=8080
|
||||
chmod +x bin/qbix-server.phar
|
||||
./bin/qbix-server.phar --port=8080
|
||||
```
|
||||
|
||||
### 3. Static binary — no PHP needed
|
||||
|
||||
```bash
|
||||
# Download from GitHub Releases
|
||||
chmod +x qbixserver-linux-x86_64
|
||||
./qbixserver-linux-x86_64 --root=./web --port=8080
|
||||
chmod +x qbix-server-linux-x86_64
|
||||
./qbix-server-linux-x86_64 --root=./web --port=8080
|
||||
```
|
||||
|
||||
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
|
||||
php -d phar.readonly=0 build-phar.php
|
||||
# Output: bin/qbixserver.phar
|
||||
# Output: bin/qbix-server.phar
|
||||
```
|
||||
|
||||
### Build the static binary
|
||||
@@ -210,13 +460,14 @@ php -d phar.readonly=0 build-phar.php
|
||||
# With static-php-cli installed locally:
|
||||
./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),
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -254,21 +505,21 @@ CDN-style static file serving with versioned URLs. See the
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
HTTP request ────→ │ Event Loop │ stream_select (zero deps)
|
||||
│ (single thread)│ or amphp/revolt (optional)
|
||||
└────────┬─────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
|
||||
│ Static │ │ PHP │ │ WebSocket │
|
||||
│ Files │ │ Dispatch│ │ Upgrade │
|
||||
│ │ │ │ │ │
|
||||
│ In-memory │ │ In-proc │ │ RFC 6455 │
|
||||
│ response │ │ or fork │ │ frames │
|
||||
│ cache │ │ pool │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
┌──────────────────┐
|
||||
HTTP request ────→ │ Event Loop │ stream_select (zero deps)
|
||||
│ (single thread) │ or amphp/revolt (optional)
|
||||
└────────┬─────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
|
||||
│ Static │ │ PHP │ │ WebSocket │
|
||||
│ Files │ │ Dispatch │ │ Upgrade │
|
||||
│ │ │ │ │ │
|
||||
│ In-memory│ │ In-proc │ │ RFC 6455 │
|
||||
│ response │ │ or fork │ │ frames │
|
||||
│ cache │ │ pool │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
**Static files** are served from an in-memory response cache. The full HTTP response
|
||||
@@ -278,12 +529,13 @@ this delivers sub-millisecond response times.
|
||||
|
||||
**PHP scripts** run in-process (single-threaded, suitable for lightweight APIs)
|
||||
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),
|
||||
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.
|
||||
|
||||
---
|
||||
@@ -313,4 +565,4 @@ sudo apt install php-cli php-sockets
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
|
||||
Part of the [Qbix Platform](https://github.com/Qbix/Platform).
|
||||
Part of the [Qbix Platform](https://github.com/Qbix/Platform).
|
||||
Reference in New Issue
Block a user