前言
系统在处理一些事情的时候,有的选择进程处理,有的选择线程处理,至于怎么选择是根据平台和编程语言来决定的。接下来我们需要理解什么是网络IO模型?
stream_socket_server初体验
系统的QQ等客户端在和linux系统内核进行通信是通过socket实现的,那如何去创建socket呢?
php有2个函数可以创建socket套接字,分别是stream_socket_server()和socket_create(),这2个函数都是创建一个套接字,区别是stream_socket_server是有点点封装函数,socket_create是原生的。本次只体验stream_socket_server函数。
接下来我们将用原生的stream_socket_server这个函数去创建一个服务(相对于swoole创建服务,swoole就是封装好的),然后去连接和请求这个服务。
原生实现代码初体验:
$host = "tcp://0.0.0.0:9501";
//创建socket服务
$server = stream_socket_server($host);
echo $host."\n";
//建立与客户端的连接
//开启服务之后,服务就处于一个挂起的状态,等待连接进来然后创建连接
//stream_socket_accept是阻塞的
//长时间没有连接进来,就会报出一个连接超时警告异常,可以在前面加@来抑制异常
$client = @stream_socket_accept($server);
var_dump($client);
socket连接处理的阻塞状态
$host = "tcp://0.0.0.0:9501";
//创建socket服务
$server = stream_socket_server($host);
echo $host."\n";
//建立与客户端的连接
//开启服务之后,服务就处于一个挂起的状态,等待连接进来然后创建连接
//stream_socket_accept是阻塞的
//长时间没有连接进来,就会报出一个连接超时警告异常,可以在前面加@来抑制异常
//监听连接,当一个连接进来之后,就结束了,那通过一个死循环while来实现一直重复监听连接
while(true){
$client = @stream_socket_accept($server);
var_dump($client);
}
开启服务之后,当有一个客户端连接进来之后,服务端是收到连接的,连接之后又继续监听等待连接;而客户端也一直等待服务端的信息返回,若长时间未收到服务端的数据,就会断开重置。
curl: (56) Recv failure: Connection reset by peer
当多个连接进来时,后面的连接会把前面未收到服务端返回信息的连接挤掉
$host = "tcp://0.0.0.0:9501";
//创建socket服务
$server = stream_socket_server($host);
echo $host."\n";
//建立与客户端的连接
//开启服务之后,服务就处于一个挂起的状态,等待连接进来然后创建连接
//stream_socket_accept是阻塞的
//长时间没有连接进来,就会报出一个连接超时警告异常,可以在前面加@来抑制异常
//监听连接,当一个连接进来之后,就结束了,那通过一个死循环while来实现一直重复监听连接
while(true){
//建立与客户端的连接
$client = @stream_socket_accept($server);
//读取客户端的信息
$data = @fread($client,1024);
//发送信息给客户端
@fwrite($client,'abc');
//关闭连接
@fclose($client);
var_dump($client);
}
这里是服务端读取客户端的信息和发送信息给客户端
创建一个客户端:
$host = "tcp://127.0.0.1:9501";
//创建一个客户端的连接,与服务端accept进行信息交互
$client = stream_socket_client($host);
//给socket通道发送信息
fwrite($client,"hello world");
//接收读取socket通道发送来的信息
var_dump(fread($client,65535));
//关闭连接
fclose($client);
在tcp创建连接之后,假设需要做个耗时操作,sleep(3)一下,看看多个客户端同时连接进来会发生什么情况?
服务端:
$host = "tcp://0.0.0.0:9501";
//创建socket服务
$server = stream_socket_server($host);
echo $host."\n";
//建立与客户端的连接
//开启服务之后,服务就处于一个挂起的状态,等待连接进来然后创建连接
//stream_socket_accept是阻塞的
//长时间没有连接进来,就会报出一个连接超时警告异常,可以在前面加@来抑制异常
//监听连接,当一个连接进来之后,就结束了,那通过一个死循环while来实现一直重复监听连接
while(true){
//建立与客户端的连接
$client = @stream_socket_accept($server);
sleep(3);
//读取客户端的信息
$data = @fread($client,1024);
//发送信息给客户端
@fwrite($client,'abc');
//关闭连接
@fclose($client);
var_dump($data);
}
客户端:
$host = "tcp://127.0.0.1:9501";
//创建一个客户端的连接,与服务端accept进行信息交互
$client = stream_socket_client($host);
$time = time();
//给socket通道发送信息
fwrite($client,"hello world");
//接收读取socket通道发送来的信息
var_dump(fread($client,65535));
//关闭连接
fclose($client);
echo "\n".time()-$time;
这个时候发现第2个客户端必须等待第1个客户端完成之后才会连接,这是为什么呢?
这主要是 stream_socket_server是单线程处理连接的,必须等待前面的连接处理之后再处理后面的,这种状态叫做阻塞调用。那如何解决呢?
swoole 通过Reactor模式解决这个问题,php-fpm通过多进程模式解决这个问题,这些模式统称为网络IO模型。
网络IO模型就好比 socket连接处理的框架或架构,比如:原生PHP=》laravel,thinkphp,yii。
5大IO模型
IO管理连接模型
内核:可以理解为 帮助我们去处理这份代码,如何去读,写,关闭,输出。
用户空间:可以理解为去告诉内核处理一些事情,代码就在用户空间里写。
5大io模型:
- 阻塞I/O(blocking IO):用户空间一直等待内核处理完之前未处理完的事情,且在等待时间内不会去做其他的事情。效率是最低的。
- 非阻塞I/O(noblocking IO):用户空间先检查内核是否有未处理完的事情,若有,就去做其他的事情,且每隔一段时间检查一次内核是否处理完,这种就是轮询的方式。
- I/O多路复用 (IO multiplexing ):开辟多个内核,用户空间根据哪个内核空闲就将信息发送给该内核去处理。
- 信号驱动I/O (signal driven IO):用户空间设置一个信息号就会去做其他的事情(对用户空间来说是非阻塞的),内核处理完未处理的事情之后会处理这个事情,处理完这个事情之后会返回该信号,表示处理完这个事情。触发回调函数。
- 异步I/O (asynchronous IO):预定模式 ,用户空间一般用事件event=》事先今天发送一个信息给内核,并告诉内核在指定的时间节点去处理这件事情,(对于用户空间来说是非阻塞的),触发回调函数。通常事件发送完之后还需要事件清理(原因是内核处理完一件事情后,在下一个时间节点有可能在做其他的事情了)。
为什么要理解5大IO模型?对我们来说有什么意义?
通过理解网络编程的一些技术知识,为了更好的理解学习swoole。
简单IO架构(代码层面)
应用composer命名空间
io
src
SignalDriven
worker.php
test
signal-driver
server.php
client.php
先创建io目录,打开cmd,cd到io目录下,输入composer init,回车,创建属于自己的命名空间;
配置一下Package name ,Auth,Package Type,License
就会在io目录下生成一个composer.json文件
然后创建一下目录 ,src(存放自己原生写的封装的类),test目录(测试用的目录包)。然后再composer.json文件里配置一下命名空间,添加配置如下:
"autoload":{
"psr-4":{
"zjl\\io\\":"./src/"
}
},
测试一下:
在src目录下创建Index.php,编写测试代码如下:
然后composer update一下
这时候会生成vendor目录及文件
然后在io目录下创建一个test.php。(可以参照workerman的架构)
然后再cmd 上运行一下 php test.php ,这样就可以了。
阻塞IO模型
Worker.php
namespace zjl\io\Blocking;
/**
* 用户连接服务
*/
class Worker
{
/**
* 自定义服务的事件注册函数
* 闭包函数
* @var null
*/
public $onReceive = null;
public $onConnect = null;
public $onClose = null;
//连接
public $socket = null;
public function __construct($socket_address)
{
$this->socket = stream_socket_server($socket_address);
echo $socket_address."\n";
}
/**
* 处理事情
*/
public function accept()
{
//接收连接,处理事情
while(true)
{
$client = @stream_socket_accept($this->socket);
//判断是不是闭包
if(is_callable($this->onConnect)){
//执行闭包函数
($this->onConnect)($this,$client);
}
$data = fread($client,65535);
if(is_callable($this->onReceive)){
//执行闭包函数
($this->onReceive)($this,$client,$data);
}
//处理完关闭连接
fclose($client);
}
}
public function send($conn,$data)
{
fwrite($conn,$data);
}
//启动服务
public function start()
{
$this->accept();
}
}
server.php
require __DIR__."/../../vendor/autoload.php";
use zjl\io\Blocking\Worker;
$host = "tcp://0.0.0.0:9501";
$server = new Worker($host);
//连接服务
$server->onConnect = function ($server,$client){
echo "有一个连接进来\n";
var_dump($client);
};
//接收和处理信息
$server->onReceive = function ($server,$client,$data){
echo "给连接发送信息\n";
$server->send($client,"hello world client \n");
};
$server->start();
测试
php server.php
curl http://127.0.0.1:9501