/首页
/开源
/关于
PHP网络编程-IO复用之基于select的简易HTTP服务器(十一)
发表@2020-01-31 16:19:00
更新@2023-03-27 19:06:40
大家好,我是老李。 没想到距离上篇文章才过去仅仅半个多月就发生了这么多的事情,其之沉、其之重、其之殇,如氤氲般笼罩环绕在这片古老的大地上。钟南山眼中的泪水让我没有丝毫的心情再在文章中随手写段子,白衣天使们脸上的疲倦让我没有了任何像以往那种调侃方式写文章的感觉。可能你们不太会适应失去了段子的本公号,但是只要哪天钟佬说“ 可以了 ”,我立马就恢复如初。 上一篇里我们基于select系统调用实现了一个非常粗暴的多人群聊聊天室,而且还夹杂解释了网上随处可见的[ 异步 ]与[ 非阻塞 ]等概念。今天我们将再接再厉再继续了解select系统调用的同时,趁热补一波儿关于HTTP协议的基础知识。与你们日常从百度上搜出来的绝大多数CSDN关于HTTP文章不同的是,我不会介绍404、302代表什么意思、也不详解GET、POST方法的区别,我试图通过一种其他的方式来简单介绍下HTTP协议。 所以本篇文章任务只有两个,写一个基于select IO的服务器,写一个解析HTTP协议的库文件。前者实际上我们直接拿过来上一篇文章中的demo就可以直接用,后者真的一滴也没有,需要从零开始撸一个,但是作为demo也不可能撸一个完整的,所以我们的目标是:没有蛀...目标是能用于解析GET方法、POST方法且Content-Type为application/x-www-form-urlencoded(粗暴说就是我们平时网页里用的最多的不包括文件上传功能的普通表单)! 首先,我把demo复制过来,你们负责粘贴走,然后试试看看能不能跑起来,好吧?这个demo主要由两个文件组成,一个文件中是基于select的服务器代码(请留意43行前面的注释),另一个文件中是HTTP协议解析代码。 服务器代码在这里,请复制并粘贴: ```php $read_fd ) { // 注意!这种获取HTTP数据的方式并不正确 // 这种写法只能获取固定2048长度的数据 // 正规正确的写法应该是通过content-length或者chunk size // 来获取完整http原始数据 $ret = socket_recv( $read_fd, $recv_content, 2048, 0 ); //var_dump( $ret ); //echo $recv_content; $decode_ret = Http::decode( $recv_content ); print_r( $decode_ret ); $encode_ret = Http::encode( array( 'username' => "wahaha", ) ); socket_write( $read_fd, $encode_ret, strlen( $encode_ret ) ); //socket_shutdown( $read_fd ); socket_close( $read_fd ); unset( $read[ $read_key ] ); $key = array_search( $read_fd, $client ); unset( $client[ $read_key ] ); } } ``` HTTP协议解析代码,请复制走并粘贴: ```php $a_raw_http_header_item ) { if ( '' != trim( $a_raw_http_header_item ) ) { list( $s_http_header_key, $s_http_header_value ) = explode( ":", $a_raw_http_header_item ); $a_http_header[ strtoupper( $s_http_header_key ) ] = $s_http_header_value; } } // 如果是post方法,处理post body if ( 'post' === strtolower( $s_http_method ) ) { // post 方法里要关注几种不同的content-type // x-www-form-urlencoded if ( 'application/x-www-form-urlencoded' == trim( $a_http_header['CONTENT-TYPE'] ) ) { $a_http_raw_post = explode( "&", $s_http_body ); // 解析http body foreach( $a_http_raw_post as $s_http_raw_body_item ) { if ( '' != $s_http_raw_body_item ) { list( $s_http_raw_body_key, $s_http_raw_body_value ) = explode( "=", $s_http_raw_body_item ); $a_http_post[ $s_http_raw_body_key ] = $s_http_raw_body_value; } } } // form-data if ( false !== strpos( $a_http_header['CONTENT-TYPE'], 'multipart/form-data' ) ) { list( $s_http_header_content_type, $s_http_body_raw_boundry ) = explode( ';', $a_http_header['CONTENT-TYPE'] ); $a_http_header['CONTENT-TYPE'] = trim( $s_http_header_content_type ); list( $_temp_unused, $s_http_body_boundry ) = explode( '=', $s_http_body_raw_boundry ); $s_http_body_boundry = '--'.$s_http_body_boundry; $a_http_raw_post = explode( $s_http_body_boundry."\r\n", $s_http_body ); foreach( $a_http_raw_post as $s_http_raw_body_item ) { if ( '' != trim( $s_http_raw_body_item ) ) { echo $s_http_raw_body_item; //$a_http_raw_body_item = explode( ';', $s_http_raw_body_item ); // 判断是 } } } } // 整理数据 $a_ret = array( 'method' => $s_http_method, 'version' => $s_http_version, 'pathinfo' => $s_http_pathinfo, 'post' => $a_http_post, 'get' => $a_http_get, 'header' => $a_http_header, ); return $a_ret; } public static function encode( $a_data ) { $s_data = json_encode( $a_data ); $s_http_line = "HTTP/1.1 200 OK"; $a_http_header = array( "Date" => gmdate( "M d Y H:i:s", time() ), "Content-Type" => "application/json", "Content-Length" => strlen( $s_data ), ); $s_http_header = ''; foreach( $a_http_header as $s_http_header_key => $s_http_header_item ) { $_s_header_line = $s_http_header_key.': '.$s_http_header_item; $s_http_header = $s_http_header.$_s_header_line."\r\n"; } $s_ret = $s_http_line."\r\n".$s_http_header."\r\n".$s_data; return $s_ret; } } ``` 先用GET方法飞一把数据,你们感受一下:  我把读取到的curl发出的http请求数据粘贴过来大家一起感受一下: ```php GET /user/login?username=wahaha&password=123456 HTTP/1.1 Host: 127.0.0.1:6666 User-Agent: curl/7.54.0 Accept: */* ``` 注意第5行,不是我搞多了眼花手抖,因为收到的数据就是这样shai儿的,我来说明下GET请求的数据是如何构成的,掰扯清楚后一切都会变得明朗: - 第1行,叫做请求行(Reqeust Line),其中GET就是请求方法,其中/user/login?username=wahaha&password=123456是具体请求参数与地址,其中HTTP/1.1表示HTTP协议版本,这三个信息中间用空格隔开。比如我们使用PHP时候获取请求方法、querystring、pathinfo信息就是通过解析这一行来获取的 - 第2-4行,叫做请求头(Header),每个请求头结束后用一个[ 回车换行符 ]结尾。比如我们使用PHP时候获取http header的一些函数就是通过解析这几行获取到的数据 - 请求行(Reqeust Line)和请求头(Header)之间通过一个[ 回车换行符 ]分割 - 第5行看起来是个空行,其实不是,这是一个肉眼不可见的[ 回车换行符 ] 白了GET请求发过来的HTTP原始数据构成后,那么使用PHP相关函数很容易就可以进行解析操作,我把上面解析HTTP协议中的一段再次拿过来你们感受下(注意注释): ```php $a_raw_http_header_item ) { if ( '' != trim( $a_raw_http_header_item ) ) { list( $s_http_header_key, $s_http_header_value ) = explode( ":", $a_raw_http_header_item ); $a_http_header[ strtoupper( $s_http_header_key ) ] = $s_http_header_value; } } ``` 我们可以通过在服务器代码中将解析后的HTTP打印一下,依然通过下面这行CURL来测试下: curl -X GET "http://127.0.0.1:6666/user/info?username=etc&password=yahahh&gender=1" 此处需要提醒的是curl本身默认是发出HTTP协议请求的,部分腿子可能是没有意识到的。  那么POST方法呢?前面我们说GET方法中按照构成是由[ 请求行 ]+[ 请求头 ]构成的,其分隔符就是[ 回车换行符 ],其实POST方法就比GET方法多出一个[ 请求体 ]的概念,我拿POSTMAN来搞个POST请求(Content-Type为x-www-form-urlencoded)然后我抓原文贴过来大家一起感受一下: ```php POST /v1/user/login?version=1.1 HTTP/1.1 Content-Type: application/x-www-form-urlencoded User-Agent: PostmanRuntime/7.22.0 Accept: */* Cache-Control: no-cache Postman-Token: b6216148-cc1e-4b1b-8127-cba6082b911a Host: 127.0.0.1:6666 Accept-Encoding: gzip, deflate, br Content-Length: 32 Connection: keep-alive username=wahahha&password=123456 ``` 来,解析一下: - 第1行,不用解释 - 第2-10行,不用解释 - 第11行,看起来是啥都没有,实际上是一个[ 回车换行符 ] - 第12行,这就是[ 请求体 ]咯,而username和password就是网页表单里的项。在点击提交后,表单里的数据项就是就按照key=value形式中间以&符号拼接后发送给服务器的。 - [ 请求体 ]和[ 请求头 ]之间用了两个[ 回车换行符 ]来分割的。为啥是两个?第10行末尾就有一个[ 回车换行符 ],再加上第11行的那个[ 回车换行符 ],所以一共是两个。 啊哈~这下结构摸清楚了,使用PHP语言中的相关函数一顿操作就可以解析POST请求了。在我们平时使用$_POST超级数组的时候,想必一定就是某个环节(主要是我不好确定是nginx还是fpm来解析)中对[ 请求体 ]进行解析。 我们demo里的代码对POST请求解析完成后,我使用print_r打印一下,你们可以感受一下,大概是这样shai儿的:  POST请求这里,我还额外跟大家补充三个值得关注的HTTP Header: Content-Type,这个header非常有意思,很多基础松软无力不持久的后端和前端经常会因为这个header引起的问题互喷、菜鸡互啄,啥问题呢?前端说我发送数据了,后端说我没收到。前端POST飞数据的时候,Content-Type可能是application/json,而后端接受数据的时候可能用的是x-www-form-urlencoded,这要是联调通过对上号,母猪自己都能治疗自己的猪瘟。简单说下,x-www-form-urlencoded是我们最常用的形式,比如网页里的表单用的就是这个,PHP作为接收方此时只需要使用$_POST就可以接受数据;multipart/form-data是仅次于x-www-form-urlencoded的,只要表单里含有文件上传域Content-Type就会采用multipart/form-data,此时PHP要通过$_POST以及$_FILES超级变量来接受数据;application/json顾名思义,就是json文本,实际上json文本通过text/plain也完全没问题的。这个header还有很多很多其他值,有兴趣的泥腿子们可以去搜集了解下,不过按我理解,大可不必背诵记忆 Content-Length,当客户端发出POST请求后,这个header实际上是告诉服务器发送的数据有多长,这里一共需要分四种情况来说明:HTTP请求时Content-Length大于实际长度,HTTP请求时Content-Length小于长度,HTTP响应时Content-Length大于实际长度,HTTP响应时Content-Length小于长度。我们说下前两种情况,后两种情况当课后作业(实际上是我懒,懒得验证了)。当HTTP请求时Content-Length大于实际长度的时候,服务器会一直等,因为提交来的参数长度还没有达到Content-Length指定的长度,TA就一直等等到超时,期间不会有任何响应;HTTP请求时Content-Length小于长度时比较粗暴,参数会被直接截断。以上通过Nginx+POSTMAN测试验证 Transfer-Encoding: chunked,当很不幸有些时候无法明确得到Content-Length的时候,会采用Transfer-Encoding: chunked说白了也就是数据分块,此时虽然无法告诉服务器所有整体的数据大小,但是可以将分块后的数据大小告诉服务器。值得注意的是当HTTP Header中同时存在Transfer-Encoding: chunked与Content-Length时,将以Transfer-Encoding: chunked为准。如果大家读过Workerman源码,就应该知道截止到目前我正在写的这篇文章的时候,Workerman的HTTP服务器还是不支持Transfer-Encoding: chunked的,这一点作者李亮也曾经确认过 通过上面两个实战级的解析研究,我觉得大家应该改变一下学习HTTP协议的方式和方法,我一再强调不要强行背诵那些302、504是什么含义、也不要使劲记忆GET、POST有什么区别,其实从根本上去动手研究解析一种协议要比背诵协议表象要有用的多。对协议不要有恐惧感,他们只是人类制定出来的规范而已,那么这个规范在什么地方呢? 所有正规无误的HTTP协议规范标准都安安稳稳地躺在https://www.w3.org/Protocols/中,还有很多RFC都在这里,比如这个RFC草案https://tools.ietf.org/html/rfc2616。你们以为我在文章开头写的[ 与你们日常从百度上搜出来的绝大多数CSDN关于HTTP文章 ]是插科打诨么?是的...除了插科打诨还能凑文章字数... 在必要时刻,年轻人一定要学会通过正规官方渠道去获取信息、研究信息、提出质疑、去伪存真。如果连获取权威信息都做不到,不要提研究和熟悉信息,如果连研究和熟悉信息都做不到,就更不要提提出质疑权威和疑问权威了,还谈什么去伪存真、独立思考? 今天我多聊这些就是想趁着疫情这个特殊时期告诉诸位,要想独立思考、提出质疑,第一点要做的就是知道从什么渠道去获取正规正式信息,第二是获取信息要尽快熟悉信息、了解其规则,其次最后一步才是结合这些信息通过自己思考加上自己理解提出质疑或疑问或意见。而我见到的大多数人,不是这样的,虽然有些人口口声声喊着[ 独立思考 ],可他们甩来甩去的只有不知道从哪儿得到的几张weibo截图或wechat聊天记录截图。
键盘打字容易,独立思考难啊