1.单进程同步阻塞模型
该模型是最简单的一个模型,服务端采用单进程创建socket套接字,阻塞监听处理客户端的请求,短连接的处理流程如下图所示:

$socket = stream_socket_server('tcp://0.0.0.0:7777', $errno, $errstr);
if (!$socket) {
echo "$errstr ($errno)".PHP_EOL;
} else {
while ($conn = stream_socket_accept($socket)) {
echo '客户端连接成功'.PHP_EOL;
$content = "Hello Client,".date('Y-m-d: H:i:s', time());
$httpResponse = "HTTP/1.1 200 OK\r\n";
$httpResponse .= "Content-Type:text/html;charset=utf-8\r\n";
$httpResponse .= "Content-Length: ".strlen($content). "\r\n\r\n";
fwrite($conn, $httpResponse.$content);
Sleep(10);
fclose($conn);
}
fclose($socket);
}
优点
不需要额外的依赖,比如Web服务器,pcntl扩展等,可以搭建基于本地的http服务器用于调试web程序。
缺点
当单个进程处理耗时任务时,新的客户端再次发起请求,只能等待服务端进程处理完请求之后才能处理,所以新的客户端可能会等待超时,如果单个进程崩溃了,那么整个服务端都会面临崩溃。
2.多进程同步阻塞模型
这种模型,服务端在处理请求的时候,会创建子进程去处理客户端请求,短连接的演示流程如下图所示:

<?php
$socket = stream_socket_server('tcp://0.0.0.0:7777', $errno, $errstr);
if (!$socket) {
echo "$errstr ($errno)".PHP_EOL;
} else {
while ($conn = stream_socket_accept($socket)) {
echo '客户端连接成功'.date('Y-m-d H:i:s', time()).PHP_EOL;
$pid = pcntl_fork();
if ($pid == -1) {
die('fork failed');
} else if ($pid) {
pcntl_wait($status);
} else {
$client = fread($conn, 1024);
$content = "Hello Client,".date('Y-m-d: H:i:s', time());
$httpResponse = "HTTP/1.1 200 OK\r\n";
$httpResponse .= "Content-Type:text/html;charset=utf-8\r\n";
$httpResponse .= "Content-Length: ".strlen($content). "\r\n\r\n";
fwrite($conn, $httpResponse.$content);
sleep(10);
fclose($conn);
exit();
}
}
fclose($socket);
}
优势
每个子进程独立,互不干扰,子进程崩溃不影响其他进程。
缺点
创建进程需要申请内存空间,当请求过多的时候,会申请很多内存,内存开销比较大。 另外进程的创建与销毁开销很大,并不是立即就能完成的。
3.多进程监听同步阻塞模型
我们上面的多进程模型是接受客户端连接后创建的多进程,进程创建和销毁的开销大,不能及时的响应客户端请求,所以我们可以在监听处理上启用多进程模式

<?php
$socket = stream_socket_server('tcp://0.0.0.0:7777', $errno, $errstr);
if (!$socket) {
echo "$errstr ($errno)".PHP_EOL;
} else {
for($i = 0; $i < 4; $i ++) {
$pid = pcntl_fork();
if ($pid == -1) {
die('fork failed');
} else if ($pid[$i]) {
echo '父进程:'.$i.'id:'.$pid[$i].PHP_EOL;
} else {
while ($conn = stream_socket_accept($socket)) {
echo '客户端连接成功'.date('Y-m-d H:i:s', time()).PHP_EOL;
$pid = pcntl_fork();
if ($pid == -1) {
die('fork failed');
} else if ($pid) {
$childs[] = $pid;
} else {
$client = fread($conn, 1024);
$content = "Hello Client,".date('Y-m-d: H:i:s', time());
$httpResponse = "HTTP/1.1 200 OK\r\n";
$httpResponse .= "Content-Type:text/html;charset=utf-8\r\n";
$httpResponse .= "Content-Length: ".strlen($content). "\r\n\r\n";
fwrite($conn, $httpResponse.$content);
sleep(10);
fclose($conn);
exit();
}
}
}
}
while(count($childs) > 0) {
foreach($childs as $k=>$pid) {
$res = pcntl_waitpid($pid, $status, WNOHANG);
if ($res == -1 || $res >0) {
unset($childs[$k]);
}
}
}
fclose($socket);
}
优势
可以解决单进程监听,多进程处理客户端连接,由于进程创建与销毁开销大,而客户端无法及时响应的问题。
缺点
多进程的通病,内存开销大。
4.多线程同步阻塞模型
多线程的模型和多进程模型类似,就是把开启子进程换成开启线程,流程图参考多进程模型。
<?php
class Client extends Thread {
public function __construct($socket){
$this->socket = $socket;
$this->start();
}
public function run(){
$client = $this->socket;
if ($client) {
$header = 0;
while(($chars = socket_read($client, 1024, PHP_NORMAL_READ))) {
$head[$header]=trim($chars);
if ($header>0) {
if (!$head[$header] && !$head[$header-1])
break;
}
$header++;
}
foreach($head as $header) {
if ($header) {
$headers[]=$header;
}
}
$response = array(
"head" => array(
"HTTP/1.0 200 OK",
"Content-Type: text/html"
),
"body" => array()
);
socket_getpeername($client, $address, $port);
$response["body"][]="<html>";
$response["body"][]="<head>";
$response["body"][]="<title>Multithread Sockets PHP ({$address}:{$port})</title>";
$response["body"][]="</head>";
$response["body"][]="<body>";
$response["body"][]="<pre>";
foreach($headers as $header)
$response["body"][]="{$header}";
$response["body"][]="</pre>";
$response["body"][]="</body>";
$response["body"][]="</html>";
$response["body"] = implode("\r\n", $response["body"]);
$response["head"][] = sprintf("Content-Length: %d", strlen($response["body"]));
$response["head"] = implode("\r\n", $response["head"]);
socket_write($client, $response["head"]);
socket_write($client, "\r\n\r\n");
socket_write($client, $response["body"]);
socket_close($client);
}
}
}
$server = socket_create_listen(8787);
while(($client = socket_accept($server))){
$clients[]=new Client($client);
}
?>
优势
由于线程是共享主进程的虚拟地址空间,共享内存,所以子线程之间可以直接通信,节约内存开销,而多进程需要通过多进程的通信方式:如管道、消息队列、共享内存等手段。(PHP的多线程是基于TSRM,线程安全。并没有做到共享内存,而是把全局的资源拷贝到每一个线程中去操作,具体的介绍可以参考:http://www.laruence.com/2008/08/03/201.html
缺点
稳定性低,某个进程发生崩溃,则所有线程都会崩溃。
5.IO多路复用异步非阻塞模型
IO多路复用,指内核一旦发生进程指定的一个或多个IO条件准备读取,它就通知该进程,目前支持I/O多路复用的系统有select、poll、epoll等,多路复用的原理就是一个进程监听多个socket,一旦socket准备就绪,就通知程序进行相应的读写操作。
select
select是最早实现IO复用的,它监视并等待多个文件描述符的属性变化(可读、可写、或异常)。select函数监视文件描述符分3类,分别为writefds、readfds、exceptfds、调用后会select会阻塞,直到有描述符就绪(可读、可写或异常),或超时,函数才会返回。返回后,可以通过遍历fdset,来找到就绪的描述符,并且描述符最大不能超过1024。
poll
poll改进了select对于描述符不超过1024的问题
epoll
select和poll在返回后需要循环遍历fdset,这样对于有效数据命中率低,效率也低。epoll在linux2.6内核中提出的,是为了改进循环遍历的问题。epoll使用了事件驱动的机制,当一个描述符有变化的时候,就会告诉进程哪个连接有I/O事件流的产生,然后进程就去处理这个连接。
下面我们来基于事件驱动,实现一个简单的I/O多路复用异步非阻塞模型,首先我们看下多路复用的流程图:

<?php
$context = stream_context_create([
'socket'=> [
'backlog' => 10000,
]
]);
stream_context_set_option($context, 'socket', 'so_reuseaddr', 1);
$socket = stream_socket_server(
'tcp://0.0.0.0:7777',
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
$context
);
stream_set_blocking($socket, false);
$base = new EventBase();
$event = new Event($base,
$socket,
Event::PERSIST | Event::READ | Event::WRITE,
function($socket) use (&$base) {
$client = stream_socket_accept($socket);
echo '客户端连接:'.date('Y-m-d H:i:s', time()).PHP_EOL;
$base = new EventBase();
$event = new Event($base, $client, Event::PERSIST | Event::READ, function(){});
$event->set($base,
$client,
Event::PERSIST | Event::READ,
function($client, $what, $event) {
$msg = fread($client, 65535);
echo '客户端连接处理'.PHP_EOL;
$content='Hello Client'.date('Y-m-d H:i:s', time());
$string="HTTP/1.1 200 OK\r\n";
$string.="Content-Type: text/html;charset=utf-8\r\n";
$string.="Content-Length: ".strlen($content)."\r\n\r\n";
fwrite($client, $string.$content);
echo 'fwrite'.PHP_EOL;
fclose($client);
$event->del();
}, $event);
$event->add();
$base->loop();
});
$event->add();
$base->loop();
6.IO多路复用模型的性能对比



7.(补充)多进程监听IO多路复用异步非阻塞模型
在IO复用的基础上结合多进程监听可以做一个更加高性能的模型
<?php
$context = stream_context_create([
'socket' => [
'backlog' => 10000,
]
]);
stream_context_set_option($context, 'socket', 'so_reuseport', 1);
stream_context_set_option($context, 'socket', 'so_reuseaddr', 1);
$socket = stream_socket_server(
'tcp://0.0.0.0:7777',
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
$context
);
for ($i = 0; $i < 4; $i++) {
$pid = pcntl_fork();
if ($pid == -1) {
exit;
} else if ($pid) {
$childs[] = $pid;
} else {
stream_set_blocking($socket, false);
$base = new EventBase();
$event = new Event($base,
$socket,
Event::PERSIST | Event::READ | Event::WRITE,
function ($socket) use (&$base) {
$client = stream_socket_accept($socket);
stream_set_blocking($client, false);
echo '客户端连接:' . date('Y-m-d H:i:s', time()) . PHP_EOL;
$base = new EventBase();
$event = new Event($base, $client, Event::PERSIST | Event::READ, function ($client, $what, $argv) use (&$event) {
$msg = fread($client, 65535);
echo '客户端连接处理' . PHP_EOL;
$content = 'Hello Client' . date('Y-m-d H:i:s', time());
$string = "HTTP/1.1 200 OK\r\n";
$string .= "Content-Type: text/html;charset=utf-8\r\n";
$string .= "Content-Length: " . strlen($content) . "\r\n\r\n";
fwrite($client, $string . $content);
fclose($client);
$event->del();
});
$event->add();
$base->loop();
});
$event->add();
$base->loop();
}
}
while(count($childs) > 0) {
foreach($childs as $k=>$pid) {
$res = pcntl_waitpid($pid, $status, WNOHANG);
if ($res === -1 || $res > 0) {
unset($childs[$k]);
}
}
}
