duyler / http-server
Non-blocking HTTP server for Duyler Framework worker mode with PSR-7 support
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/duyler/http-server
Requires
- php: ^8.4
- nyholm/psr7: ^1.8
- nyholm/psr7-server: ^1.1
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/log: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.90
- phpstan/phpstan: ^1.11
- phpstan/phpstan-strict-rules: ^1.6
- phpunit/phpunit: ^10.5
- rector/rector: ^1.2
README
Non-blocking HTTP server for Duyler Framework worker mode with full PSR-7 support.
Features
- ✅ Non-blocking I/O - Works seamlessly with Duyler Event Bus MainCyclic state
- ✅ PSR-7 Compatible - Full support for PSR-7 HTTP messages
- ✅ HTTP & HTTPS - Support for both HTTP and HTTPS protocols
- ✅ WebSocket Support - RFC 6455 compliant WebSocket implementation with zero-cost abstraction
- ✅ File Upload/Download - Complete multipart form-data and file streaming support
- ✅ Static Files - Built-in static file serving with LRU caching
- ✅ Keep-Alive - HTTP persistent connections support
- ✅ Range Requests - Partial content support for large file downloads
- ✅ Rate Limiting - Sliding window rate limiter with configurable limits
- ✅ Graceful Shutdown - Clean server termination with timeout
- ✅ Server Metrics - Built-in performance and health monitoring
- ✅ High Performance - Optimized for long-running worker processes
Requirements
- PHP 8.4 or higher
- ext-sockets (usually pre-installed)
Installation
composer require duyler/http-server
Quick Start
Basic HTTP Server
use Duyler\HttpServer\Server; use Duyler\HttpServer\Config\ServerConfig; use Nyholm\Psr7\Response; $config = new ServerConfig( host: '0.0.0.0', port: 8080, ); $server = new Server($config); // Check if server started successfully if (!$server->start()) { die('Failed to start HTTP server'); } // In your event loop while (true) { if ($server->hasRequest()) { $request = $server->getRequest(); // Check for null (race condition or error) if ($request === null) { continue; } // Process request $response = new Response(200, [], 'Hello World!'); $server->respond($response); } // Do other work... }
Configuration
ServerConfig Options
use Duyler\HttpServer\Config\ServerConfig; $config = new ServerConfig( // Network host: '0.0.0.0', // Bind address port: 8080, // Bind port // SSL/TLS ssl: false, // Enable HTTPS sslCert: null, // Path to SSL certificate sslKey: null, // Path to SSL private key // Static Files publicPath: null, // Path to public directory // Timeouts requestTimeout: 30, // Request timeout in seconds connectionTimeout: 60, // Connection timeout in seconds // Limits maxConnections: 1000, // Maximum concurrent connections maxRequestSize: 10485760, // Max request size (10MB) bufferSize: 8192, // Read buffer size // Keep-Alive enableKeepAlive: true, // Enable persistent connections keepAliveTimeout: 30, // Keep-alive timeout in seconds keepAliveMaxRequests: 100, // Max requests per connection // Static Cache enableStaticCache: true, // Enable in-memory static file cache staticCacheSize: 52428800, // Max cache size (50MB) // Rate Limiting enableRateLimit: false, // Enable rate limiting rateLimitRequests: 100, // Max requests per window rateLimitWindow: 60, // Rate limit window in seconds // Performance maxAcceptsPerCycle: 10, // Max new connections per cycle // Debug debugMode: false, // Enable debug logging mode );
Advanced Usage
HTTPS Server
$config = new ServerConfig( host: '0.0.0.0', port: 443, ssl: true, sslCert: '/path/to/certificate.pem', sslKey: '/path/to/private-key.pem', ); $server = new Server($config); $server->start();
WebSocket Server
use Duyler\HttpServer\WebSocket\WebSocketServer; use Duyler\HttpServer\WebSocket\WebSocketConfig; use Duyler\HttpServer\WebSocket\Connection; use Duyler\HttpServer\WebSocket\Message; $wsConfig = new WebSocketConfig( maxMessageSize: 1048576, pingInterval: 30, validateOrigin: false, ); $ws = new WebSocketServer($wsConfig); $ws->on('connect', function (Connection $conn) { echo "New connection: {$conn->getId()}\n"; }); $ws->on('message', function (Connection $conn, Message $message) { $data = $message->getJson(); $conn->send([ 'type' => 'echo', 'data' => $data, ]); }); $ws->on('close', function (Connection $conn, int $code, string $reason) { echo "Connection closed: $code\n"; }); $server->attachWebSocket('/ws', $ws); $server->start(); while (true) { if ($server->hasRequest()) { $request = $server->getRequest(); if ($request !== null) { $response = new Response(200, [], 'Hello'); $server->respond($response); } } usleep(1000); }
See examples/websocket-chat.php for a complete chat application example.
Static File Serving
use Duyler\HttpServer\Handler\StaticFileHandler; $staticHandler = new StaticFileHandler( publicPath: '/path/to/public', enableCache: true, maxCacheSize: 52428800, // 50MB ); while (true) { if ($server->hasRequest()) { $request = $server->getRequest(); // Try to serve static file first $response = $staticHandler->handle($request); if ($response === null) { // Not a static file, handle dynamically $response = handleDynamicRequest($request); } $server->respond($response); } }
File Download
use Duyler\HttpServer\Handler\FileDownloadHandler; $fileHandler = new FileDownloadHandler(); $response = $fileHandler->download( filePath: '/path/to/file.pdf', filename: 'document.pdf', mimeType: 'application/pdf' ); $server->respond($response);
File Upload
// Uploads are automatically parsed from multipart/form-data $request = $server->getRequest(); $uploadedFiles = $request->getUploadedFiles(); foreach ($uploadedFiles as $field => $file) { /** @var \Psr\Http\Message\UploadedFileInterface $file */ if ($file->getError() === UPLOAD_ERR_OK) { $file->moveTo('/path/to/uploads/' . $file->getClientFilename()); } }
Rate Limiting
$config = new ServerConfig( enableRateLimit: true, rateLimitRequests: 100, // Max 100 requests rateLimitWindow: 60, // Per 60 seconds (per IP) ); $server = new Server($config); $server->start(); // Rate limiting is applied automatically // Clients exceeding limits receive 429 Too Many Requests
Graceful Shutdown
$server = new Server(new ServerConfig()); $server->start(); // Register shutdown handler pcntl_signal(SIGTERM, function() use ($server) { $success = $server->shutdown(30); // 30 second timeout exit($success ? 0 : 1); }); while (true) { if ($server->hasRequest()) { $request = $server->getRequest(); $response = new Response(200, [], 'OK'); $server->respond($response); } }
Server Metrics
$server = new Server(new ServerConfig()); $server->start(); // Get metrics periodically $metrics = $server->getMetrics(); // [ // 'uptime_seconds' => 3600, // 'total_requests' => 10000, // 'successful_requests' => 9850, // 'failed_requests' => 150, // 'active_connections' => 5, // 'total_connections' => 10050, // 'closed_connections' => 10045, // 'timed_out_connections' => 10, // 'cache_hits' => 8500, // 'cache_misses' => 1500, // 'cache_hit_rate' => 85.0, // 'avg_request_duration_ms' => 12.3, // 'min_request_duration_ms' => 1.2, // 'max_request_duration_ms' => 450.5, // 'requests_per_second' => 2.78, // ]
API Reference
Server
Methods
start(): bool- Start the server (returns false on failure)stop(): void- Stop the servershutdown(int $timeout): bool- Graceful shutdown with timeoutreset(): void- Reset the server staterestart(): void- Restart the serverhasRequest(): bool- Check if there's a pending request (non-blocking)getRequest(): ?ServerRequestInterface- Get the next request (null if unavailable)hasPendingResponse(): bool- Check needs respondrespond(ResponseInterface): void- Send response for the current requestgetMetrics(): array- Get server performance metricssetLogger(LoggerInterface)- Set external LoggerattachWebSocket(string $path, WebSocketServer $ws): void- attach WebSocketServer
StaticFileHandler
Methods
handle(ServerRequestInterface): ?ResponseInterface- Handle static file requestgetCacheStats(): array- Get cache statisticsclearCache(): void- Clear the cache
FileDownloadHandler
Methods
download(string $filePath, ?string $filename, ?string $mimeType): ResponseInterfacedownloadRange(string $filePath, int $start, int $end, ...): ResponseInterfaceparseRangeHeader(string $rangeHeader, int $fileSize): ?array
Testing
# Run all tests composer test # Run with coverage (requires Xdebug or pcov) composer test:coverage # Run PHPStan composer phpstan
Performance Tips
- Enable Keep-Alive - Reduces connection overhead for multiple requests
- Use Static Cache - Cache frequently accessed static files in memory
- Adjust Buffer Size - Increase for high-throughput scenarios
- Set Appropriate Timeouts - Balance between responsiveness and resource usage
- Limit Max Connections - Prevent resource exhaustion
Roadmap
- HTTP/2 support
- Worker pool management
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
The MIT License (MIT). Please see License File for more information.