22/**
33 * HTTP Server with Keep-Alive Support
44 * High-performance HTTP server implementation with connection pooling
5- *
5+ *
66 * Usage:
77 * php http_server_keepalive.php [host] [port]
8- *
8+ *
99 * Test with wrk:
1010 * wrk -t12 -c400 -d30s --http1.1 http://127.0.0.1:8080/
1111 */
1515
1616use function Async \spawn ;
1717use function Async \await ;
18+ use function Async \delay ;
1819
1920// Configuration
2021$ host = $ argv [1 ] ?? '127.0.0.1 ' ;
2122$ port = (int )($ argv [2 ] ?? 8080 );
2223$ keepaliveTimeout = 30 ; // seconds
2324
25+ $ socketCoroutines = 0 ;
26+ $ socketCoroutinesRun = 0 ;
27+ $ socketCoroutinesFinished = 0 ;
28+ $ requestCount = 0 ;
29+ $ requestHandled = 0 ;
30+
2431echo "=== Async HTTP Server with Keep-Alive === \n" ;
2532echo "Starting server on http:// $ host: $ port \n" ;
2633echo "Keep-Alive timeout: {$ keepaliveTimeout }s \n" ;
@@ -43,15 +50,15 @@ function parseHttpRequest($request)
4350 // Fast path: find first space and second space to extract URI
4451 $ firstSpace = strpos ($ request , ' ' );
4552 if ($ firstSpace === false ) return '/ ' ;
46-
53+
4754 $ secondSpace = strpos ($ request , ' ' , $ firstSpace + 1 );
4855 if ($ secondSpace === false ) return '/ ' ;
49-
56+
5057 $ uri = substr ($ request , $ firstSpace + 1 , $ secondSpace - $ firstSpace - 1 );
51-
58+
5259 // Check for Connection: close header (simple search)
5360 $ connectionClose = stripos ($ request , 'connection: close ' ) !== false ;
54-
61+
5562 return [
5663 'uri ' => $ uri ,
5764 'connection_close ' => $ connectionClose
@@ -64,11 +71,11 @@ function parseHttpRequest($request)
6471function processHttpRequest ($ client , $ rawRequest )
6572{
6673 global $ cachedResponses ;
67-
74+
6875 $ parsedRequest = parseHttpRequest ($ rawRequest );
6976 $ uri = $ parsedRequest ['uri ' ];
7077 $ shouldKeepAlive = !$ parsedRequest ['connection_close ' ];
71-
78+
7279 // Use cached responses for static content
7380 if (isset ($ cachedResponses [$ uri ])) {
7481 $ responseBody = $ cachedResponses [$ uri ];
@@ -78,11 +85,11 @@ function processHttpRequest($client, $rawRequest)
7885 $ responseBody = json_encode (['error ' => 'Not Found ' , 'uri ' => $ uri ], JSON_UNESCAPED_SLASHES );
7986 $ statusCode = 404 ;
8087 }
81-
88+
8289 // Build and send response directly
8390 $ contentLength = strlen ($ responseBody );
8491 $ statusText = $ statusCode === 200 ? 'OK ' : 'Not Found ' ;
85-
92+
8693 if ($ shouldKeepAlive ) {
8794 $ response = 'HTTP/1.1 ' . $ statusCode . ' ' . $ statusText . "\r\n" .
8895 'Content-Type: application/json ' . "\r\n" .
@@ -97,13 +104,13 @@ function processHttpRequest($client, $rawRequest)
97104 'Server: AsyncKeepAlive/1.0 ' . "\r\n" .
98105 'Connection: close ' . "\r\n\r\n" . $ responseBody ;
99106 }
100-
107+
101108 $ written = fwrite ($ client , $ response );
102-
109+
103110 if ($ written === false ) {
104111 return false ; // Write failed
105112 }
106-
113+
107114 return $ shouldKeepAlive ;
108115}
109116
@@ -113,53 +120,65 @@ function processHttpRequest($client, $rawRequest)
113120 */
114121function handleSocket ($ client )
115122{
123+ global $ socketCoroutinesRun , $ socketCoroutinesFinished ;
124+ global $ requestCount , $ requestHandled ;
125+
126+ $ socketCoroutinesRun ++;
127+
116128 try {
117129 while (true ) {
118130 $ request = '' ;
119131 $ totalBytes = 0 ;
120-
132+
121133 // Read HTTP request with byte counting
122134 while (true ) {
123135 $ chunk = fread ($ client , 1024 );
124-
136+
125137 if ($ chunk === false || $ chunk === '' ) {
126138 // Connection closed by client or read error
127139 return ;
128140 }
129-
141+
130142 $ request .= $ chunk ;
131143 $ totalBytes += strlen ($ chunk );
132-
144+
133145 // Check for request size limit
134146 if ($ totalBytes > 8192 ) {
135147 // Request too large, close connection immediately
136148 fclose ($ client );
149+ $ requestCount ++;
150+ $ requestHandled ++;
137151 return ;
138152 }
139-
153+
140154 // Check if we have complete HTTP request (ends with \r\n\r\n)
141155 if (strpos ($ request , "\r\n\r\n" ) !== false ) {
142156 break ;
143157 }
144158 }
145-
159+
146160 if (empty (trim ($ request ))) {
147161 // Empty request, skip to next iteration
148162 continue ;
149163 }
150-
164+
165+ $ requestCount ++;
166+
151167 // Process request and send response
152168 $ shouldKeepAlive = processHttpRequest ($ client , $ request );
153-
169+
170+ $ requestHandled ++;
171+
154172 if ($ shouldKeepAlive === false ) {
155173 // Write failed or connection should be closed
156174 return ;
157175 }
158-
176+
159177 // Continue to next request in keep-alive connection
160178 }
161-
179+
162180 } finally {
181+ $ socketCoroutinesFinished ++;
163182 // Always clean up the socket
164183 if (is_resource ($ client )) {
165184 fclose ($ client );
@@ -173,37 +192,59 @@ function handleSocket($client)
173192 */
174193function startHttpServer ($ host , $ port ) {
175194 return spawn (function () use ($ host , $ port ) {
195+
196+ global $ socketCoroutines ;
197+
176198 // Create server socket
177199 $ server = stream_socket_server ("tcp:// $ host: $ port " , $ errno , $ errstr , STREAM_SERVER_BIND | STREAM_SERVER_LISTEN );
178200 if (!$ server ) {
179201 throw new Exception ("Could not create server: $ errstr ( $ errno) " );
180202 }
181-
203+
204+ stream_context_set_option ($ server , 'socket ' , 'tcp_nodelay ' , true );
205+
182206 echo "Server listening on $ host: $ port \n" ;
183207 echo "Try: curl http:// $ host: $ port/ \n" ;
184208 echo "Benchmark: wrk -t12 -c400 -d30s http:// $ host: $ port/ \n\n" ;
185-
209+
186210 // Simple accept loop - much cleaner!
187211 while (true ) {
188212 // Accept new connections
189213 $ client = stream_socket_accept ($ server , 0 );
190214 if ($ client ) {
215+ $ socketCoroutines ++;
191216 // Spawn a coroutine to handle this client's entire lifecycle
192217 spawn (handleSocket (...), $ client );
193218 }
194219 }
195-
220+
196221 fclose ($ server );
197222 });
198223}
199224
225+ spawn (function () {
226+
227+ global $ socketCoroutines , $ socketCoroutinesRun , $ socketCoroutinesFinished , $ requestCount , $ requestHandled ;
228+
229+ while (true ) {
230+ delay (2000 );
231+ echo "Sockets: $ socketCoroutines \n" ;
232+ echo "Coroutines: $ socketCoroutinesRun \n" ;
233+ echo "Finished: $ socketCoroutinesFinished \n" ;
234+ echo "Request: $ requestCount \n" ;
235+ echo "Handled: $ requestHandled \n\n" ;
236+ }
237+ });
200238
201239// Start server
202240try {
203241 $ serverTask = startHttpServer ($ host , $ port );
204242 await ($ serverTask );
205-
243+
206244} catch (Exception $ e ) {
207245 echo "Server error: " . $ e ->getMessage () . "\n" ;
208- exit (1 );
209- }
246+ } finally {
247+ echo "Sockets: $ socketCoroutines \n" ;
248+ echo "Coroutines: $ socketCoroutinesRun \n" ;
249+ echo "Finished: $ socketCoroutinesFinished \n" ;
250+ }
0 commit comments