1.1. IO 模型笔记
1.1.1. BIO
测试代码
- 使用 php 的进程模式 如果是 java 可以理解为线程也是一样的
<?php
set_time_limit(0);
error_reporting(E_ALL ^ E_NOTICE);
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$ip = '127.0.0.1';
$port = 8089;
$res = socket_bind($socket, $ip, $port);
if (!$res) {
throw new Exception(
"socket bind Err. \n" . socket_strerror(socket_last_error($sock))
);
}
$listen_res = socket_listen($socket, 1);
if (!$listen_res) {
throw new Exception(
"socket bilisten_resnd Err. \n" .
socket_strerror(socket_last_error($socket))
);
}
echo "Socket Listen {$ip}:{$port} " . PHP_EOL;
while (true) {
sleep(1);
if (($fd = socket_accept($socket)) !== false) {
echo "Client $fd has connected\n";
$pid = pcntl_fork();
if ($pid == -1) {
echo "fork failure!\n";
die();
} elseif ($pid == 0) {
while (true) {
sleep(1);
$buf = 'This is my buffer.';
if (
false !== ($bytes = socket_recv($fd, $buf, 2048, MSG_DONTWAIT))
) {
echo "Read $bytes bytes from socket_recv().".PHP_EOL;
if ($bytes > 0) {
print_r($buf);
} else {
socket_close($fd);
break;
}
}
}
return ;
}
}
}
socket_close($socket);
$ nc 127.0.0.1 8089
$ php BIO.php
Socket Listen 127.0.0.1:8089
Client Resource id #5 has connected
Client Resource id #6 has connected
Client Resource id #7 has connected
结论
- 查看进程数 4 个 每加一个链接 增加一个进程
- accept 和 recv 都会存在 IO 阻塞
- BIO 使用开启子进程(或线程)处理 IO 的阻塞处理
- 缺点 : 一个链接一个进程 会开辟大量的进程内存空间占用浪费资源,CPU 调度消耗大
- 优点 : 可以接收很多的链接,实现相对简单
1.1.2. NIO
<?php
set_time_limit(0);
error_reporting(E_ALL ^ E_NOTICE);
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$ip = '127.0.0.1';
$port = 8090;
$res = socket_bind($socket, $ip, $port);
if (!$res) {
throw new Exception(
"socket bind Err. \n" . socket_strerror(socket_last_error($sock))
);
}
$listen_res = socket_listen($socket, 1);
if (!$listen_res) {
throw new Exception(
"socket bilisten_resnd Err. \n" .
socket_strerror(socket_last_error($socket))
);
}
echo "Socket Listen {$ip}:{$port} " . PHP_EOL;
$nonblock = socket_set_nonblock($socket);
if (!$nonblock) {
throw new Exception(
"socket socket_set_nonblock Err. \n" .
socket_strerror(socket_last_error($socket))
);
}
$clients = [];
while (true) {
sleep(1);
if (($fd = socket_accept($socket)) !== false) {
echo "Client $fd has connected\n";
socket_set_nonblock($fd);
$clients[] = $fd;
}
foreach ($clients as $k=>$fd) {
$buf = 'This is my buffer.';
if (false !== ($bytes = socket_recv($fd, $buf, 2048, MSG_DONTWAIT))) {
echo "Read $bytes bytes from socket_recv()." . PHP_EOL;
if ($bytes === 0) {
socket_close($fd);
unset($clients[(int)$k]);
} else {
print_r($buf);
}
}
}
}
socket_close($socket);
$ nc 127.0.0.1 8090
$ php NIO.php
Socket Listen 127.0.0.1:8090
Client Resource id #5 has connected
Client Resource id #6 has connected
Client Resource id #7 has connected
结论
- 查看进程数 1 个
- accept 和 recv 都会存在 IO 阻塞
- NIO 开启 NONBLOCK 处理 accept 非阻塞 IO
- 缺点 : resv 的检测依赖循环 效率比较低下
- 优点 : 避免了多进程(线程)的问题
1.1.3. 多路复用器 select
<?php
set_time_limit(0);
error_reporting(E_ALL ^ E_NOTICE);
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$ip = '127.0.0.1';
$port = 8091;
$res = socket_bind($socket, $ip, $port);
if (!$res) {
throw new Exception(
"socket bind Err. \n" . socket_strerror(socket_last_error($sock))
);
}
$listen_res = socket_listen($socket, 1);
if (!$listen_res) {
throw new Exception(
"socket bilisten_resnd Err. \n" .
socket_strerror(socket_last_error($socket))
);
}
echo "Socket Listen {$ip}:{$port} " . PHP_EOL;
$all_sock[(int)$socket] = $socket;
var_dump($all_sock);
while (true) {
sleep(1);
$read = $write = $except = [];
$tv_sec = 5;
$tv_usec = 5000;
$read = $all_sock;
$changed = socket_select($read, $write, $except, $tv_sec, $tv_sec);
if (false === $changed) {
print "[error]socket_select() failed
(" .socket_strerror(socket_last_error()) . ")\n";
}
if ($changed < 1) {
continue;
}
foreach ($read as $k=>$rsock) {
if ($rsock === $socket) {
$client = socket_accept($socket);
echo "Client $client has connected\n";
$all_sock[(int)$client] = $client;
} else {
$buf = 'This is my buffer.';
if (false !== ($bytes = socket_recv($rsock, $buf, 2048, MSG_DONTWAIT))) {
echo "Read {$bytes} bytes from socket_recv()." . PHP_EOL;
if ($bytes === 0) {
socket_close($rsock);
unset($all_sock[(int)$k]);
} else {
print_r($buf);
}
}
}
}
}
socket_close($sock);
$ nc 127.0.0.1 8092
$ php mults.php
Socket Listen 127.0.0.1:8092
array(1) {
[4]=>
resource(4) of type (Socket)
}
Client Resource id #5 has connected
Client Resource id #6 has connected
Client Resource id #7 has connected
结论
- 查看进程数 1 个
- accept 和 recv 都会存在 IO 阻塞
- socket_select 在给定的套接字数组上以指定的超时时间运行 select 系统调用
- 缺点 : 限制了数量不能超过 1024 个,效率较低
- 优点 : 一次系统调用返回多个可用的 socket 资源
- select 方式,遍历所有的 socket,如果某个达到了可读条件就进行拿出来执行,最多监听 1024 个
- 用户空间保存所有的连接
- 内核空间遍历,得到可读的
- 转交给用户空间处理
- poll 方式,和 select 方式相似,只是没有监听个数限制,1w 长连接,就要遍历 1w 个,即使有很多都没有信息,浪费 cpu
- epoll,没有描述符限制,也没有遍历,当描述符状态改变时,触发事件回调,事件监听机制
- 需要安装 libevent 库(linux),php 需要安装 event 扩展,pecl install event,才能使用 epoll
- 将每个 fd 和回调添加到内核中,内核监听网卡等状态,当有请求数据是针对某个 fd 时,就将其执行 read 的回调放到就绪队列中,等待执行
- fd 在内核中以树状(红黑树)存储,方便查找