今天我就要把我的最新研究成果展示看看,而不玩ARMBIAN了,因为刷了两台S905L3的发现一点挑战都没有从0.2写WEB服务难啊,你需要懂HTTP协议和SOCKET!不过有经验我们很快就可以搭建一个
(资料图)
0.HTTP
为了准确了解HTTP服务,我写了个简单的代理脚本,源码放这了:
输入php test.php
,然后访问IP:81,就会看见终端出现:
为什么明明就一次循环输出了多次数据?哈哈,因为请求和响应标头里有一个神奇的KEEP-ALIVE,自行百度吧
这样,我们就可以发现,客户端第一行包括了路径、请求方式,接下来就是各种参数服务端响应第一部分是响应头,包括状态码、服务程序名称、响应时间、响应文件类型等第二部分与第一部分的分割线是一个换行\r\n,接下来就是主要的数据了。就这么简单!不要怕,知道了这些,我们可以轻轻松松写一个WEB SERVER
1.了解流程
socket其实很简单!看看大神的文章马上就懂了:https://zhuanlan.zhihu.com/p/260139078首先我们需要启动一个TCP监听,我们无需管底层的解析,socket已经为我们解决了这些我们首先读取客户端发来哪些数据,再决定发回哪些数据,最后有一端关闭连接就算完成了
2.小试牛刀
这样,我们就可以写一个简单的程序来小试牛刀了:
$http_port = 88;$s = socket_create(AF_INET6,SOCK_STREAM,SOL_TCP); // 创建一个socket实例socket_set_block($s); // 阻塞模式,如socket_accept()会等待有客户端连接才下一步,一般可以省很多事socket_bind($s,"::",$http_port) or die("io.http : 无法绑定HTTP服务."); // 绑定端口socket_listen($s,4) or die("io.http : 监听失败!"); // 开始监听,最大队列为4
有了监听事情好了大半,接下来有连接了会放在队列里,等待我们接受(accept)它们。如何接受这些请求呢?$c = socket_accept($s);
就行了
$c很重要!我们可以通过socket函数执行很多操作读取对方IP到变量$ip:socket_getpeername($c,$ip);
读取对方访问路径:list($proto,$path,$version) = explode(" ",socket_read($c , 1*1024*1024,PHP_NORMAL_READ));
注释:$proto:协议,如GET POST PUT DELETE......$path:请求的目录,如 /testversion:HTTP版本号,直接忽略即可PHP_NORMAL_READ:读一行,这对我们来说正好可以满足需求(重要的信息都在第一行)写东西到缓冲区socket_write($socket,"");
注意只是写到了缓冲区,我们无法保证此时用户接收到了数据,贸然socket_close()
会出错:连接已重置比如写响应头:
socket_write($c,"HTTP/1.1 404 Not FoundDate: Tue, 27 Dec 2022 06:45:30 GMTContent-Type: text/html; charset=UTF-8\r\n");
接着写数据,比如Hello world!: Hello world!socket_write($c,"
最后可选平滑关闭连接socket_shutdown($c);
或者强制关闭连接socket_close($c);
结束!我们放到一起看看效果:
OKOK!可以看见我们已经成功了!但是只能接收一次数据实在是太难受了,我们接下来改进一下:
3.改进
3.1 HEADER
说响应头简单倒真的很简单,就是KEY: VALUE\n的结构,那我们可以包装一下传入数组输出heaerHTTP状态码我们不要了,反正浏览器也不会看,它懂这些我们写了也没有用
function create_header(array $header,int $status = 200){ $tmp = "HTTP/1.1 $status"; foreach(array_merge([ "Server"=>"ioPan", // 服务程序 "Content-Type"=>"text/html ;charset=UTF-8",// 默认响应HTML "Date"=>gmdate("D, d M Y H:i:s T")// 响应时间 ],$header) as $n=>$v){ $tmp .= "\r\n$n: $v"; // 对应key:value\n } return $tmp."\r\n\r\n"; // header结束了需要两个换行}
3.2 循环结构
只要让socket_accept()循环不就可以一直为我们工作了吗?好,我们用while循环下去:
while(true){ $_ = socket_accept($s); socket_getpeername($_,$ip); list($proto,$path,$version) = explode(" ",socket_read($_ , 1*1024*1024,PHP_NORMAL_READ)); echo "io.http : 新的客户端连接 IP[$ip];请求路径:[$path]\n"; // 干些活 socket_write($_,create_header([],200)); socket_write($_,"Hello world!
");}
我就偷懒不演示了,可以发现好几个客户端同时访问也几乎没什么问题。
3.3 本地文件
这个最简单,只要判断本地有文件且可读,返回状态码200,调用fread()读取输出文件,如果请求的地址文件不存在或不可读,返回状态码404,展示404.html,如果是个目录返回403。当然如果你愿意可以再加Range(断点续传)的支持
if(is_dir($path) or !is_readable($path)) socket_write($_,create_header([],304));else socket_write($_,create_header([ "Content-Length"=>filesize($path), "Content-Type"=>"application/octet-stream"],200)) and socket_write($_,file_get_contents($path));
4.showTime
把上述代码组装起来,我们就成功写了个高性能的WEB SERVER,不过只有单进程,一次只能接受一个请求。改进:使用端口复用(socket_set_option($s,SOL_SOCKET, SO_REUSEADDR ,true);
)并且使用POSIX多进程(pcntl_fork()
),但是由于Windows不支持POSIX,我就不演示了