# swooleDemo
**Repository Path**: shanhubei/swooleDemo
## Basic Information
- **Project Name**: swooleDemo
- **Description**: 几个swoole 示例
- **Primary Language**: PHP
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-03-07
- **Last Updated**: 2024-01-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Swoole简单入门示例
Swoole可以让PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务。Swoole 可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(IOT)、车联网、智能家居等领域。
# 前提
使用Composer构建项目,构建好项目。
参考:http://www.shanhubei.com/composer-swoole.html
或者利用搜索引擎。
本篇介绍以下几个示例:
一、使用Swoole发送邮件
二、使用Swoole实现在线聊天
三、使用Systemctl管理Swoole服务
四、使用Swoole实现毫秒级定时任务
五、使用Websocket上传文件
# 部分预览


#  一、使用Swoole发送邮件 
使用composer安装邮件发送组件:phpmailer。
```
composer require phpmailer/phpmailer
```
## 主程序
在目录:src/app/下建立Mail.php,用作Swoole服务端主程序。
```
 4, //worker进程数,一般设置为CPU数的1-4倍  
        'daemonize' => true, //启用守护进程
        'log_file' => '/data/logs/swoole.log', //指定swoole错误日志文件
        'log_level' => 0, //日志级别 范围是0-5,0-DEBUG,1-TRACE,2-INFO,3-NOTICE,4-WARNING,5-ERROR
        'dispatch_mode' => 1, //数据包分发策略,1-轮询模式
        'task_worker_num' => 4, //task进程的数量
        'task_ipc_mode' => 3, //使用消息队列通信,并设置为争抢模式
        //'heartbeat_idle_time' => 600, //一个连接如果600秒内未向服务器发送任何数据,此连接将被强制关闭
        //'heartbeat_check_interval' => 60, //启用心跳检测,每隔60s轮循一次
    ];
    // 邮件服务器配置
    protected $mailConfig = [
        'smtp_server' => 'smtp.163.com',
        'username' => 'username@163.com',
        'password' => '密码/口令',// SMTP 密码/口令
        'secure' => 'ssl', //Enable TLS encryption, `ssl` also accepted
        'port' => 465, // tcp邮件服务器端口
    ];
    // 安全密钥
    protected $safeKey = 'MDDGnQE33ytd2jDFADS39DSEWsdD24sK';
    public function __construct($mailConfig, $options = [])
    {
        // 构建Server对象,监听端口
        $this->serv = new swoole_server($this->host, $this->port);
        if (!empty($options)) {
            $this->options = array_merge($this->options, $options);
        }
        $this->serv->set($this->options);
        $this->mailConfig = $mailConfig;
        // 注册事件
        $this->serv->on('Start', [$this, 'onStart']);
        $this->serv->on('Connect', [$this, 'onConnect']);
        $this->serv->on('Receive', [$this, 'onReceive']);
        $this->serv->on('Task', [$this, 'onTask']);  
        $this->serv->on('Finish', [$this, 'onFinish']);
        $this->serv->on('Close', [$this, 'onClose']);
        // 启动服务
        //$this->serv->start();
    }
    protected function init()
    {
        //
    }
    public function start()
    {
        // Run worker
        $this->serv->start();
    }
    public function onStart($serv)
    {
        // 设置进程名
        cli_set_process_title($this->taskName);
        //记录进程id,脚本实现自动重启
        $pid = "{$serv->master_pid}\n{$serv->manager_pid}";
        file_put_contents($this->pidPath, $pid);
    }
    //监听连接进入事件
    public function onConnect($serv, $fd, $from_id)
    {
        $serv->send($fd, "Hello {$fd}!" );
    }
    // 监听数据接收事件
    public function onReceive(swoole_server $serv, $fd, $from_id, $data)
    {
        $res['result'] = 'failed';
        $key = $this->safeKey;
        $req = json_decode($data, true);
        $action = $req['action'];
        $token = $req['token'];
        $timestamp = $req['timestamp'];
        if (time() - $timestamp > 180) {
            $res['code'] = '已超时';
            $serv->send($fd, json_encode($res));
            exit;
        }
        $token_get = md5($action.$timestamp.$key);
        if ($token != $token_get) {
            $res['msg'] = '非法提交';
            $serv->send($fd, json_encode($res));
            exit;
        }
        $res['result'] = 'success';
        $serv->send($fd, json_encode($res)); // 同步返回消息给客户端
        $serv->task($data);  // 执行异步任务
    }
    /**
    * @param $serv swoole_server swoole_server对象
    * @param $task_id int 任务id
    * @param $from_id int 投递任务的worker_id
    * @param $data string 投递的数据
    */
    public function onTask(swoole_server $serv, $task_id, $from_id, $data)
    {
        $res['result'] = 'failed';
        $req = json_decode($data, true);
        $action = $req['action'];
        echo date('Y-m-d H:i:s')." onTask: [".$action."].\n";
        switch ($action) {
            case 'sendMail': //发送单个邮件
                $mailData = [
                    'emailAddress' => 'xxxxxx@qq.com',
                    'subject' => 'swoole demo',
                    'body' => '测试This is the HTML message body in bold!,
欢迎访问www.shanhubei.com'
                ];
                $this->sendMail($mailData);
                break;
            case 'sendMailQueue': // 批量队列发送邮件
                $this->sendMailQueue();
                break;
            default:
                break;
        }
    }
    /**
    * @param $serv swoole_server swoole_server对象
    * @param $task_id int 任务id
    * @param $data string 任务返回的数据
    */
    public function onFinish(swoole_server $serv, $task_id, $data)
    {
        //
    }
    // 监听连接关闭事件
    public function onClose($serv, $fd, $from_id) {
        echo "Client {$fd} close connection\n";
    }
    public function stop()
    {
        $this->serv->stop();
    }
    private function sendMail($mail_data = [])
    {
        $mail = new PHPMailer(true);
        try {
            $mailConfig = $this->mailConfig;
            //$mail->SMTPDebug = 2;        // 启用Debug
            $mail->isSMTP();   // Set mailer to use SMTP
            $mail->Host = $mailConfig['smtp_server'];  // SMTP服务
            $mail->SMTPAuth = true;                  // Enable SMTP authentication
            $mail->Username = $mailConfig['username'];    // SMTP 用户名
            $mail->Password = $mailConfig['password'];     // SMTP 密码/口令
            $mail->SMTPSecure = $mailConfig['secure'];     // Enable TLS encryption, `ssl` also accepted
            $mail->Port = $mailConfig['port'];    // TCP 端口
            $mail->CharSet  = "UTF-8"; //字符集
            $mail->Encoding = "base64"; //编码方式
            //Recipients
            $mail->setFrom($mailConfig['username'], 'Shanhubei'); //发件人地址,名称
            $mail->addAddress($mail_data['emailAddress'], '亲');     // 收件人地址和名称
            //$mail->addCC('hellowebanet@163.com'); // 抄送
            //Attachments
            if (isset($mail_data['attach'])) {
                $mail->addAttachment($mail_data['attach']);         // 添加附件
            }
            
            //$mail->addAttachment('/tmp/image.jpg', 'new.jpg');    // Optional name
            //Content
            $mail->isHTML(true);                                  // Set email format to HTML
            $mail->Subject = $mail_data['subject'];
            $mail->Body    = $mail_data['body'];
            //$mail->AltBody = '这是在不支持HTML邮件客户端的纯文本格式';
            $mail->send();
            return true;
        } catch (\Exception $e) {
            echo 'Message could not be sent. Mailer Error: '. $mail->ErrorInfo;
            return false;
        }
    }
	
	// 邮件发送队列
    private function sendMailQueue()
    {
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
       
        //$password = '123';
        //$redis->auth($password);
        swoole_timer_tick(1000, function($timer) use ($redis) { // 启用定时器,每1秒执行一次
            $value = $redis->lpop('mailerlist');
            if($value){
                //echo '获取redis数据:' . $value;
                $json = json_decode($value, true);
                $start = microtime(true);
                $rs = $this->sendMail($json);
                $end = microtime(true);
                if ($rs) {
                    echo '发送成功!'.$value.', 耗时:'. round($end - $start, 3).'秒'.PHP_EOL;
                } else { // 把发送失败的加入到失败队列中,人工处理
                    $redis->rpush("mailerlist_err", $value);
                }
            }else{
                swoole_timer_clear($timer); // 停止定时器
                echo "Emaillist出队完成";
            }
        });
    }
    
}
```
## 运行服务端
在public/目录下建立mailServer.php,代码如下:
```
 'smtp.163.com',
    'username' => 'xxxxx@163.com',
    'password' => 'xxxx',// SMTP 密码/口令
    'secure' => 'ssl', //Enable TLS encryption, `ssl` also accepted
    'port' => 465, // tcp邮件服务器端口
];
$server = new Mail($config);
$server->start();
```
你可以注册一个163邮箱,然后开通smtp功能。我DEMO中使用的是163邮箱发邮件发多了被封号了,所以在线演示demo没上传了。配置好邮件服务器参数后,运行:
```
php mailServer.php
```
此时再使用命令`netstat -lntp`查看进程。
## 运行客户端
在public/目录下新建mailClient.php,代码如下:
```
client = new swoole_client(SWOOLE_SOCK_TCP);
    }
    public function connect($type) {
        if( !$this->client->connect("127.0.0.1", 9502 , 1) ) {
            echo "Error: {$this->client->errMsg}[{$this->client->errCode}]\n";
        }
        // fwrite(STDOUT, "请输入消息 Please input msg:");
        // $msg = trim(fgets(STDIN));
        $action = 'sendMail';
		if($type == 'q'){
			$action = 'sendMailQueue';
		}
        $time = time();
        $key = 'MDDGnQE33ytd2jDFADS39DSEWsdD24sK';
        $token = md5($action.$time.$key);
        $data = [
            'action' => $action,
            'token' => $token,
            'timestamp' => $time
        ];
        $msg = json_encode($data);
        $this->client->send( $msg );
        $message = $this->client->recv();
        echo "Get Message From Server:{$message}\n";
    }
}
$type = 'q';  //区分批量和单个发送标志
if($type == 'q'){
	$redis = new \Redis();
	$redis->connect('127.0.0.1', 6379);
	//$password = '123456x';
	///$redis->auth($password);
	$arr = [];
	$arr[0] = [
		'subject' => '注册cloudfog-HA',
		'emailAddress' => '2823175272@qq.com',
		'body' => '您好,您的CloudFog使用的用户名是:123, 密码是:123。
请不要将此邮件泄漏给他人,并尽快登录CloudFog更换新密码。如有疑问请联系管理员。'
	];
	$arr[1] = [
		'subject' => '注册cloudfog2',
		'emailAddress' => '2823175272@qq.com',
		'body' => '网易邮箱'
	];
		  
	foreach ($arr as $k=>$v) {
		$redis->rpush("mailerlist", json_encode($v, JSON_UNESCAPED_UNICODE));
	}	
}
$client = new Client();
$client->connect($type);
```
### 运行命令启动客户端:
```
php mailClient.php
```
此时在命令行窗口会返回如下信息:
```
[root@localhost public]# php mailClient.php 
Get Message From Server:{"result":"success"}
```
这样就已经执行邮件发送任务了,如果出现故障,可以查看日志文件`/data/logs/swoole.log`。
然后你可以去查看对方邮箱是否收到相关邮件。
本文中使用了redis作为简单队列,你也可以使用复杂点的队列rabbitmq。你也可以使用Crontab来做定时任务,不过它最小粒度是分钟级别的。当然对于批量发送邮件,如果你不用php的话,可以用Python或者Java,它们都有相当成熟的解决方案。
# 二、使用Swoole实现在线聊天
### 建立服务端主程序
准备工作就绪后,我们开始来撸代码。
首先在项目目录:src/app/ 下建立`Chat.php`,用作Swoole服务端主程序。
Chat.php文件的主体结构是这样的:
```
 4, //worker进程数,一般设置为CPU数的1-4倍  
        'daemonize' => true, //启用守护进程
        'log_file' => '/data/logs/chatswoole.log', //指定swoole错误日志文件
        'log_level' => 3, //日志级别 范围是0-5,0-DEBUG,1-TRACE,2-INFO,3-NOTICE,4-WARNING,5-ERROR
        'dispatch_mode' => 1, //数据包分发策略,1-轮询模式
    ];
 
    public function __construct($options = [])
    {
        $this->ws = new swoole_websocket_server($this->host, $this->port);
        if (!empty($options)) {
            $this->options = array_merge($this->options, $options);
        }
        $this->ws->set($this->options);
        $this->ws->on("open", [$this, 'onOpen']);
        $this->ws->on("message", [$this, 'onMessage']);
        $this->ws->on("close", [$this, 'onClose']);
    }
    public function start()
    {
        // Run worker
        $this->ws->start();
    }
    public function onOpen(swoole_websocket_server $ws, $request)
    {
        // 设置进程名
        cli_set_process_title($this->taskName);
        //记录进程id,脚本实现自动重启
        $pid = "{$ws->master_pid}\n{$ws->manager_pid}";
        file_put_contents($this->pidFile, $pid);
        echo "server: handshake success with fd{$request->fd}\n";
    }
    public function onMessage(swoole_websocket_server $ws, $frame)
    {
        //$ws->push($frame->fd, "server-push:".date("Y-m-d H:i:s"));
        $connets = $ws->connections;
        echo count($connets)."\n";
        echo $frame->data. "\n";
        if ($frame->data == '美女') {
            $mmpic = [
                'http://pic15.photophoto.cn/20100402/0036036889148227_b.jpg',
                'http://pic23.nipic.com/20120814/5914324_155903179106_2.jpg',
                'http://pic40.nipic.com/20140403/8614226_162017444195_2.jpg'
            ];
            $picKey = array_rand($mmpic);
            $ws->push($frame->fd, file_get_contents($mmpic[$picKey]), WEBSOCKET_OPCODE_BINARY);
        } else {
            $ws->push($frame->fd, $this->reply($frame->data));
        }
        
    }
    public function onClose($ws, $fid)
    {
        echo "client {$fid} closed\n";
        foreach ($ws->connections as $fd) {
            $ws->push($fd, $fid. '已离开!');
        }
    }
    private function reply($str) {
        $str = mb_strtolower($str);
        switch ($str) {
            case 'hello':
                $res = 'Hello, Friend.';
                break;
            case 'fuck':
                $res = 'Fuck bitch.';
          
                break;
            case 'ping':
                $res = 'PONG.';
                break;
            case 'time':
                $res = date('H:i:s');
                break;
            
            default:
                $res = $str;
                break;
        }
        return $res;
    }
}
```
我们需要往`onMessage()`方法里填代码。在这里,服务端接收客户端消息,并作出相应动作。Websocket可以向客户端发送字符串、二进制等形式消息。
```
public function onMessage(swoole_websocket_server $ws, $frame)
    {
        //$ws->push($frame->fd, "server-push:".date("Y-m-d H:i:s"));
     if ($frame->data == '美女') {
            $mmpic = [
                'http://pic15.photophoto.cn/20100402/0036036889148227_b.jpg',
                'http://pic23.nipic.com/20120814/5914324_155903179106_2.jpg',
                'http://pic40.nipic.com/20140403/8614226_162017444195_2.jpg'
            ];
            $picKey = array_rand($mmpic); //随机返回一张图片
            $ws->push($frame->fd, file_get_contents($mmpic[$picKey]), WEBSOCKET_OPCODE_BINARY);
        } else {
            $ws->push($frame->fd, $this->reply($frame->data));
        }
    }
    private function reply($str) {
        $str = mb_strtolower($str);
        switch ($str) {
            case 'hello':
                $res = 'Hello, Friend.';
                break;
            case 'fuck':
                $res = 'Fuck bitch.';
                break;
            case 'ping':
                $res = 'PONG.';
                break;
            case 'time':
                $res = date('H:i:s');
                break;
            
            default:
                $res = $str;
                break;
        }
        return $res;
    }
```
将上面代码加入到Chat.php中,用来处理接收消息并响应回复。响应回复的内容可以根据实际情况修改,本代码只是用来演示。
启动服务端
在public/目录下建立chatServer.php,代码如下:
```
 true
];
$ws = new Chat($opt);
$ws->start();
```
然后运行以下代码启动服务端:
`php chatServer.php`
如果一切正常,你可以netstat -lntp看一下,系统会监听进程名为swooleChat,端口为9504的服务。
### 启动客户端
我们使用HTML5的websocket客户端来连接服务端,本地建立wsClient.html
```

Are we meeting today?

yes

一部分保持在行尾,另一部分换到下一行。

yes

yes