大家好,我是老李。
没想到距离上篇文章才过去仅仅半个多月就发生了这么多的事情,其之沉、其之重、其之殇,如氤氲般笼罩环绕在这片古老的大地上。钟南山眼中的泪水让我没有丝毫的心情再在文章中随手写段子,白衣天使们脸上的疲倦让我没有了任何像以往那种调侃方式写文章的感觉。可能你们不太会适应失去了段子的本公号,但是只要哪天钟佬说“ 可以了 ”,我立马就恢复如初。
众所周知我连在群里发美景图都少了好多
上一篇里我们基于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
require_once "./Lib/Http.php";
$host = '0.0.0.0';
$port = 6666;
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
socket_set_option( $listen_socket, SOL_SOCKET, SO_REUSEADDR, 1 );
socket_set_option( $listen_socket, SOL_SOCKET, SO_REUSEPORT, 1 );
socket_bind( $listen_socket, $host, $port );
socket_listen( $listen_socket );
socket_set_nonblock( $listen_socket );
socket_getsockname( $listen_socket, $addr, $port );
echo 'Select HTTP Server - '.$addr.':'.$port.PHP_EOL;
$client = array( $listen_socket );
while ( true ) {
$read = $client;
$write = array();
$exception = array();
$ret = socket_select( $read, $write, $exception, NULL );
//echo "select-loop : {$ret}".PHP_EOL.PHP_EOL.PHP_EOL;
//print_r( $read );
if ( $ret <= 0 ) {
continue;
}
// 就是说,如果 listen-socket 中有事件,listen-socket能有啥事件:就是用新的客户端来了
if ( in_array( $listen_socket, $read ) ) {
$connection_socket = socket_accept( $listen_socket );
if ( !$connection_socket ) {
continue;
}
socket_getpeername( $connection_socket, $client_ip, $client_port );
//echo "Client {$client_ip}:{$client_port}".PHP_EOL;
$client[] = $connection_socket;
$key = array_search( $listen_socket, $read );
unset( $read[ $key ] );
}
// 对于其他socket
foreach( $read as $read_key => $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
class Http {
// 定义下目前支持的http方法们,目前只支持get和post
private static $a_method = array( 'get', 'post' );
public static function decode( $s_raw_http_content ) {
$s_http_method = '';
$s_http_version = '';
$s_http_pathinfo = '';
$s_http_querystring = '';
$s_http_body_boundry = ''; // 当post方法且为form-data的时候.
$a_http_post = array();
$a_http_get = array();
$a_http_header = array();
$a_http_file = array();
// 先通过两个 \r\n\r\n 把 请求行+请求头 与 请求体 分割开来.
list( $s_http_line_and_header, $s_http_body ) = explode( "\r\n\r\n", $s_raw_http_content, 2 );
// 再分解$s_http_line_and_header数组
// 数组的第一个元素一定是 请求行
// 数组剩余所有元素就是 请求头
$a_http_line_header = explode( "\r\n", $s_http_line_and_header );
$s_http_line = $a_http_line_header[ 0 ];
unset( $a_http_line_header[ 0 ] );
$a_http_raw_header = $a_http_line_header;
// 好了,请求行 + 请求头数组 + 请求体 都有了
// 先从请求行分解 method + pathinfo + querystring + http版本
list( $s_http_method, $s_http_pathinfo_querystring, $s_http_version ) = explode( ' ', $s_http_line );
if ( false === strpos( $s_http_pathinfo_querystring, "?" ) ) {
$s_http_pathinfo = $s_http_pathinfo_querystring;
} else {
list( $s_http_pathinfo, $s_http_querystring ) = explode( '?', $s_http_pathinfo_querystring );
}
// 处理querystring为数组
if ( '' != $s_http_querystring ) {
$a_raw_http_get = explode( '&', $s_http_querystring );
foreach( $a_raw_http_get as $s_http_get_item ) {
if ( '' != trim( $s_http_get_item ) ) {
list( $s_get_key, $s_get_value ) = explode( '=', $s_http_get_item );
$a_http_get[ $s_get_key ] = $s_get_value;
}
}
}
// 处理$s_http_header
foreach( $a_http_raw_header as $a_raw_http_header_key => $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请求数据粘贴过来大家一起感受一下:
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请求的数据是如何构成的,掰扯清楚后一切都会变得明朗:
明白了GET请求发过来的HTTP原始数据构成后,那么使用PHP相关函数很容易就可以进行解析操作,我把上面解析HTTP协议中的一段再次拿过来你们感受下(注意注释):
<?php
// 数组剩余所有元素就是 请求头
$a_http_line_header = explode( "\r\n", $s_http_line_and_header );
// 这个就是请求行
// 也就是说$s_http_line中保存的就是:
// GET /user/login?username=wahaha&password=123456 HTTP/1.1
$s_http_line = $a_http_line_header[ 0 ];
unset( $a_http_line_header[ 0 ] );
$a_http_raw_header = $a_http_line_header;
list( $s_http_method, $s_http_pathinfo_querystring, $s_http_version ) = explode( ' ', $s_http_line );
if ( false === strpos( $s_http_pathinfo_querystring, "?" ) ) {
$s_http_pathinfo = $s_http_pathinfo_querystring;
} else {
list( $s_http_pathinfo, $s_http_querystring ) = explode( '?', $s_http_pathinfo_querystring );
}
// 处理querystring为数组
// 比如username=xxx&password=yyy&gener=1这种,处理好
if ( '' != $s_http_querystring ) {
$a_raw_http_get = explode( '&', $s_http_querystring );
foreach( $a_raw_http_get as $s_http_get_item ) {
if ( '' != trim( $s_http_get_item ) ) {
list( $s_get_key, $s_get_value ) = explode( '=', $s_http_get_item );
$a_http_get[ $s_get_key ] = $s_get_value;
}
}
}
// 处理$s_http_header
// http headers在这里处理
foreach( $a_http_raw_header as $a_raw_http_header_key => $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)然后我抓原文贴过来大家一起感受一下:
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
来,解析一下:
啊哈~这下结构摸清楚了,使用PHP语言中的相关函数一顿操作就可以解析POST请求了。在我们平时使用$_POST超级数组的时候,想必一定就是某个环节(主要是我不好确定是nginx还是fpm来解析)中对[ 请求体 ]进行解析。
我们demo里的代码对POST请求解析完成后,我使用print_r打印一下,你们可以感受一下,大概是这样shai儿的:
POST请求这里,我还额外跟大家补充三个值得关注的HTTP Header:
通过上面两个实战级的解析研究,我觉得大家应该改变一下学习HTTP协议的方式和方法,我一再强调不要强行背诵那些302、504是什么含义、也不要使劲记忆GET、POST有什么区别,其实从根本上去动手研究解析一种协议要比背诵协议表象要有用的多。对协议不要有恐惧感,他们只是人类制定出来的规范而已,那么这个规范在什么地方呢?
所有正规无误的HTTP协议规范标准都安安稳稳地躺在https://www.w3.org/Protocols/中,还有很多RFC都在这里,比如这个RFC草案https://tools.ietf.org/html/rfc2616。你们以为我在文章开头写的[ 与你们日常从百度上搜出来的绝大多数CSDN关于HTTP文章 ]是插科打诨么?是的...除了插科打诨还能凑文章字数...
在必要时刻,年轻人一定要学会通过正规官方渠道去获取信息、研究信息、提出质疑、去伪存真。如果连获取权威信息都做不到,不要提研究和熟悉信息,如果连研究和熟悉信息都做不到,就更不要提提出质疑权威和疑问权威了,还谈什么去伪存真、独立思考?
今天我多聊这些就是想趁着疫情这个特殊时期告诉诸位,要想独立思考、提出质疑,第一点要做的就是知道从什么渠道去获取正规正式信息,第二是获取信息要尽快熟悉信息、了解其规则,其次最后一步才是结合这些信息通过自己思考加上自己理解提出质疑或疑问或意见。而我见到的大多数人,不是这样的,虽然有些人口口声声喊着[ 独立思考 ],可他们甩来甩去的只有不知道从哪儿得到的几张weibo截图或wechat聊天记录截图。
键盘打字容易,独立思考难啊