單進程服務器簡陋版:php
<?php /** * Single http server. * * Access http://127.0.0.1:8081 * * @license Apache-2.0 * @author farwish */ $s_socket_uri = 'tcp://0.0.0.0:8081'; $s_socket = stream_socket_server($s_socket_uri, $errno, $errstr) OR trigger_error("Failed to create socket: $s_socket_uri, Err($errno) $errstr", E_USER_ERROR); while(1) { while($connection = @stream_socket_accept($s_socket, 60, $peer)) { echo "Connected with $peer. Request info...\n"; $client_request = ""; // Read until double \r while( !preg_match('/\r?\n\r?\n/', $client_request) ) { $client_request .= fread($connection, 1024); } if (!$client_request) { trigger_error("Client request is empty!"); } $headers = "HTTP/1.1 200 OK\r\n" ."Server: nginx\r\n" ."Content-Type: text/html; charset=utf-8\r\n" ."\r\n"; $body = "<h1>hello world</h1><br><br>"; if ((int) fwrite($connection, $headers . $body) < 1) { trigger_error("Write to socket failed!"); } fclose($connection); } }
HTTP 底層基於 TCP,因此 socket 地址指定爲 tcp 協議沒有問題;stream_socket_server 功能至關於執行了 socket => bind => listen,stream_socket_accept 阻塞等待 client 鏈接,並設置了超時時間,默認的 timeout 時間使用在 php.ini 中設置。html
注意這裏的錯誤抑制符@,抑制 accept 超時狀況產生的 PHP Warning,若是用到 stream_select 也須要加錯誤抑制符 @ 來避免中斷信號引發的 PHP Warning。nginx
HTTP 響應報文格式包含 響應狀態碼、響應首部、響應主體(若是有的話),響應首部每行以 \r\n 結尾,響應頭部結束單獨一行 \r\n 結尾,後面就是響應主體了,響應頭部加響應主體以 fwrite 寫入 socket 鏈接,fclose 關閉鏈接。數組
注意:上面的簡陋版既沒有設置 socket 上下文選項,也沒有使用 I/O 複用,更不是多進程的,只是做爲請求響應的演示。服務器
較爲嚴謹的HTTP協議處理版:併發
$method = ''; $url = ''; $protocol_version = ''; $request_header = []; $content_type = 'text/html; charset=utf-8'; $content_length = 0; $request_body = ''; $end_of_header = false; // @see http://php.net/manual/en/function.fread.php $buffer = fread($connection, 8192); if (false !== $buffer) { // Http request format check. if (false !== strstr($buffer, "\r\n")) { $list = explode("\r\n", $buffer); } if ($list) { foreach ($list as $line) { if ($end_of_header) { if (strlen($line) === $content_length) { $request_body = $line; } else { throw new \Exception("Content-Length {$content_length} not match request body length " . strlen($line) . "\n"); } break; } if ( empty($line) ) { $end_of_header = true; } else { // Header. // if (false === strstr($line, ': ')) { $array = explode(' ', $line); // Request line. if (count($array) === 3) { $method = $array[0]; $url = $array[1]; $protocol_version = $array[2]; } } else { $array = explode(': ', $line); // Request header. list ($key, $value) = $array; $request_header[$key] = $value; if ( strtolower($key) === strtolower('Content-type') ) { $content_type = $value; } // Have request body. if ($key === 'Content-Length') { $content_length = $value; } } } } } } // No request body, show buffer from read. $response_body = $request_body ?: $buffer; $response_header = "HTTP/1.1 200 OK\r\n"; $response_header .= "Content-type: {$content_type}\r\n"; if (empty($content_length) && (strlen($response_body) > 0)) { $response_header .= "Content-Length: " . strlen($response_body) . "\r\n"; } foreach ($request_header as $k => $v) { $response_header .= "{$k}: {$v}\r\n"; } $response_header .= "\r\n"; fwrite($connection, $response_header . $response_body); fclose($connection);
以上程序屬於 accept 以後的處理步驟,外層邏輯這裏已省略,也適用在多進程服務器中子進程的處理部分。socket
這裏仍是用 fread 統一讀取數據,設置讀取的長度 8192(fread 的默認也是8192),$buffer 含有頭部信息和數據,按 \r\n 分解成數組元素再處理,處理方式按照 HTTP 請求報文格式。tcp
首先是請求行,如 GET /index HTTP/1.1 三部分以 「空格」 隔開,行尾以 \r\n 結束。性能
其次是報文首部,如 Content-Type: text/html 鍵值中間以 「冒號」 加 「空格」 隔開,行尾以 \r\n 結束。url
請求報文頭部以一行 \r\n 結束。
最後是請求主體(若是有的話),若是報文首部中有 Content-Length 值,就說明有請求主體。
響應數據按照 響應頭部 加 響應主體,寫入 socket 鏈接,最後關閉鏈接。
HTTP協議格式示意:
小結:
完整步驟 1.建立服務端套接字 2.接受請求 3.解釋請求行肯定請求的特定文件 4.從文件系統中獲取請求文件 5.建立被請求的文件組成的HTTP響應報文,發送給客戶端 6.若是請求的文件不存在,服務端返回 404 Not Found 報文。
無重邏輯(字符輸出)的場景中,ab 100併發1000次訪問壓測,對比傳統調優後的 Nginx + PHP,PHP 實現的多進程非阻塞 I/O HTTP Server 的 QPS 性能稍高,有一個理由能夠解釋:它不須要 Nginx 來轉發請求。
PHP實現的 HTTP Server 有不少細節須要自身處理,不一樣因素會對處理請求的性能產生直接影響以及面臨某些場景下才能產生的BUG,因此穩定性上須要經受更多考驗。
Reference: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Overview