swoole创建websocket服务器

时间:2022-05-16 10:24:31

swoole算是nodejs在php中的一种实现,异步响应请求,性能超强

1 安装准备

1.1 安装swoole前必须保证系统已经安装了下列软件

php-5.3.10 或更高版本

gcc-4.4 或更高版本

make

autoconf

pcre (centos系统可以执行命令:yum install pcre-devel)

1.2 下载并解压

下载地址 https://github.com/swoole/swoole-src/releases

进入页面后选择download链接下的tar.gz的压缩包

下载源代码包后,解压

tar xzvf xxx.tar.gz

在终端进入源码目录,执行下面的命令进行编译和安装

cd swoole
phpize
./configure --enable-swoole-debug
make
sudo make install

编译参数根据自己的需求选择,详情参看官方文档。

1.3 编译安装成功后,修改php.ini

在php.ini中加入 extension=swoole.so

通过在命令行使用 php-m查看,是否安装了swoole

注意:如通重新编译的话需要 make clean

2 构建Swoole基本实例

2.1 tcp服务器实例

(来自w3cschool教程https://www.w3cschool.cn/swoole/bnte1qcd.html)

服务端代码:Server.php

<?php
// Server
class Server
{
private $serv; public function __construct() {
$this->serv = new swoole_server("0.0.0.0", 9501);
$this->serv->set(array(
'worker_num' => 8,
'daemonize' => false,
'max_request' => 10000,
'dispatch_mode' => 2,
'debug_mode'=> 1
)); $this->serv->on('Start', array($this, 'onStart'));
$this->serv->on('Connect', array($this, 'onConnect'));
$this->serv->on('Receive', array($this, 'onReceive'));
$this->serv->on('Close', array($this, 'onClose')); $this->serv->start();
} public function onStart( $serv ) {
echo "Start\n";
} public function onConnect( $serv, $fd, $from_id ) {
$serv->send( $fd, "Hello {$fd}!" );
} public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
echo "Get Message From Client {$fd}:{$data}\n";
} public function onClose( $serv, $fd, $from_id ) {
echo "Client {$fd} close connection\n";
}
}
// 启动服务器
$server = new Server();

从代码中可以看出,创建一个swoole_server基本分三步:

  1. 通过构造函数创建swoole_server对象
  2. 调用set函数设置swoole_server的相关配置选项
  3. 调用on函数设置相关回调函数 关于set配置选项以及on回调函数的具体说明,请参考我整理的swoole文档( 配置选项)

    这里只给出简单介绍。onStart回调在server运行前被调用,onConnect在有新客户端连接过来时被调用,onReceive函数在有数据发送到server时被调用,onClose在有客户端断开连接时被调用。 这里就可以大概看出如何使用swoole:在onConnect处监听新的连接;在onReceive处接收数据并处理,然后可以调用send函数将处理结果发送出去;在onClose处处理客户端下线的事件。

客户端的代码:Client.php

<?php
class Client
{
private $client; public function __construct() {
$this->client = new swoole_client(SWOOLE_SOCK_TCP);
} public function connect() {
if( !$this->client->connect("127.0.0.1", 9501 , 1) ) {
echo "Error: {$fp->errMsg}[{$fp->errCode}]\n";
}
$message = $this->client->recv();
echo "Get Message From Server:{$message}\n"; fwrite(STDOUT, "请输入消息:");
$msg = trim(fgets(STDIN));
$this->client->send( $msg );
}
} $client = new Client();
$client->connect();

这里,通过swoole_client创建一个基于TCP的客户端实例,并调用connect函数向指定的IP及端口发起连接请求。随后即可通过recv()和send()两个函数来接收和发送请求。需要注意的是,这里我使用了默认的同步阻塞客户端,因此recv和send操作都会产生网络阻塞。

使用方法

进入到文件目录,在窗口1先启动php Serve.php,然后再开一个窗口(窗口2)启动php Client.php

窗口1内容:

# root @ WENGINE in /data/learnSwoole [9:24:57] C:130
$ php Server.php
Start
Get Message From Client 1:ceshi1
Client 1 close connection

窗口2内容:

# root @ WENGINE in /data/learnSwoole [9:23:07]
$ php Client.php
Get Message From Server:Hello 1!
请输入消息:ceshi1

2.2 web服务器

服务端代码 http_server.php

$http = new swoole_http_server("0.0.0.0", 9501);

$http->on('request', function ($request, $response) {
var_dump($request->get, $request->post);
$response->header("Content-Type", "text/html; charset=utf-8");
$response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");
}); $http->start();

Http服务器只需要关注请求响应即可,所以只需要监听一个onRequest事件。当有新的Http请求进入就会触发此事件。事件回调函数有2个参数,一个是$request对象,包含了请求的相关信息,如GET/POST请求的数据。

另外一个是response对象,对request的响应可以通过操作response对象来完成。$response->end()方法表示输出一段HTML内容,并结束此请求。

● 0.0.0.0 表示监听所有IP地址,一台服务器可能同时有多个IP,如127.0.0.1本地回环IP、192.168.1.100局域网IP、210.127.20.2 外网IP,这里也可以单独指定监听一个IP

● 9501 监听的端口,如果被占用程序会抛出致命错误,中断执行。

2.3 WebSocket服务器

服务端程序代码 ws_server.php

//创建websocket服务器对象,监听0.0.0.0:9502端口
$ws = new swoole_websocket_server("0.0.0.0", 9502); //监听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 "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
}); //监听WebSocket连接关闭事件
$ws->on('close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
}); $ws->start();

WebSocket服务器是建立在Http服务器之上的长连接服务器,客户端首先会发送一个Http的请求与服务器进行握手。握手成功后会触发onOpen事件,表示连接已就绪,onOpen函数中可以得到$request对象,包含了Http握手的相关信息,如GET参数、Cookie、Http头信息等。

建立连接后客户端与服务器端就可以双向通信了。

● 客户端向服务器端发送信息时,服务器端触发onMessage事件回调

● 服务器端可以调用$server->push()向某个客户端(使用$fd标识符)发送消息

● 服务器端可以设置onHandShake事件回调来手工处理WebSocket握手

运行程序

客户端的代码

可以使用Chrome浏览器进行测试,JS代码为:

var wsServer = 'ws://127.0.0.1:9502';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
console.log("Connected to WebSocket server.");
}; websocket.onclose = function (evt) {
console.log("Disconnected");
}; websocket.onmessage = function (evt) {
console.log('Retrieved data from server: ' + evt.data);
}; websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
};
  • 不能直接使用swoole_client与websocket服务器通信,swoole_client是TCP客户端
  • 必须实现WebSocket协议才能和WebSocket服务器通信,可以使用swoole/framework提供的PHP WebSocket客户端
  • WebSocket服务器除了提供WebSocket功能之外,实际上也可以处理Http长连接。只需要增加onRequest事件监听即可实现Comet方案Http长轮询。

关于onRequest回调

swoole_websocket_server 继承自 swoole_http_server

  • 设置了onRequest回调,websocket服务器也可以同时作为http服务器
  • 未设置onRequest回调,websocket服务器收到http请求后会返回http 400错误页面
  • 如果想通过接收http触发所有websocket的推送,需要注意作用域的问题,面向过程请使用“global”对swoole_websocket_server进行引用,面向对象可以把swoole_websocket_server设置成一个成员属性

可以创建更多的服务器 参照官方文档尝试

https://wiki.swoole.com/wiki/page/475.html

3 使用laravel5.5实现的前后台通信实例

主要思路是使用php artisan 自建命令控制服务端,使用HTML5的websocket实现客户端功能

服务端:app/Console/Commands/Websocket.php内容

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use swoole_http_request;
use swoole_http_response;
use swoole_websocket_server; class WebSocket extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'websocket
{cmd=start : can use start|stop|status|restart}
{--daemon : set to run in daemonize mode}
'; /**
* The console command description.
*
* @var string
*/
protected $description = 'swoole server control'; /**
* server
*
* @var swoole_websocket_server
*/
private $server; /**
* * TYPE_ADMIN
* */
const TYPE_ADMIN = 0X00; /**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
} /**
* 处理不同command信息
*
* @return mixed
*/
public function handle()
{
$command = $this->argument('cmd');
$option = $this->option('daemon'); switch ($command) {
case 'start':
$this->initWs($option);
break;
case 'stop':
$res = $this->sendAdminRequest($command);
if ($res){
$this->info('stop the server successfully');
} else {
$this->info('the server is not running');
}
break;
case 'status':
$res = $this->sendAdminRequest($command);
if ($res){
$this->info($res);
} else {
$this->info('the server is not running');
}
break;
case 'restart':
$res = $this->sendAdminRequest($command);
if ($res){
$this->info('restart the server successfully');
} else {
$this->info('the server is not running');
}
break;
default:
$this->info('请按照下面格式输入命令:php artisan websocket {start|stop|status|restart}');
break;
} } //初始化服务端
public function initWs($daemonize = false)
{
if ($daemonize) {
$this->info('Starting Websocke server in daemon mode...');
} else {
$this->info('Starting Websocke server in interactive mode...');
} $server = new swoole_websocket_server('0.0.0.0', 9501);
$server->set([
'daemonize' => $daemonize,
'log_file' => '/var/log/websocket.log'
]); $server->on('close', function ($server, $fd) {
$this->info('close websocket server');
}); $server->on('open', function (swoole_websocket_server $server, $request) {
$this->info('websocket open');
}); $server->on('open', [$this, 'onOpen']);
$server->on('close', [$this, 'onClose']);
$server->on('message', [$this, 'onMessage']);
$server->on('request', [$this, 'onRequest']); $this->server = $server;
$this->server->start();
} public function onOpen(swoole_websocket_server $server, $request)
{
$this->info('websocket open');
} public function onClose($server, $fd)
{
$this->info('close websocket server');
} public function onMessage(swoole_websocket_server $server, $frame)
{
$this->info($frame->data);
$data = json_decode($frame->data, true); //对data进行逻辑处理
$reply = '发送的信息是:' . $data['message'];
$response = [
'status' => true,
'data' => $reply
];
$server->push($frame->fd, json_encode($response));
} //websocket客户端同样支持http协议
public function onRequest(swoole_http_request $request, swoole_http_response $response)
{
if ($request->post['type'] == self::TYPE_ADMIN) {
$ret = json_encode($this->commandHandle($request->post['content']));
return $response->end($ret);
}
} //操作命名
public function commandHandle($command) {
if ($command == 'status') {
$this->info('handle status');
return $this->server->stats();
}
if ($command == 'restart') {
$this->info('handle restart');
return $this->server->reload();
}
if ($command == 'stop') {
$this->info('handle stop');
return $this->server->stop() && $this->server->shutdown();
}
return 'Unknown Command';
} //发送http请求
public function sendAdminRequest($content) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:9501");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Expect:']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'type' => self::TYPE_ADMIN,
'content' => $content
]); $response = curl_exec($ch);
curl_close($ch);
return $response;
} }

客户端内容

<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>websocket client</title>
</head>
<body>
<div>
<input id="message-content" type="text" name="message" />
<button onclick="sendMessage()">发送消息</button>
</div>
</body>
<script>
var wsServer = 'ws://115.159.81.46:9501';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
console.log("Connected to WebSocket server.");
}; websocket.onclose = function (evt) {
console.log("Disconnected");
}; websocket.onmessage = function (evt) {
console.log('从服务器接收到json信息: ' + evt.data);
alert('服务器返回信息:' + JSON.parse(evt.data).data);
}; websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
}; function sendMessage(){
var content = document.getElementById('message-content').value;
var data = {
message : content,
}
websocket.send(JSON.stringify(data));
};
</script>
</html>

启动websocket服务器

进入系统根目录,

php artisan websocket [--daemon] //是否使用daemon模式

php artisan websocket start|stop|status|restart //默认是start