简易HTTP协议解析

    首先介绍一些必要的知识点。
       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结尾。
 
格式如下:
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服务端协议的解析,只考以Content-Length方式分帧的http帧,不考虑chunked编码的http协议解析。
流程图如下:

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

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

根据如上流程图实现的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端协议解析,实现粘包、缺包,拆包
  */
 
 
$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;
     }
 
}

 

客户端代码如下:

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客户端,实现一次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协议解析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值