本文包含TCP
,UDP
,HTTP
和WebSocket
服务的搭建,以及Task
异步任务.在面试中也有些面试官喜欢TCP
和UDP
的区别.本文也列举了几个区别.多学点理论知识也是不亏的.
TCP/UDP的区别
首先他们都是传输层的协议
.
TCP | UDP | |
---|---|---|
速度 | 慢(效率低) | 快(效率高) |
链接 | 面向连接 | 面向非链接 |
资源消耗 | 较多 | 较少 |
传输方式 | 字节流 | 报文 |
传输可靠性 | 可靠 | 不可靠 |
拥塞控制 | 慢开始,避免拥塞,快重出,快恢复 | 无 |
应用场景 | 少量数据 | 大量数据 |
TCP 服务
服务端
//创建Server对象,监听 127.0.0.1:9501端口
$serv = new swoole_server("127.0.0.1", 9501);
$serv->set([
'worker_num' => 4, #worker进程数 建议cpu的1-4倍
'max_request' => 1000, #表示worker进程在处理完n次请求后结束运行。manager会重新创建一个worker进程。此选项用来防止worker进程内存溢出。
]);
//监听连接进入事件
/*
* $fd 客户端链接的唯一标示
* $reactor_id 线程id 官方文档是没有的
* */
$serv->on('connect', function ($serv, $fd, $reactor_id) {
echo "form_id:{$reactor_id}-Client:{$fd} Connect.\n";
});
//监听数据接收事件
/*
*$from_id是官方文档给的线程id。当然也可以和上面统一成一致的
* */
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
$serv->send($fd, "from——id: $from_id-Client:{$fd}-Server: " . $data);
});
//监听连接关闭事件
$serv->on('close', function ($serv, $fd) {
echo "Client: Close.\n";
});
//启动服务器
$serv->start();
客户端
//SWOOLE_SOCK_TCP是swoole内置常量。指通过tcp链接
//*
//swoole_client::__construct
//swoole_client->__construct(int $sock_type, int $is_sync = SWOOLE_SOCK_SYNC, string $key);
//$sock_type表示socket的类型,如TCP/UDP
//使用$sock_type | SWOOLE_SSL可以启用SSL加密
//$is_sync表示同步阻塞还是异步非阻塞,默认为同步阻塞
//$key用于长连接的Key,默认使用IP:PORT作为key。相同key的连接会被复用
//*/
$client = new swoole_client(SWOOLE_SOCK_TCP);
/*
* swoole_client->connect接到远程服务器,函数原型
*$swoole_client->connect(string $host, int $port, float $timeout = 0.5, int $flag = 0)
* onnect方法接受4个参数:
* $host是远程服务器的地址,1.10.0或更高版本已支持自动异步解析域名,$host可直接传入域名
* $port是远程服务器端口
* $timeout是网络IO的超时,包括connect/send/recv,单位是s,支持浮点数。默认为0.5s,即500ms
* $flag参数在UDP类型时表示是否启用udp_connect 设定此选项后将绑定$host与$port,此UDP将会丢弃非指定host/port的数据包。
* $flag参数在TCP类型,$flag=1表示设置为非阻塞socket,connect会立即返回。如果将$flag设置为1,那么在send/recv前必须使用swoole_client_select来检测是否完成了连接
* */
$connect = $client->connect('127.0.0.1',9501);
if(!$connect){
echo "connect failed error {$client->errCode}";
}
//让用户输入数据
fwrite(STDOUT,'请输入手机号');
//获取用户输入的数据
$msg = fgets(STDIN);
//将用户输入的数据发送给tcp服务端
//触发server端on函数的receive方法
$client->send($msg);
//接受来组server的数据
//来时server端on函数的receive方法的返回值
$ser_msg = $client->recv();
echo $ser_msg;
启动服务
# 先下载telnet和lsof包
yum install -y telnet lsof
# 启动服务
php tcp/server.php
# 如果报下面的错,说明端口被占用
WARNING swSocket_bind(:426): bind(127.0.0.1:9501) failed, Error: Address already in use[98]
# 查看端口占用情况也可以用netstat -anp | grep 9501
lsof -i:9501
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
php 62153 root 3u IPv4 136767 0t0 TCP localhost:9501 (LISTEN)
# 杀死进程62153是查询的pid
kill -9 62153
# 再次启动服务
php tcp/server.php
# 没有报错说明成功~
检测服务
-- client.php 检测
# 执行client.php文件
php tcp/client.php
# 提示输入手机号,瞎写一个
请输入手机号15012345678
# 客户端返回
from——id: 0-Client:2-Server: 15012345678
# 服务端返回
form_id:0-Client:3 Connect.
Client: Close.
# ok,没问题!
-- telnet 命令检测
# 打开第二个终端(客户端),输入下面内容
telnet 127.0.0.1 9501
# 返回
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
# 说明连接成功,查看终端1(服务端)
form_id:0-Client:1 Connect.
# 这是代码中connect的回掉输出的内容.
# 在终端2(客户端)输入你好
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
'你好,QvBiLam'
# 会立刻返回服务端中receive的回调send的内容
from——id: 0-Client:2-Server: '你好,QvBiLam'
# 退出talnet.按住control + ] 中括号.
# 再回车进入telnet输入
quit
# ok,没问题!
UDP 服务
服务端
$server = new swoole_server("127.0.0.1", 9502, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);
//监听数据接受事件
/*
* UDP服务器与TCP服务器不同,UDP没有连接的概念.
* 启动Server后,客户端无需Connect.
* 直接可以向Server监听的9502端口发送数据包.
* 对应的事件为onPacket。
* $clientInfo是客户端的相关信息,是一个数组,有客户端的IP和端口等内容
* 调用 $server->sendto 方法向客户端发送数据
* */
$server->on('Packet', function ($server, $data, $clientInfo) {
$server->sendto($clientInfo['address'], $clientInfo['port'], "data:" . $data);
var_dump($clientInfo);
});
//启动服务
$server->start();
客户端
$client = new swoole_client(SWOOLE_SOCK_UDP);
// 链接UDP服务端
$connect = $client->connect('127.0.0.1',9502);
// 判断链接是否成功
if(!$connect){
echo 'connect failed error';
}
// 向服务端发送消息
$client->send('hello!');
// 获取服务端返回消息
$msg = $client->recv();
// 输出服务端返回消息
echo $msg;
启动服务
php udp/server.php
检测服务
-- client.php 检测
# 端口1 启动服务
php udp/server.php
# 端口2 执行客户端
php udp/client.php
# 端口1 返回内容
array(4) {
["server_socket"]=>
int(3)
["server_port"]=>
int(9502)
["address"]=>
string(9) "127.0.0.1"
["port"]=>
int(41543)
}
# 端口2 返回内容
data:hello!
# ok,没问题!
-- netcat 命令检测
# 下载netcat
yum install -y nc
# 链接UDP服务器
nc -u 127.0.0.1 9502
# 键入hello , Qvbilam.服务端返回内容
data:hel, Qvbilam
# 终端1 显示
array(4) {
["server_socket"]=>
int(3)
["server_port"]=>
int(9502)
["address"]=>
string(9) "127.0.0.1"
["port"]=>
int(46331)
}
# ok,没问题!
HTTP 服务
服务端
$server = new Swoole\Http\Server("0.0.0.0",9503);
// 设置静态资源目录
/*
* enable_static_handel开启静态文件请求处理功能,需配合document_root使用.
* document_root配置静态文件根目录,与enable_static_handler配合使用。
* 注意:设置了set后将不会走on方法
* */
$server->set([
'enable_static_handler' => true,
'document_root' => "/data/wwwroot/test"
]);
/*
* end向客户端浏览器发送HTML内容,只能调用一次.
* 如果需要分多次向客户端发送数据,请使用write方法
* */
$server->on('request', function($request,$response) {
$response->end("<h1>hello</h1>");
});
$server->start();
启动服务
php http/server.php
检测服务
-- 没有设置set方法的测试
# 请求http服务
curl http://127.0.0.1:9503
# 返回on方法end的内容
<h1>hello</h1>
-- 开启set方法的测试
curl 127.0.0.1:9503/
# 返回
<h1>hello</h1>
# 可以看到还是走的end方法.访问test目录下的文件
curl 127.0.0.1:9503/index.html
# 返回
<h1>QvBiLam </h1>
# 访问不存在的文件
curl 127.0.0.1:9503/index.html123
# 返回
<h1>hello</h1>
总结
- 设置了set,如果
访问存在的文件
就会不走end
方法. - 设置了set,如果
访问不存在的文件
就会走end
方法.
WebSocket
服务端
$server = new Swoole\WebSocket\Server("0.0.0.0", 9500);
//监听websocket链接事件。调用onOepen函数
$server->on('open', 'onOpen');
function onOpen($server, $request)
{
print_r('connet ' . $request->fd);
}
//监听websocket消息事件
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
// 想客户端发送数据
$server->push($frame->fd, "get message success:{$frame->data}");
});
$server->on('close', function ($ser, $fd) {
echo "client {$fd} closed\n";
});
$server->start();
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>QvBiLam</title>
</head>
<body>
<h1>hello QvBiLam</h1>
</body>
</html>
<script>
var WsUrl = "ws://127.0.0.1:9502"
var websocket = new WebSocket(WsUrl);
//链接websock服务
websocket.onopen = function(evt){
console.log("connet success")
//向服务端发送消息hhhh
websocket.send('hhhhhh');
}
//接受服务端消息
websocket.onmessage = function(evt){
//获取服务端传来的数据push
console.log(evt.data)
}
websocket.onclose = function(evt){
console.log("cloes")
}
websocket.onerror = function(evt,e){
console.log(evt.data)
}
</script>
测试结果
客户端显示
服务端显示
面向对象优化
class ws_oop
{
CONST HOST = '0.0.0.0';
CONST PORT = 9502;
public $ws = null;
public $timer_id = null;
public function __construct()
{
$this->ws = new Swoole\WebSocket\Server(self::HOST, self::PORT);
$this->ws->set([
'work_num' => 2,
'task_worker_num' => 2
]);
$this->ws->on('open', [$this, "onOpen"]);
$this->ws->on('message', [$this, "onMessage"]);
$this->ws->on('close', [$this, "onClose"]);
$this->ws->on('task', [$this, 'onTask']);
$this->ws->on('finish', [$this, 'onFinish']);
$this->ws->start();
}
/*
*监听链接事件
* */
public function onOpen($ws, $request)
{
var_dump($request->fd);
$ws->push($request->fd,'欢迎链接,' . $request->fd);
Swoole_timer_tick(1000,function($timer) use($ws,$request){
$ws->push($request->fd,'现在开始进行攻击,伤害:100');
$this->timer_id = $timer;
});
$timer = $this->timer_id;
Swoole_timer_after(5000,function() use($ws,$request,$timer){
Swoole_timer_clear($timer);
$ws->push($request->fd,'攻击完成!成功击毙敌人');
});
$ws->push($request->fd,'发现敌人,准备进行攻击');
}
public function onMessage($ws, $farme)
{
echo 'client-push-message :' . $farme->data . "\n";
$data = [
'task' => 1,
'fd' => $farme->fd
];
// $ws->task($data);
$ws->push($farme->fd, "server-push:" . $farme->data . time());
}
public function onClose($ws, $fd)
{
echo "close : {$fd}\n";
}
}
new ws_oop();
Task
启用Task
至于是个什么我也说不明白,咱啥也不知道,咱也不敢乱说.实践起来很好用,咱就用.执行一些比较耗时的错做.例如群发邮件,聊天广播等.我们就可以用到Task
异步任务.
通过Websocket
服务的OOP
代码修改.
public function onMessage($ws, $farme)
{
echo 'client-push-message :' . $farme->data . "\n";
$data = [
'task' => 1,
'fd' => $farme->fd
];
// $ws->task($data);
$ws->push($farme->fd, "server-push:" . $farme->data . time());
}
/*
* 当前的Task进程在调用onTask回调函数时会将进程状态切换为忙碌,这时将不再接收新的Task,当onTask函数返回时会将进程状态切换为空闲然后继续接收新的Task。
* $src_worker_id来自于哪个worker进程
* $data 是任务的内容(就是task($data)的$data)
*/
public function onTask($sever, $tastId, $workId, $data)
{
print_r($data);
sleep(10);
return "on task finish";
}
// 执行成功后的回调函数$data为onTask返回的值
/*
* 当worker进程投递的任务在task_worker中完成时,task进程会通过swoole_server->finish()方法将任务处理的结果发送给worker进程。
* $task_id是任务的ID
* $data是任务处理的结果内容
* task进程的onTask事件中没有调用finish方法或者return结果,worker进程不会触发onFinish
* 执行onFinish逻辑的worker进程与下发task任务的worker进程是同一个进程
*/
public function onFinish($server, $taskId, $data)
{
echo "taskId:{$taskId}\n";
echo "finish-data-success:{$data}\n";
}
解释:
当客户端向服务端发送消息会触发onMessage()
然后走到onTask()
onTask()
中有个非常耗时的操作,sleep(10)
等待10s.
onTask()
执行成功会调用onFinish()
Task
是异步任务,会同时进行的~
执行结果
按照正常的PHP
的执行顺序应该是
- 停10s
- 输出
client-push-message
- 输出数组
$data
- 输出
on task finish
- 输出
taskId
和finish-data-success
10s
后的输出结果
应用Task
后的执行结果
- 输出
client-push-message
- 输出数组
$data
- 停10s
- 输出
on task finish
- 输出
taskId
和finish-data-success