简易HTTP协议解析
首先介紹一些必要的知識(shí)點(diǎn)。
TCP協(xié)議為操作系統(tǒng)底層協(xié)議,能夠保證應(yīng)用層獲取到完整的、順序一直的包序列。但TCP不提供具體的分包,需要上層協(xié)議自己解決。TCP發(fā)送給上層協(xié)議的數(shù)據(jù)是一個(gè)沒有意義的字符串序列。如何解釋這段序列,需要應(yīng)用層定義,也就是應(yīng)用層協(xié)議規(guī)范的內(nèi)容。
應(yīng)用層協(xié)議按格式一般可以分為文本協(xié)議和二進(jìn)制協(xié)議。文本協(xié)議最常見的就是HTTP,二進(jìn)制協(xié)議如websocket。無論是哪種協(xié)議,都需要對格式嚴(yán)格定義,以方便程序?qū)ψ址蛄羞M(jìn)行分包、拆包。
HTTP協(xié)議通過兩種方式定義協(xié)議幀(一個(gè)HTTP請求或一個(gè)HTTP響應(yīng))結(jié)束標(biāo)志。第一種是在http header中使用Content-Length頭給出body長度,header與body使用\r\n\r\n分隔,這樣我們就能夠確定一個(gè)HTTP幀的開始與結(jié)束。另外一種是chunked編碼的http幀,通過在header中使用Transfer-Encoding:chunked標(biāo)志聲明該編碼方式,消息體由數(shù)量未定的塊組成,并以最后一個(gè)大小為0的塊為結(jié)束。每一個(gè)非空的塊都以該塊包含數(shù)據(jù)的字節(jié)數(shù)(字節(jié)數(shù)以十六進(jìn)制表示)開始,跟隨一個(gè)CRLF (回車及換行),然后是數(shù)據(jù)本身,最后塊CRLF結(jié)束。在一些實(shí)現(xiàn)中,塊大小和CRLF之間填充有白空格(0x20)。最后一塊是單行,由塊大小(0),一些可選的填充白空格,以及CRLF。最后一塊不再包含任何數(shù)據(jù),但是可以發(fā)送可選的尾部,包括消息頭字段。消息最后以CRLF結(jié)尾。
?
格式如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | HTTP/1.1 200 OK Content-Type: text/plain Transfer-Encoding: chunked 25 This is the data in the first chunk 1C and this is the second one 3 con 8 sequence 0 |
為簡單起見,這里對HTTP服務(wù)端協(xié)議的解析,只考以Content-Length方式分幀的http幀,不考慮chunked編碼的http協(xié)議解析。
流程圖如下:
?
http協(xié)議解析流程圖
如上圖所示,TCP提供的是字節(jié)流,所以會(huì)出現(xiàn)如下三種種情況:
針對以上三種情況,分別要做處理,出現(xiàn)粘包和缺包時(shí),我們需要緩存尚未處理的部分留待下次接收到數(shù)據(jù)時(shí)一并處理。
根據(jù)如上流程圖實(shí)現(xiàn)的PHP程序如下:
代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | <?php /** * Created by PhpStorm. * User: Jenner * Date: 2015/8/10 * Time: 14:00 * * 簡單http server端協(xié)議解析,實(shí)現(xiàn)粘包、缺包,拆包 */ $server = new Server(); //注冊響應(yīng)回調(diào)函數(shù) $server->registerHandler(function($connection, $header, $body){ echo "get request: " . time() . PHP_EOL; echo "header: " . PHP_EOL . $header . PHP_EOL; echo "body: " . PHP_EOL . $body . PHP_EOL; $response = "HTTP/1.1 200 OK\r\n"; $response .= "Date: Mon, 10 Aug 2015 06:22:08 GMT\r\n"; $response .= "Content-Type: text/html;charset=utf-8\r\n\r\n"; socket_write($connection, $response, strlen($response)); }); //啟動(dòng)server $server->start(); /** * 簡易http server 支持http協(xié)議解析 * Class Server */ class Server { /** * @var string 字符流緩存 */ protected $cache = ""; /** * @var http請求處理器 */ protected $handler; /** * 啟動(dòng)server */ public function start() { $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_bind($socket, '0.0.0.0', 1212); socket_listen($socket); while ($connection = socket_accept($socket)) { echo "connected" . PHP_EOL; while(true){ $bytes = socket_read($connection, 1); echo "read: " . $bytes . PHP_EOL; if(empty($bytes)) { sleep(1); continue; } if($this->parse($connection, $bytes)){ break; } echo "parse" . PHP_EOL; usleep(100); } } } /** * 注冊處理器 * @param $handler */ public function registerHandler($handler) { $this->handler = $handler; } /** * http協(xié)議解析 * @param $connection * @param $data * @return bool */ protected function parse($connection, $data) { $data = $this->cache . $data; var_dump($data); $header = $body = ""; if (strstr($data, "\r\n\r\n") === false) { $this->cache = $data; echo "cached" . PHP_EOL; return false; } $http_info = explode("\r\n\r\n", $data, 2); $header = $http_info[0]; $body = count($http_info) > 1 ? $http_info[1] : 0; $content_length = $this->getContentLength($header); if ($content_length == 0) { // 正好是一個(gè)http幀 call_user_func($this->handler, $connection, $header, null); socket_close($connection); return true; } elseif($content_length > strlen($body)){ // 缺少body部分 $this->cache = $data; return false; } else { // 發(fā)生粘包,摘出當(dāng)前包,緩存剩余部分 $body = substr($body, 0, $content_length); $this->cache = substr($body, $content_length); call_user_func($this->handler, $connection, $header, $body); return false; } } /** * 獲取content-length * @param $headers * @return int */ protected function getContentLength($headers) { $headers = explode("\r\n", $headers); foreach ($headers as $header) { if (stristr("content-length", $headers) === false) continue; $content_length = intval(explode(":", $header, 2)[1]); return $content_length; } return 0; } } |
客戶端代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <?php /** * Created by PhpStorm. * User: Jenner * Date: 2015/8/10 * Time: 14:19 * * 簡單http socket客戶端,實(shí)現(xiàn)一次HTTP請求 */ $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if(socket_connect($socket, "127.0.0.1", 1212) === false){ echo "ERROR:" . socket_strerror(socket_last_error($socket)) . PHP_EOL; exit; } $request_headers = array( "GET / HTTP/1.1", "Host: xxx.xxx", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", ); $request = implode("\r\n", $request_headers); // if comment the follow line, you will read nothing. because the request format is error. $request .= "\r\n\r\n"; var_dump($request); if(socket_write($socket, $request, strlen($request)) === false){ echo "ERROR:" . socket_strerror(socket_last_error($socket)) . PHP_EOL; exit; } echo "the server will not response until time is out" . PHP_EOL; $response = socket_read($socket, 1024); if($response === false || empty($response)){ echo "ERROR: timeout or socket error" . PHP_EOL; exit; } echo "response:" . PHP_EOL; var_dump($response); socket_close($socket); |
總結(jié)
以上是生活随笔為你收集整理的简易HTTP协议解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我姓徐媳妇姓荆该起个什么网名^qUxJg
- 下一篇: Harshanie Lakshika J