I/O复用主要解决服务端t同时处理大量连接的场景,实际上就是一个进程/线程处理多个连接。
如果不使用I/O复用,处理多个连接就必须开多个进程/线程,一般系统能开启的进程/线程是有上限的,并且进程/线程切换开销很大,所以现在主流采用I/O复用的方式来应对高并发场景。
I/O复用实现方式主要为select,poll,epoll。
select,poll主要通过轮询的方式监听连接事件,epoll是通过事件通知监听,更加高效。nginx就是采用的epoll方式。
今天用php实现一个tcp服务器,采用select方式,主要使用stream_select函数
服务端实现
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2021/9/5
* Time: 18:54
*/
function php_tcp_server($ip='0.0.0.0', $port='6000', $recv_len=1024)
{
$sockets=[];//存放所有连接
$clients=[];//存放客户端连接
$serv = stream_socket_server("tcp://{$ip}:{$port}", $errno, $error); //创建tcp服务器套接字
$errno && exit($error);
stream_set_blocking($serv,0);//设置为异步,不然fread,stream_socket_acceptd等会堵塞
$w=[];
$e=[];
while (1){
//重新装载已存在所有的流 select每次轮询需要重新装载
$sockets=array_merge([$serv],$clients);
$res=stream_select($sockets,$w,$e,0,0);
//一旦$socket里的连接有可读事件,返回数字,代表有几个可读事件,同时$sockets里面只剩下有可读事件的连接了,所以每次轮询都要重载.
if($res===false){
message("select fail");
break;
}
if($sockets){
if(in_array($serv,$sockets)){
//如果是服务套接字有可读事件,代表有新的连接
$cli = @stream_socket_accept($serv,0);//获取新的连接
if($cli){
message("新加入:".$cli);
$clients[]=$cli;
}
$index=array_search($serv,$sockets);
unset($sockets[$index]);
}
//处理其他可读的客户端连接
foreach ($sockets as $v){
$r=fread($v,1024);
if($r===false){//判断连接是否还在
message($v." 已断开");
$index=array_search($v,$clients);
unset($clients[$index]);
}
if($r){
message($v." 收到的值:".$r);
fwrite($v,$r);//在回写给客户端
}
}
foreach ($clients as $k=>$v){//最后处理其他已经掉了的连接
if(!is_resource($v)){
unset($clients[$k]);
message($v." 已经断开");
}
}
}
usleep(100);//加个延迟,避免没有连接的时候cpu一直空转
//message("等待");
}
}
function message($str){
echo $str."\n\r";
}
php_tcp_server("0.0.0.0",6000,1024);
客户端
我这里使用了parallel多线程实现主线程发消息,副线程接受服务端消息。
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2021/9/5
* Time: 19:07
*/
// client.php
$block=new \parallel\Channel(1);
$block->send(0); //这个channel 实际为了两个线程共享数据
$r1 = new \parallel\Runtime();
$fu = $r1->run(function ()use ($block){//创建线程 该线程创建socket客户端连接服务端,并轮询获取服务端发来的数据
$host = "127.0.0.1";
$port = 6000;
$socket = stream_socket_client("tcp://{$host}:{$port}", $errno, $errMsg);
if ($socket === false) {
echo "unable to create socket: " . $errMsg."\n\r";
exit();
}
stream_set_blocking($socket, 0);//设置异步
echo"success connect to server: [{$host}:{$port}]...\n";
while (1) {
//收到消息
$msg = fread($socket, 1024);
if ($msg === false) {
echo("已经断开\n\r");
exit();
}
if($msg){
echo "收到信息:".$msg."\n\r";
}
$input=$block->recv();//获得主线程用户输入的信息
if($input!==0){
fwrite($socket,$input);//发送
}
$block->send(0);//chanel填充个0,主线程会输入数据
usleep(100);
//1.避免空转 2.因为channel的原因,不设置一定时间堵塞,有可能一直是这个线程操作channel,主线程被channel堵塞了
}
});
while (1){
//主线程主要用来发消息
$input = trim(fread(STDIN, 1000));
if($input){
$res=$block->recv();
$block->send($input);
usleep(100);
}
}
效果
服务端
客户端
这里就实现了一个简单的tcp服务端,select并不高效,有机会使用epoll实现I/O复用。