简易HTTP协议解析

HTTP协议 Jenner 11003℃ 0评论
       首先介绍一些必要的知识点。
       TCP协议为操作系统底层协议,能够保证应用层获取到完整的、顺序一直的包序列。但TCP不提供具体的分包,需要上层协议自己解决。TCP发送给上层协议的数据是一个没有意义的字符串序列。如何解释这段序列,需要应用层定义,也就是应用层协议规范的内容。
       应用层协议按格式一般可以分为文本协议和二进制协议。文本协议最常见的就是HTTP,二进制协议如websocket。无论是哪种协议,都需要对格式严格定义,以方便程序对字符串序列进行分包、拆包。
 
       HTTP协议通过两种方式定义协议帧(一个HTTP请求或一个HTTP响应)结束标志。第一种是在http header中使用Content-Length头给出body长度,header与body使用\r\n\r\n分隔,这样我们就能够确定一个HTTP帧的开始与结束。另外一种是chunked编码的http帧,通过在header中使用Transfer-Encoding:chunked标志声明该编码方式,消息体由数量未定的块组成,并以最后一个大小为0的块为结束。每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF (回车及换行),然后是数据本身,最后块CRLF结束。在一些实现中,块大小和CRLF之间填充有白空格(0x20)。最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。消息最后以CRLF结尾。
 
格式如下:
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服务端协议的解析,只考以Content-Length方式分帧的http帧,不考虑chunked编码的http协议解析。
流程图如下:

http协议解析流程图
 
如上图所示,TCP提供的是字节流,所以会出现如下三种种情况:
  1. 缺包:当前接收到的字符不够一个完整的HTTP帧
  2. 粘包:当前接收到的字符串包含一个以上的HTTP帧
  3. 当前接收到的字符串正好是一个HTTP帧

针对以上三种情况,分别要做处理,出现粘包和缺包时,我们需要缓存尚未处理的部分留待下次接收到数据时一并处理。

根据如上流程图实现的PHP程序如下:

代码如下:
<?php

/**
 * Created by PhpStorm.
 * User: Jenner
 * Date: 2015/8/10
 * Time: 14:00
 *
 * 简单http server端协议解析,实现粘包、缺包,拆包
 */


$server = new Server();
//注册响应回调函数
$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));
});

//启动server
$server->start();


/**
 * 简易http server 支持http协议解析
 * Class Server
 */
class Server
{

    /**
     * @var string 字符流缓存
     */
    protected $cache = "";

    /**
     * @var http请求处理器
     */
    protected $handler;

    /**
     * 启动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协议解析
     * @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) { // 正好是一个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 { // 发生粘包,摘出当前包,缓存剩余部分
            $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;
    }

}

 

客户端代码如下:

<?php
/**
 * Created by PhpStorm.
 * User: Jenner
 * Date: 2015/8/10
 * Time: 14:19
 *
 * 简单http socket客户端,实现一次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);

源码地址:https://github.com/huyanping/learning-http-protocol

 

原创文章,转载请注明: 转载自始终不够

本文链接地址: 简易HTTP协议解析

转载请注明:始终不够 » 简易HTTP协议解析

喜欢 (3)