Swoole是使用纯C语言编写的 ,提供了PHP语言的异步多线程服务器。
异步TCP/UDP网络客户端 、异步MYSQL、异步Redis、数据库连接池
消息队列、毫秒定时器、异步DNS查询、Swoole内置了HTTP/webSocket服务器端和客户端
crontab linux定时器 精确度到分 。 swoole 精确到 毫秒。
官网:https://www.swoole.com/
那么使用swoole能做什么项目呢? https://wiki.swoole.com/wiki/page/p-case.html
比如说 YY语音 虎牙视频 应用场景还是很广泛的。所以今天我们来学习学习。
支持并发百万 TCP 长连接 、视频、直播、 通讯
典型的应用场景:
1、移动互联网API服务器
2、物联网
3、微服务
4、高性能web服务
5、游戏服务器
6、在线聊天系统 web IM
PHP工程师如果不懂swoole的话,还不算完美。
Swoole安装准备工作:
1、linux 环境
2、php7 swoole2.1 redis
3、源码安装php7 源码安装swoole
如何学好swoole 1、查看文档 2、实现swoole特性的功能点 3、多看其他现有swoole案例代码
边看文档 边写代码 多看别人怎么写,对于自己的提高是非常有必要的。
swoole 提问社区:
https://segmentfault.com/t/swoole
php7.1的安装
进入 php.net 下载最新版 php7.1.4 或 PHP7.2.4 进行下载并安装。
linux 安装php扩展服务
http://www.xiaosongit.com/index.php/index/detail/id/371.html
php支持swoole
http://www.xiaosongit.com/index.php/index/detail/id/369.html
好的,上边我们都已经配置好了,那么真正的项目如何开始进行呢?
现在我们就来看看吧。
我们建议使用源码进行安装
----------------------------------------------------------------------------
SWOOLE 分为 HTTP TCP UDP 三种连接方式。
分别是什么关系又是如何获取的呢?
swoole_server运行模式
单线程模式
这种模式就是传统的异步非阻塞。在Reactor内直接回调php的函数。如果回调函数中有阻塞操作会导致为同步模式。
work_num参数对于base模式仍然有效,swoole会启动多Reactor进程。
Base模式下Reactor和worker是同一个角色。
优点:
base模式没有IPC开销,性能更好
Base模式代码更简单,不容易出错。
缺点:
1、Tcp连接实在worker进程中维持的,所以当某个worker进程挂掉时,此worker内所有人连接都将被关闭
2、少量TCP长连接无法利用到所有worker进程
3、Tcp连接与worker是绑定的,长连接应用中某些连接的数据量大,这些连接所在的worker进程负载会非常高。
但某些连接数据量较小,所在worker进程的负载会非常低,不同worker进程无法实现负载均衡。
Base适用场景:客户端连接之间不需要交互,如Memcache/Http服务器。
进程模式
多进程模式是最复杂的方式,用了大量的进程间通信,进程管理机制。适合业务逻辑非常复杂的场景。
swoole提供了完善的进程管理、内存保护机制。在业务逻辑非常复杂的情况下,也可以长期稳定的运行。
进程模式优点:
1、连接与数据请求是分离的,不会因为某些连接数据量大,某些数据量小导致进程不均衡。
2、worker进程发送致命错误是,连接并不会被切断。
配置选项:
https://wiki.swoole.com/wiki/page/275.html
TCP服务
//创建Server对象,监听127.0.0.1:9501端口 $server=new Swoole_server("127.0.0.1",9501,SWOOLE_PROCESS,SWOOLE_SOCK_TCP); $server->set([ 'worker_num'=>8, //worker_num 进程数 'max_request'=>10000 ]); /** * @title 监听连接事件 * $fd 客户端连接的唯一标识 * $reactor_id 线程 ID 这是第三个参数 */ $server->on("connect",function($server,$fd,$reactor_id){ echo "Client: {$reactor_id}-{$fd}-Content.\n"; }); /** * @title 监听数据接收事件 * $fd 客户端数据连接唯一标识 * $reactor_id 进程ID * $data 发送过来的内容 */ $server->on("receive",function($server,$fd,$reactor_id,$data){ $server->send($fd,"Server:{$reactor_id}-{$fd} {$data}"); }); /** * @title 关闭连接 * $server * $fd 客户端数据连接唯一标识 */ $server->on("close",function($server,$fd){ echo "Clinet:close".$fd; }); //启动服务 $server->start();
还有其他三种调用方式,请查看图片:
tcp服务已经写好了,那么如何来连接?
telnet ip 端口
[root@localhost ~]# telnet 127.0.0.1 9501 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. nihao Server:0-1 nihao 5656 Server:0-1 5656
服务器端:
root@localhost swoole]# php tcp.php Client: 0-1-Content. Client: 0-2-Content.
swoole连接mysql查询数据的案例
[root@localhost swoole]# php tcp.php 启动的时候初始化....onStart 启动的时候初始化....onStart 启动的时候初始化....onStart 启动的时候初始化....onStart 启动的时候初始化....onStart 启动的时候初始化....onStart 启动的时候初始化....onStart 启动的时候初始化....onStart Client: 0-1-Content. 收到客户端信息:a:2:{s:7:"message";s:16:"hellow1553245896";s:4:"page";i:2;}Clinet:close1 Client: 0-2-Content. 收到客户端信息:a:2:{s:7:"message";s:16:"hellow1553245902";s:4:"page";i:2;}Clinet:close2 Client: 0-3-Content. 收到客户端信息:a:2:{s:7:"message";s:16:"hellow1553245909";s:4:"page";i:3;}Clinet:close3 Client: 0-4-Content. 收到客户端信息:a:2:{s:7:"message";s:16:"hellow1553245919";s:4:"page";i:1;}Clinet:close4 Client: 0-5-Content. 收到客户端信息:a:2:{s:7:"message";s:16:"hellow1553245941";s:4:"page";i:1;}Clinet:close5
服务器端代码:
<?php //创建Server对象,监听127.0.0.1:9501端口 $server=new Swoole_server("0.0.0.0",9501,SWOOLE_PROCESS,SWOOLE_SOCK_TCP); $server->set([ 'worker_num'=>8, //worker_num 进程数 'max_request'=>10000 ]); $server->on('WorkerStart',function($server,$worker_id){ echo "启动的时候初始化....onStart\n"; //每一次连接都要启动吗? $dsn="mysql:host=localhost;dbname=test;port=3306"; //$dsn="mysql:localhost=localhost;dbname=test;port=3306"; //以上这两种localhost 和 host 这是一样的, 记住没以后面都没有引号或单引号 $root="root"; $pass="song"; $param=[ PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION ]; $server->db=new pdo($dsn,$root,$pass,$param); }); /** * @title 监听连接事件 * $fd 客户端连接的唯一标识 * $reactor_id 线程 ID 这是第三个参数 */ $server->on("connect",function($server,$fd,$reactor_id){ echo "Client: {$reactor_id}-{$fd}-Content.\n"; //每一次连接都要启动吗? //$dsn="mysql:host=localhost;dbname=test;port=3306"; //$dsn="mysql:localhost=localhost;dbname=test;port=3306"; //以上这两种localhost 和 host 这是一样的, 记住没以后面都没有引号或单引号 /* $root="root"; $pass="song"; $param=[ PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION ]; $server->db=new pdo($dsn,$root,$pass,$param); */ //在这里不能立马发送信息给客户端,如果发送信息,则断开与客户端的连接 //$server->send($fd,$fd." nihao"); }); /** * @title 监听数据接收事件 * $fd 客户端数据连接唯一标识 * $reactor_id 进程ID * $data 发送过来的内容 */ $server->on("receive",function($server,$fd,$reactor_id,$data){ printf("收到客户端信息:%s",$data); $data=unserialize($data); //在这里查询数据库么 //$info=str_repeat($server->db,10).time(); $start=($data['page']-1)*10; $sql="select * from user limit {$start},10"; $stmt=$server->db->prepare($sql); $stmt->execute(); $obj=$stmt->fetchAll(PDO::FETCH_CLASS); $info=var_export($obj,true); $server->send($fd,"Server:{$reactor_id}-{$fd} {$data['message']} {$info}"); }); /** * @title 关闭连接 * $server * $fd 客户端数据连接唯一标识 */ $server->on("close",function($server,$fd){ echo "Clinet:close".$fd."\n"; }); //启动服务 $server->start();
客户端连接:
$client=stream_socket_client('tcp://172.28.81.174:9501',$errno,$error); if(!$client){ print_r($errno); print_r($error); } $message=serialize([ 'message'=>'hellow'.time(), 'page'=>1, ]); fwrite($client,$message,mb_strlen($message,'utf-8')); $info=fread($client,1024); echo "服务器说:".$info."\n"; fclose($client);
服务器说:Server:0-5 hellow1553245941 array ( 0 => stdClass::__set_state(array( 'id' => '1', 'name' => 'hoe', 'gongzi' => '7000', 'class_id' => '1', )), 1 => stdClass::__set_state(array( 'id' => '2', 'name' => 'james', 'gongzi' => '4000', 'class_id' => '2', )), 2 => stdClass::__set_state(array( 'id' => '3', 'name' => 'jack', 'gongzi' => '1200', 'class_id' => '3', )), 3 => stdClass::__set_state(array( 'id' => '4', 'name' => 'jim', 'gongzi' => '700', 'class_id' => '2', )), 4 => stdClass::__set_state(array( 'id' => '5', 'name' => 'jack', 'gongzi' => '1200', 'class_id' => '5', )), 5 => stdClass::__set_state(array( 'id' => '6', 'name' => 'hoe', 'gongzi' => '7000', 'class_id' => '1', )), 6 => stdClass::__set_state(array( 'id' => '7', 'name' => 'james', 'gongzi' => '4000', 'class_id' => '2', )), 7 => stdClass::__set_state(array( ' [Finished in 0.6s]
查看连接mysql的连接数:
[root@localhost ~]# mysqladmin -uroot -psong processlist Warning: Using a password on the command line interface can be insecure. +----+------+-----------+------+---------+------+-------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+------+-----------+------+---------+------+-------+------------------+ | 43 | root | localhost | test | Sleep | 106 | | | | 44 | root | localhost | test | Sleep | 106 | | | | 45 | root | localhost | test | Sleep | 106 | | | | 46 | root | localhost | test | Sleep | 106 | | | | 47 | root | localhost | test | Sleep | 32 | | | | 48 | root | localhost | test | Sleep | 106 | | | | 49 | root | localhost | test | Sleep | 106 | | | | 50 | root | localhost | test | Sleep | 106 | | | | 57 | root | localhost | | Query | 0 | init | show processlist | +----+------+-----------+------+---------+------+-------+------------------+
TCP客户端连接
我们一直使用telnet 这样的方式连接 swoole server ,那么有没有写成php脚本的形式去连接呢,下面我们就来看一看。
好的,现在我们来看一看代码如何实现:
php tcp_server.php 这样的命令 一执行 ,就好似 nginx 开启了服务,我们就可以直接通过IP 和端口去使用
tcp_client.php
//连接 swoole tcp 服务 $client=new swoole_client(SWOOLE_SOCK_TCP); if(!$client->connect("127.0.0.1",9501)){ echo "服务器连接失败"; exit; } //cli常量 swoole大部分代码在cli下运行。 fwrite(STDOUT,"请输入要发送的值"); $msg=trim(fgets(STDIN)); //把消息发送给tcp server服务器 $client->send($msg); //接收来自server的数据 $result=$client->recv(); echo $result;
上面的程序 执行一次就关闭了,那么如何像telnet 一样可以多次输入值呢?
看下面的代码:
//连接 swoole tcp 服务 $client=new swoole_client(SWOOLE_SOCK_TCP); if(!$client->connect("127.0.0.1",9501)){ echo "服务器连接失败"; exit; } while(true){ //cli常量 swoole大部分代码在cli下运行。 fwrite(STDOUT,"请输入要发送的值,如果需要退出请输入 exit :"); $msg=trim(fgets(STDIN)); if($msg=="exit"){ break; } //把消息发送给tcp server服务器 $client->send($msg); //接收来自server的数据 $result=$client->recv(); echo $result; }
问题:
我如何知道我开启了几个进程?
ps aft | grep tcp_server.php
相关命令:
杀死所有关于tcp.php 的进程
kill -9 `ps -ef | grep tcp.php | grep -v "grep" | awk '{print $2}'| xargs`
如何创建UDP服务呢?
udp_server.php
//创建udp服务器 其实和tcp差不不多,就是最后传递的参数不一样 $server=new Swoole_server("127.0.0.1",9502,SWOOLE_PROCESS,SWOOLE_SOCK_UDP); //监听数据接收事件 /** * * $server,swoole_server对象 * $data,收到的数据内容,可能是文本或者二进制内容 * $client_info,客户端信息包括address/port/server_socket 3项数据 * * */ $server->on("packet",function($server,$data,$clientInfo){ $server->sendto($clientInfo['address'],$clientInfo['port'],"Server say:".$data."\n"); var_dump($clientInfo); var_dump($data."\n"); }); $server->start();
udp_client.php
//创建连接udp服务器客户端 $client=new Swoole_client(SWOOLE_SOCK_UDP); if(!$client->connect("127.0.0.1",9502)){ echo "服务器UDP服务连接失败"; exit; } //向服务器发送数据 fwrite(STDOUT,"请输入要发送的数据:"); $msg=trim(fgets(STDIN)); $client->send($msg); //接收来自server的数据 $result=$client->recv(); echo $result;
使用命令终端连接:
#centos下安装nc应该是 yum -y install nc ,而且UDP是无连接传输,所以coonect事件是监控不到的 yum -y install nc nc -u 127.0.0.1 9502
如图所示:
http_server.php
这个启动和nginx类似,像访问nginx一样就可以访问swoole
但是不能在线浏览php文件。
swoole_http_server 继承 swoole_server ,是一个完整的http服务器实现。 swoole_http_server 支持同步和异步 2 种模式。
http/websocket服务器都是继承自swoole_server,所以swoole_server提供的API,如task/finish/tick等都可以使用
onRequest
onTask
onFinish
onTick
这些回调函数都是可以使用的,因为 swoole_http_server 继承于 swoole_server .
无论是同步模式还是异步模式,swoole_http_server 都可以支持大量TCP 客户端连接。
同步和异步仅仅体现在 对请求的处理方式上。
同步模式:
这种模式等同于nginx/php-fpm/apache ,它需要大量的worker来完成并发处理。编程方式与普通的php web 程序完全一致。
与php-fpm/apache 不同的是 客户端连接并不会独占进程,服务器依然可以应对大量并发连接。
异步模式:
这种模式整个服务器是异步非阻塞的,服务器可以应对大规模的并发连接和并发请求,但是编程方式完全使用异步API。
如:mysql、redis、http_client 、file_get_contents、sleep 等阻塞IO操作必须切换为异步的方式。
如异步 swoole_client swoole_event_add,swoole_timer等API。
http_server.php
//创建http swoole服务 $http=new swoole_http_server('0.0.0.0',8811); //这里可以使用set来进行设置swoole服务器。 $http->on("request",function($request,$response){ //如果多次向浏览器输出内容,请使用 $response->write(); //如果一次性向浏览器输出内容,使用$response->end(); $response->end("HTTP SERVER SWOOLE SAY:".$request->get['title']); }); //启动服务 $http->start();
启动服务:
php http_server.php
测试服务:
连接方法有多种: curl http://127.0.0.1:8811
或者直接在浏览器中打开 也是可以的。
http://192.168.61.103:8811
如果传递参数:
http://192.168.61.103:8811/?username=james&age=22
自己实例:
//创建http swoole服务 $http=new swoole_http_server('0.0.0.0',8811); $http->on("request",function($request,$response){ //如果多次向浏览器输出内容,请使用 $response->write(); //如果一次性向浏览器输出内容,使用$response->end(); //这种方式可以实现,但不是最好的,浏览器一致处于等待状态,可以使用 swoole_sock_server(); 这样会更好 for($i=1;$i<50;$i++){ $response->write("HTTP SERVER SWOOLE SAY :".$i."<br>"); sleep(1); } $response->end("HTTP SERVER SWOOLE SAY end"); }); //启动服务 $http->start();
http_server 中 onrequest的回调函数中,有两个参数, $request $response
分别看看都是什么意思:
$request swoole_http_request->$header //Http请求的头部信息。类型为数组,所有key均为小写。 echo $request->header['host']; echo $request->header['accept-language']; swoole_http_request->$server //Http请求相关的服务器信息,相当于PHP的$_SERVER数组 echo $request->server['request_time']; swoole_http_request->$get //Http请求的GET参数 // 如:index.php?hello=123 echo $request->get['hello']; // 获取所有GET参数 var_dump($request->get); swoole_http_request->$post //HTTP POST参数,格式为数组。 echo $request->post['hello']; swoole_http_request->$cookie //HTTP请求携带的COOKIE信息,与PHP的$_COOKIE相同 echo $request->cookie['username']; swoole_http_request->$files //文件上传信息。 类型以form名称为key的二维数组。与php的$_FILES相同。 Array( [name] => facepalm.jpg [type] => image/jpeg [tmp_name] => /tmp/swoole.upfile.n3FmFr [error] => 0 [size] => 15476 ) name 浏览器上传时传入的文件名称 type MIME类型 tmp_name 上传的临时文件,文件名以/tmp/swoole.upfile开头 size 文件尺寸 swoole_http_request->rawContent swoole_http_request->getData //获取完整的原始Http请求报文。包括Http Header和Http Body function swoole_http_request->getData() : string
获取url中的参数:
服务器代码:
//创建http swoole服务 $http=new swoole_http_server('0.0.0.0',8811); $http->on("request",function($request,$response){ //如果多次向浏览器输出内容,请使用 $response->write(); //如果一次性向浏览器输出内容,使用$response->end(); //$response->end("HTTP SERVER SWOOLE SAY:".$request->server['request_uri']);q $response->end("HTTP SERVER SWOOLE SAY:".$request->get['username']); }); //启动服务 $http->start();
配置静态路径根目录swoole_http_server
#创建swoole_http_server 服务 $server=new swoole_http_server("127.0.0.1",8812); //给swoole进行配置 /** * 配置静态文件根目录,与enable_static_handler配合使用。 * 设置document_root并设置 enable_static_handler 为true的时候,底层收到http请求后会先判断 * document_root路径下是否存在此文件,如果存在就直接返回,不在触发 onRequest 回调。 * 使用静态文件处理特性时,应当将动态PHP代码和静态文件进行隔离,静态文件存放到特定的目录 */ $server->set([ 'document_root'=>'/usr/local/nginx/html/swoole/', 'enable_static_handler'=>true, ]); #进行监听 客户端发来的信息 $server->on("request",function($request,$response){ $response->end("SWOOLE SERVER RETURN DATA:".$request->get['username']); }); #启动服务 $server->start();
这是运行结果:
设置文件上传的临时目录
$server=new swoole_http_server("127.0.0.1",8012); $server->set([ "upload_tmp_dir"=>'/usr/local/nginx/html/data/' ]); $server->on("request",function($request,$response){ $response->end("server..."); }); $server->start();
webSocket服务端以及客户端连接实例
websocket_server.php
//创建webSocket服务器 $ws=new swoole_websocket_server("0.0.0.0",9999,SWOOLE_PROCESS); //监听webSocket连接打开事件 $ws->on("open",function($ws,$request){ var_dump($request->fd,$request->get,$request->server); $ws->push($request->fd,"hello ,welcome \n"); }); //监听websocket消息事件 $ws->on("message",function($ws,$frame){ echo $frame->fd."----say: ".$frame->data."\n"; $ws->push($frame->fd,"server say:".$frame->data."--".date("Y-m-d H:i:s")); }); //监听websocket的关闭事件 $ws->on("close",function($ws,$fd){ echo "client close".$fd."\n"; }); //启动websocket服务 $ws->start();
html页面代码:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <style type="text/css"> #container{ overflow-y: scroll; } #container div{ font-size: 14px; color:#666; padding: 5px 0px; margin:0px auto; width:95%; } </style> <script type="text/javascript"> window.onload=function(){ var container=document.getElementById("container"); var submitButton=document.getElementById("submit"); var message=null; var webSocket=new WebSocket("ws://192.168.61.103:9999"); webSocket.onopen=function(event){ webSocket.send("你好!"); console.log("服务器连接成功..."); } webSocket.onmessage=function(event){ console.log("服务器回复说:"+event.data); container.appendChild(createDiv(event.data)); } webSocket.onclose=function(event){ console.log("connect close"); } submitButton.onclick=function(){ message=document.getElementById("message").value; if(message){ webSocket.send(message); document.getElementById("message").value=""; }else{ alert("请输入内容后,点击发送"); } } function createDiv(message){ var div=document.createElement("div"); var textNode=document.createTextNode(message); div.appendChild(textNode); return div; } } </script> </head> <body> <div id="container" style="border:1px solid #ccc; height:360px;width:600px;"> </div> <input type="text" id="message"><input type="button" id="submit" value="发送"> </body> </html>
服务器端执行 php websocket_server.php 进行监听
客户端打开 html页面 进行测试
效果就是这样:
webSocket改造成 面向对象编程方式
ws_server.php
class Ws{ public $server=null; CONST HOST="0.0.0.0"; const PORT=9999; public function __construct(){ $this->server=new swoole_websocket_server(self::HOST,self::PORT); $this->server->on("open",[$this,"onOpen"]); $this->server->on("message",[$this,"onMessage"]); $this->server->on("close",[$this,"onClose"]); $this->server->start(); } public function onOpen($server,$frame){ echo $frame->fd."---连接成功\n"; } public function onMessage($server,$frame){ echo $frame->fd."----发来消息说:".$frame->data."\n"; $server->push($frame->fd,"服务器返回消息说:".$frame->data."--".date("Y-m-d H:i:s")); } public function onClose($server,$fd){ echo $fd." 客户端已经关闭"; } } new Ws();
html页面保持不变,还是和原来一样
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <style type="text/css"> #container{ overflow-y: scroll; } #container div{ font-size: 14px; color:#666; padding: 5px 0px; margin:0px auto; width:95%; } </style> <script type="text/javascript"> window.onload=function(){ var container=document.getElementById("container"); var submitButton=document.getElementById("submit"); var message=null; var webSocket=new WebSocket("ws://192.168.61.103:9999"); webSocket.onopen=function(event){ webSocket.send("你好!"); console.log("服务器连接成功..."); } webSocket.onmessage=function(event){ console.log("服务器回复说:"+event.data); container.appendChild(createDiv(event.data)); } webSocket.onclose=function(event){ console.log("connect close"); } submitButton.onclick=function(){ message=document.getElementById("message").value; if(message){ webSocket.send(message); document.getElementById("message").value=""; }else{ alert("请输入内容后,点击发送"); } } function createDiv(message){ var div=document.createElement("div"); var textNode=document.createTextNode(message); div.appendChild(textNode); return div; } } </script> </head> <body> <div id="container" style="border:1px solid #ccc; height:360px;width:600px;"> </div> <input type="text" id="message"><input type="button" id="submit" value="发送"> </body> </html>
进行测试: