php stream_socket创建tcp服务端
PHP 基于stream实现TCP 协议
使用php 自带的stream函数组 理解tcp协议.
所用到的工具和API:
- nc,瑞士军刀
- postman
- stream 函数
- event 类
一个简单tcp服务端
主要用到两个函数,stream_socket_server和stream_socket_accept
stream_socket_server,创建服务端socket
stream_socket_server( string $address,// 类型 ,如tcp协议: tcp://host:port int &$error_code = null, // 错误码 string &$error_message = null,// 错误消息 int $flags STREAM_SERVER_BIND | STREAM_SERVER_LISTEN , ?resource $context=null, ): resource|false
stream_socket_accept,接受socket套接字连接. 一直等待有请求或者超时
stream_socket_accept( resource $socket,// stream_socket_server创建的socket ?float $timeout = null,// 等待超时时间,单位秒,默认ini_get('default_socket_timeout') string &$peer_name = null,// 已存在的连接客户端地址,通过stream_socket_get_name()获取 )
完整示例,创建一个监听8000端口的tcp服务:
// 服务端
$socket=stream_socket_server("tcp://0.0.0.0:8000");
// 等待连接
$connect=stream_socket_accept($socket);
// 打印连接信息
printf("connect:%s",stream_socket_get_name($connect,true));
使用NC 测试连接.
nc -v 127.0.0.1 8000
结果
返回successed 说明成功.
使用curl 也能成功,只是没有返回内容.
Connection reset by peer
是服务器关闭了.没有返回内容.
通过netstat
或 lsof
查看进程情况
小技巧:
利用linux 的watch 命令实时那刷新命令,类似top命令.
watch -n 1 "lsof -i -P -n | grep php"
添加延迟退出命令,sleep,就能查看请求过程.
我们已经知道两种状态:LISTEN
,ESTABLISHED
PS: TCP常见的几种状态
- CLOSED(已关闭):表示连接尚未建立或已经关闭。
- LISTEN(监听):表示服务器正在等待传入连接请求。
- SYN-SENT(已发送同步):表示客户端已发送连接请求,并等待服务器的确认。
- SYN-RECEIVED(已接收同步):表示服务器已收到客户端的连接请求,并发送确认。
- ESTABLISHED(已建立):表示连接已成功建立,双方可以进行数据传输。
- FIN-WAIT-1(等待关闭):表示连接的一方已发送关闭请求,等待另一方的确认。
- FIN-WAIT-2(等待关闭):表示连接的一方已收到关闭请求的确认,等待对方发送关闭请求。
- CLOSE-WAIT(等待关闭):表示连接的一方已收到关闭请求,等待自己的应用程序关闭连接。
- CLOSING(关闭中):表示双方同时发送关闭请求,正在关闭连接。
- LAST-ACK(最后确认):表示连接的一方已发送关闭请求的确认,等待对方的关闭请求。
- TIME-WAIT(时间等待):表示连接已关闭,但仍保留在系统中一段时间,以确保在网络中的延迟数据包被丢弃。
- CLOSE(关闭):表示连接已经关闭。
获取请求内容
实际就是读取stream,和php读文件一样的操作.
$message= fread($connect, 1024);
printf("message:%s \n",$message);
或stream_socket_recvfrom
$message= stream_socket_recvfrom($connect, 1024);
ps: nc使用管道操作传内容.
echo "hello" | nc 127.0.0.1:8000
返回内容
其实就是写入fput或者fwrite 都可以.
fwrite($connect,"hello world!");
或stream_socket_sendto
stream_socket_sendto($connect,"hello world!");
HTTP服务器
上面返回内容,但浏览器不显示.
只需要组装一个http协议头的返回值就可以了.
// 协议头
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html; charset=UTF-8\r\n";
$response .= "\r\n";
// 内容
$response .= "<html><body><h1>Hello, world!</h1></body></html>";
// 返回内容
fwrite($connect,$response);
完整代码:
// 服务端
$socket=stream_socket_server("tcp://0.0.0.0:8000");
// 连接
$connect=stream_socket_accept($socket);
// 连接信息
printf("connect:%s \n",stream_socket_get_name($connect,true));
// 获取内容
$message= fread($connect, 1024);
printf("message:%s \n",$message);
// 返回内容
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html; charset=UTF-8\r\n";
$response .= "\r\n";
$response .= "<html><body><h1>Hello, world!</h1></body></html>";
fwrite($connect,$response);
fclose($connect);
fclose($socket);
多连接
上面演示只是一次性请求,请求一次服务器就断开了. 支持多个请求.
把客户端处理封装成一个函数,反复调用就可以了.
// 服务端
$socket=stream_socket_server("tcp://0.0.0.0:8000");
function receive():void {
global $socket;
// 连接
$connect=stream_socket_accept($socket,3);
// 连接信息
printf("connect:%s \n",stream_socket_get_name($connect,true));
// 获取内容
$message= fread($connect, 1024);
printf("message:%s \n",$message);
// 返回内容
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html; charset=UTF-8\r\n";
$response .= "\r\n";
$response .= "<html><body><h1>Hello, world!</h1></body></html>";
fwrite($connect,$response);
fclose($connect);
// 重复监听
receive();
}
receive();
ps: 递归调用会无限增加调用栈,内存会被占满,导致系统崩溃,上面只是做为演示连接,真正使用时不会这么用.
比如,下面代码,运行到一定程度就会崩溃
function foo(){
global $num;
$num++;
count(debug_backtrace());// 堆栈数会无限多,导致系统变慢或者内存消耗光而崩溃.
foo();
}
foo();
改成循环方式:
$socket=stream_socket_server("tcp://0.0.0.0:8000");
while(true){
$connect = @stream_socket_accept($socket,1);
if(!$connect){
continue;
}
// onConnect
echo "onConnect:".stream_socket_get_name($connect,true).PHP_EOL;
// onRequest
$request=fread($connect, 1024);
// onResponse
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html; charset=UTF-8\r\n";
$response .= "\r\n";
$response .= "<html><body><h1>Hello, world!</h1></body></html>";
fwrite($connect, $response);
fclose($connect);
}
fclose($socket);
超时处理
stream_socket_accept,会hold住程序,一直等待请求.直至超时.
$socket=stream_socket_server("tcp://0.0.0.0:8000");
$connect = stream_socket_accept($socket,3);
if(!$connect){
echo "超时";
}
echo "应用结束".PHP_EOL;
在3秒后,没有应用请求,就会自动超时,报Warning错误.
- 错误捕捉
...
set_error_handler(function ($errCode,$errMsg){
echo $errMsg.PHP_EOL;
// 重新监听
});
$connect = stream_socket_accept($socket,3);
restore_error_handler();
...
PS:
ps: restore_error_handler 重置错误捕捉,相当于注销上面的set_error_handler函数.
- $connect 返回false,有可能是由于超时引起的.
多客户端请求
注意: 这里的并发,指并发多个连接. 而不是并发处理.
上面虽然通解决了多个链接,但一次只能处理一次请求.需要等待一个请求结束才能发起下一个请求,不支持并发请求.
ab -n 100 -c 10 http://127.0.0.1:8000/
并发10个请求.
如果处理太慢就有大量请求就卡住了.
服务端只保持一个客户端,如何支持多个客户端连接?
通过 stream_set_blocking
和 timeout=-1
来一直监听新连接.
stream_set_blocking($socket,false)
不阻塞读取或写入,但不知道状态. 不能等待socket状态,变成异步状态.
stream_socket_accept的timeout=-1
立马超时,不等待. 只有等下次循环才能获取和判断状态.
$socket=stream_socket_server("tcp://0.0.0.0:8000");
// 不阻塞
stream_set_blocking($socket,false);
// 客户端列表
$clients=[];
while(true){
// 超时设为-1意为不等待,如果有连接加入客户端列表
$connect = @stream_socket_accept($socket,-1,$remoteAddress);
if($connect){
// onConnect
echo "onConnect:".$remoteAddress.PHP_EOL;
$clients[$remoteAddress]=$connect;
stream_set_blocking($connect,false);
}
// TODO::批量处理clients,发送,接收或关闭
}
上面只加入,不处理,不关闭. 都处理等待状态.可以看到服务端可以同时处理3个连接.而不是等待处理其他连接处理完成,才能建立链接.
I/O多路复用
上面通过循环不阻塞方式,再循环处理,无论数据有没有变动,clients都需要遍历一遍.
只监听连接数据变动可以使用系统多路复用技术,有 select、poll、epoll.
select
缺点是,最大只支持1024个请求.
$server=stream_socket_server("tcp://0.0.0.0:8000",$errno, $errstr);
if (!$server) {
die("$errstr ($errno)");
}
stream_set_blocking($server , false);
$clients["main"] = $server;
while(1){
// 拷贝一份,以防select修改原socket
$read = $clients;
$ret = @stream_select($read, $write, $except,0);
if($ret === false){
break;
}
foreach ($read as $k=>$client) {
// 主socket
if($client === $server){
// 创建新客户端
$conn = @stream_socket_accept($server,-1,$remoteAddress);
if(!$conn){
break;
}
$clients[$remoteAddress] = $conn;
echo "total: ".(count($clients)-1)." client\n";
continue;
}
// TODO::处理请求
}
}
poll
poll和select 区别不大,只是支持更多的连接. php没有支持.
epoll
epoll 需要php安装event扩展.参考:https://www.php.net/manual/zh/book.event.php
先了解event的操作,event事件收集和触发器.
event类核心方法.
EventBase类
,事件容器.new EventBase( ?EventConfig $conf,// 事件配置对象 )
Event类
, 事件new Event( // EventBase对象 EventBase $base , //文件描述符,可以是socket,stream,时钟,信号. 当为时钟时为"-1",信号为数字常量,如SIGHUP mixed $fd, /** 事件类型,可选值 * Event::READ:表示当文件描述符准备好"读取"时触发事件。 * Event::WRITE:表示当文件描述符准备好"写入"时触发事件。 * Event::SIGNAL:表示用于信号的事件。 * Event::TIMEOUT:表示在超时后触发事件。 * Event::PERSIST: 表示事件持久化. 如Event::TIMEOUT|Event::PERSIST,又被挂起,相当于重置一样. */ int $what, // 事件回调函数,callback( mixed $fd = null , int $what = ?, mixed $arg = null ):void callable $cb , // 自定义变量 mixed $arg = NULL, )
操作方法
Event::add(float $timeout=-1);// 添加事件,时间为秒,支持小数. 如果将一直挂着. Event::del();// 删除事件 EventBase::loop();// 分发信号
小例:
a. 定时器,每隔一段时间,执行callback
$eventBase=new EventBase();
// 定义一个类型为TIMEOUT的事件
$timeEvent=new Event($eventBase,-1,Event::TIMEOUT|Event::PERSIST,function(){
echo microtime(true).PHP_EOL;
});
// 添加事件,1秒钟后执行
$timeEvent->add(1);
// 等待执行
$eventBase->loop();
b. 信号量触发
$eventBase=new EventBase();
// 定义一个类型为信号量的事件
$timeEvent=new Event($eventBase,SIGTERM,Event::SIGNAL,function(){
echo "关闭事件".PHP_EOL;
});
// 添加事件,不指定超时
$timeEvent->add();
// 等待执行
$eventBase->loop();
c. socket 变化事件
$eventBase=new EventBase();
$socket=stream_socket_server("tcp://0.0.0.0:8000");
stream_set_blocking($socket, false);
$pid=stream_socket_get_name($socket,true);
$events[$pid]=$socket;
// 循环监听socket变化
$serEvent=new Event($eventBase,$socket,Event::READ|Event::PERSIST,function ()use($socket){
global $eventBase;
global $events;// 必须全局,不能引用.不然连接后丢失断开.
$connect=@stream_socket_accept($socket,-1,$id);
echo "连接:".$id.PHP_EOL;
if(!$connect){
return;
}
stream_set_blocking($connect, false);
// 循环监听客户端连接状态
$event=new Event($eventBase,$connect,Event::READ|Event::PERSIST
,function ($fd,$that,$id)use($connect){
global $events;
echo "msg:".stream_socket_recvfrom($connect,1024).PHP_EOL;
stream_socket_sendto($connect,"ok.".$id);
fclose($connect);
unset($events[$id]);
},$id);
$event->add();
$events[$id]=$event;// 添加到全局
});
$serEvent->add();
// 等待执行
$eventBase->loop();
查看.使用的是什么I/O复用?
// 支持的方法
printf("methods:%s \n",implode(',',Event::getSupportedMethods()));
$eventConfig = new EventConfig;
// 禁用select
$eventConfig->avoidMethod('select');
$eventConfig->requireFeatures(EventConfig::FEATURE_ET);// 边缘触发
$eventBase=new EventBase($eventConfig);
// 当前使用到的
echo "i/o method:".$eventBase->getMethod().PHP_EOL;
echo "feature:",$eventBase->getFeatures().PHP_EOL;
原作者:阿金
本文地址:https://hi-arkin.com/archives/php-stream-tcp.html