? swoole的資料:
[https://wiki.swoole.com](https://wiki.swoole.com/)
主要看了 環境依賴、編譯安裝、快速起步
2. 起步 聊天室 websocket 參見ws.zip。
開始遇到的問題:
如何重載
Swoole提供了柔性終止/重啟的機制,管理員只需要向SwooleServer發送特定的信號,Server的worker進程可以安全的結束。
? SIGTERM: 向主進程/管理進程發送此信號服務器將安全終止
? 在PHP代碼中可以調用$serv->shutdown()完成此操作
? SIGUSR1: 向主進程/管理進程發送SIGUSR1信號,將平穩地restart所有worker進程
? 在PHP代碼中可以調用$serv->reload()完成此操作
? swoole的reload有保護機制,當一次reload正在進行時,收到新的重啟信號會丟棄
? 如果設置了user/group,Worker進程可能沒有權限向master進程發送信息,這種情況下必須使用root賬戶,在shell中執行kill指令進行重啟
由于swoole 是常駐內存的,如果修改了代碼 直接繼續發送包代碼是不生效的。需要reload
1. 設置進程標題,通過ps grep 查找來kill
~~~
cli_set_process_title('chat_process'); 這個mac 不支持
kill -15 pid
~~~
2. 將進程pid 寫入腳本,root用戶手動重啟
~~~
ps -efH|grep swoole mac上 是 ps -efh|grep php
$server->on('start',function($serv ) {
// cli_set_process_title('chat_process');
$managerPid = $serv->manager_pid;
$shString = <<<SH
echo "Reloading..."
kill -USR1 {$managerPid}
echo "Reloaded"
SH;
$sh_file = '.reload_manager.sh';
file_put_contents($sh_file, $shString);
});
~~~
3. 安裝inotify 擴展 監聽文件,將文件 在onWorkStart 回調中require\_once. 監聽到文件變化了,自動重啟
參考sd框架
4. 鏈接的fd 不好感知是哪個設備,如何將針對不同設備形成定向廣播
$server->connection\_list()? 獲取全部在線鏈接
5. 數據庫超時
暫時設置了數據庫的重連參數
'break\_reconnect' => true,
6. 引入tp框架后,調試輸出的不顯示了
介入其他項目里的 alert dlert 函數
7. 報錯在終端不方便追蹤bug,
接管異常
設計同步方案
針對重載,在根目錄建立server\_function.php workStart里 require\_once
~~~
<?php
function onMessage($server, $frame){
\Think\App::invokeClass('\app\index\controller\Message', [])->receive($server, $frame);
}
function onTask($server, $task_id, $src_worker_id, $data){
\Think\App::invokeClass('\app\index\controller\Task', [])->run($server, $task_id, $src_worker_id, $data);
}
function onClose($server, $fd){
$data = json_encode([
'op' => 'after_close',
'data' => '',
'from_fd' => $fd,
], JSON_UNESCAPED_UNICODE);
$server->task($data);
}
function onRequest ($server, $request, $response) {
//請求過濾
if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
return $response->end();
}
// 環境常量
$response->header('Access-Control-Allow-Origin', "*");
$_SERVER = [
'argv' => [],
];
$_GET = $request->get?:[];
$_POST = $request->post;
foreach ($request->server as $key=>$value) {
$_SERVER[strtoupper($key)] = $value;
}
$_COOKIE = $request->cookie;
$_FILES = $request->files;
$ret = \Think\App::invokeClass('\app\index\controller\Index', [$server, $request, $response])->request();
$response->end($ret);
exit();
}
~~~
主文件index.php
~~~
<?php
namespace think;
global $server;
$server = new \swoole_websocket_server("0.0.0.0", 9501);
$server->set(
['task_worker_num'=>10]
);
$server->on('start',function($serv ) {
// cli_set_process_title('chat_process');
$managerPid = $serv->manager_pid;
$shString = <<<SH
echo "Reloading..."
kill -USR1 {$managerPid}
echo "Reloaded"
SH;
$sh_file = '.reload_manager.sh';
file_put_contents($sh_file, $shString);
});
$server->on('WorkerStart',function($serv , $worker_id) {
define('ERROR_LOG_TYPE', 'ws_error_log');
define('APP_PATH', __DIR__ . '/application/');
define('THINK_PATH', __DIR__ . '/thinkphp/');
// 加載框架引導文件
require_once __DIR__ . '/base.php';
$_SERVER = [
'REQUEST_METHOD' => 'GET',
'argv' => [],
];
});
$server->on('open', function (\swoole_websocket_server $server, $request) {
echo "server: handshake success with fd{$request->fd}\n";
});
$server->on('message', function (\swoole_websocket_server $server, $frame) {
echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
$_SERVER['REQUEST_TIME'] = time();
App::initCommon();
require_once __DIR__. '/server_function.php';
onMessage($server, $frame);
});
$server->on('close', function ($ser, $fd) {
echo "client {$fd} closed\n";
require_once __DIR__. '/server_function.php';
onClose($ser, $fd);
});
$server->on('finish', function ($ser, $fd) {
return true;
});
$server->on('task', function(\swoole_websocket_server $server, $task_id, $src_worker_id, $data){
App::initCommon();
require_once __DIR__. '/server_function.php';
onTask($server, $task_id, $src_worker_id, $data);
return true;
});
$server->start();
~~~
首先,參考?[https://www.tuicool.com/articles/emY3Ar](https://www.tuicool.com/articles/emY3Ar)? 有了思路 ,然后閱讀tp5源碼 app 發現可以用靜態方法將 映射調用某個類,因此。
將task message 映射到Task 的run? 和 Message控制器 的 receive 方法里。
然后 定義數據格式為json 然后 反json后 動態op 參數調用內部方法。
消息的處理
經過討論,為了方便客戶端離線后臺看到消息,我們設計了數據表 ws\_message表。
業務消息必然和shop\_id table\_id 相關,然后保存 發送方from\_client\_id 和接受方to\_client\_id 并記錄 發送消息設備類型 主還是副
經過討論,主設備發送消息 無需記錄, 消息 中data 和user\_info 為序列化字段。
Message 類的組成
~~~
<?php
namespace app\index\controller;
use app\common\lib\SystemLog;
use app\index\model\WsDevices;
use app\index\model\WsMessage;
use app\index\model\WsOnlineClients;
class Message
{
public function __construct()
{
config('default_return_type', 'json');
}
public $from_fd;
public $server;
public $date_format = 'Y-m-d H:i:s';
public function success($info, $data = [])
{
debug('api_end');
$push_data = [
'code' => 0,
'msg' => $info,
'time' => date($this->date_format, $_SERVER['REQUEST_TIME']),
'data' => $data,
'ttfb_time' => debug('api_begin', 'api_end', 6) . 's',
];
return $this->server->push($this->from_fd, json_encode($push_data, JSON_UNESCAPED_UNICODE));
}
public function error($info, $data = [])
{
debug('api_end');
$push_data = [
'code' => 1,
'msg' => $info,
'time' => date($this->date_format, $_SERVER['REQUEST_TIME']),
'data' => $data,
'ttfb_time' => debug('api_begin', 'api_end', 6) . 's',
];
return $this->server->push($this->from_fd, json_encode($push_data, JSON_UNESCAPED_UNICODE));
}
public function receive($server, $frame)
{
debug('api_begin');
$data = $frame->data;
$this->from_fd = $frame->fd;
$this->server = $server;
echo $data . '\n';
$data = json_decode($data, 1);
if (is_array($data)) {
if (method_exists($this, $data['op'])) {
$call = $data['op'];
unset($data['op']);
$this->$call($server, $frame, $data);
} else {
goto rawMsg;
}
} else {
rawMsg:
$this->rawMsg($server, $frame, $frame->data);
}
}
// 登錄
public function login($server, $frame, $data)
{
// dlert2("fd:#{$frame->fd}|shop_id:{$data['shop_id']}|client_id:{$data['client_id']}|type:{$data['type']}的設備登錄了,".datetime());
WsDevices::login($frame->fd, $data['shop_id'], $data['client_id'], $data['type']);
if($data['type'] == '主'){
// 登錄后重發數據, 客戶端重新鏈接后會重新拉去列表
// $data = json_encode([
// 'op' => 'after_login',
// 'client_id' => $data['client_id'],
// 'from_fd' => $frame->fd,
// ], JSON_UNESCAPED_UNICODE);
// $server->task($data);
}
}
// 登出
public function logout($server, $frame, $data)
{
$server->close($frame->fd);
}
// 加菜 退菜 打折 贈送 結賬
public function notify($server, $frame, $data)
{
if ($data['from_client_type'] == '主') {
$sub_clients = WsDevices::getSubClientByShopId($data['shop_id']);
if ($sub_clients) {
$sub_client_ids = array_column($sub_clients, 'client_id');
$fds = WsOnlineClients::where('client_id', 'in', $sub_client_ids)->column('fd');
foreach ($fds as $fd) {
$server->push($fd, $data);
}
}
} else {
$main_clients = WsDevices::getMainClientByShopId($data['shop_id']);
if ($main_clients) {
$ws_message_client_ids = [];
foreach ($main_clients as $main_client) {
$id = WsMessage::add($data['shop_id'], $data['table_id'], $data['type'], $data['data'], $data['user_info'], $data['from_client_id'], $main_client['client_id'], '副', $data['status']);
$ws_message_client_ids[$main_client['client_id']] = $id;
}
$main_client_ids = array_column($main_clients, 'client_id');
if ($main_client_ids) {
$fds = WsOnlineClients::where('client_id', 'in', $main_client_ids)->column('client_id,fd');
$online_messages = array_intersect_key($ws_message_client_ids, $fds);
foreach ($fds as $client_id=>$fd) {
if(isset($online_messages[$client_id])){
$msg_id = $online_messages[$client_id];
$push_data = [
'id' => $msg_id,
'status' => $data['status'],
'table_id' => $data['table_id'],
'type' => $data['type'],
'title' => "桌id:{$data['table_id']} {$data['type']}",
];
$info = $server->connection_info($fd);
if($info['websocket_status'] == 3){
$ret = $server->push($fd, json_encode($push_data, JSON_UNESCAPED_UNICODE));
if($ret){
WsMessage::where('id', $msg_id)->update(['is_delivered'=>1]);
}else{
$fail_msg = sprintf('時間:%s,店鋪id:%d下子設備 client_id:%s 向主設備client_id %s發送 【桌號id:%s,類型:%s,消息id:%d】的消息失敗', datetime(),$data['shop_id'], $data['from_client_id'], $client_id, $data['table_id'], $data['type'], $msg_id);
alert(__CLASS__.':'.__FUNCTION__.':L'.__LINE__.PHP_EOL.$fail_msg);
dlert(__CLASS__.':'.__FUNCTION__.':L'.__LINE__.PHP_EOL.$fail_msg);
SystemLog::error_log($fail_msg, __CLASS__.':'.__FUNCTION__.':L'.__LINE__, ERROR_LOG_TYPE);
}
}
}
}
}
} else {
$this->error('店鋪主設備未記錄,請先聯系商家完成主設備初始化');
}
}
}
// 處理非序列化消息
public function rawMsg($server, $frame, $msg)
{
$server->push($frame->fd, "{$frame->data}");
// dlert2($msg);
if (stripos($msg, '說') !== false) {
$data = json_encode([
'op' => 'say',
'data' => $msg,
'from_fd' => $frame->fd,
], JSON_UNESCAPED_UNICODE);
$server->task($data);
}
// 獲取特殊管理的消息
if (stripos($msg, 'from_admin') !== false) {
parse_str($msg, $params);
dump($params);
$op = $params['op'];
switch ($op) {
case 'reload':
echo 'reloading server';
$server->reload();
break;
case 'get_connections':
$server->push($frame->fd, json_encode($server->connection_list() ?: [], JSON_UNESCAPED_UNICODE));
break;
case 'send_messages':
$data = json_encode([
'op' => 'admin_say',
'data' => $params['content'],
'from_fd' => $frame->fd,
'to_fds' => $params['to_fds'],
], JSON_UNESCAPED_UNICODE);
$server->task($data);
break;
default:
break;
}
}
}
}
~~~
rawMsg? 是用于不是json 格式的字符串,處理消息。用于客戶端測試 echo服務器。和其他特殊命令
login? 用于自定義登錄消息,記錄在線設備,更新設備狀態
logout 下線(設備-刪除ws\_online\_clients)記錄。通過手動close fd 抓onClose里的 task 異步任務。
notify 主要邏輯 主要用于 產生消息記錄,并廣播至同店鋪主設備(多個主設備比較復雜,需要二次同步,不實現)。push成功后更新is\_delived 字段
http接口
1.獲取消息列表(待處理-shop\_id)[ws.weiwoju.com/api.php/index/index/messageList/shop\_id/{shop\_id}](http://ws.weiwoju.com/api.php/index/index/messageList/shop_id/%7Bshop_id%7D)
直接查詢
2. 處理標記消息
[ws.weiwoju.com/api.php/index/index/deal/{id:1,status}](http://ws.weiwoju.com/api.php/index/index/deal/%7Bid:1,status%7D)
3. 獲取主設備在線狀態
[ws.weiwoju.com/api.php/index/index/check\_online\_main/shop\_id/{shop\_id}](http://ws.weiwoju.com/api.php/index/index/check_online_main/shop_id/%7Bshop_id%7D)
其實就是查詢online表和devices 表的
4. http 內請求websocket
[ws.weiwoju.com/api.php/index/index/notify\_main/](http://ws.weiwoju.com/api.php/index/index/notify_main/)參數見代碼
和web端請求格式一致。
進程模型
ssl開啟
生成證書:
SSL支持
本章將詳細講解如何制作證書以及如何開啟Swoole的SSL的單向、雙向認證。
準備工作
選擇任意路徑,執行如下命令創建文件夾結構
~~~
mkdir ca
cd ca
mkdir private
mkdir server
mkdir newcerts
~~~
在ca目錄下創建openssl.conf文件,文件內容如下
~~~
[ ca ]
default_ca = foo # The default ca section
[ foo ]
dir = /path/to/ca # top dir
database = /path/to/ca/index.txt # index file.
new_certs_dir = /path/to/ca/newcerts # new certs dir
certificate = /path/to/ca/private/ca.crt # The CA cert
serial = /path/to/ca/serial # serial no file
private_key = /path/to/ca/private/ca.key # CA private key
RANDFILE = /path/to/ca/private/.rand # random number file
default_days = 365 # how long to certify for
default_crl_days= 30 # how long before next CRL
default_md = md5 # message digest method to use
unique_subject = no # Set to 'no' to allow creation of
# several ctificates with same subject.
policy = policy_any # default policy
[ policy_any ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = match
localityName = optional
commonName = optional
emailAddress = optional
~~~
其中,/path/to/ca/是ca目錄的絕對路徑。
創建ca證書
在ca目錄下創建一個shell腳本,命名為new\_ca.sh。文件內容如下:
~~~
#!/bin/sh
openssl genrsa -out private/ca.key
openssl req -new -key private/ca.key -out private/ca.csr
openssl x509 -req -days 365 -in private/ca.csr -signkey private/ca.key -out private/ca.crt
echo FACE > serial
touch index.txt
openssl ca -gencrl -out private/ca.crl -crldays 7 -config "./openssl.conf"
~~~
執行sh new\_ca.sh命令,創建ca證書。生成的證書存放于private目錄中。
注意?在創建ca證書的過程中,需要輸入一些信息。其中,countryName、stateOrProvinceName、organizationName、organizationalUnitName這四個選項的內容必須要填寫,并且需要記住。在生成后續的證書過程中,要保證這四個選項的內容一致。
創建服務端證書
在ca目錄下創建一個shell腳本,命名為new\_server.sh。文件內容如下:
~~~
#!/bin/sh
openssl genrsa -out server/server.key
openssl req -new -key server/server.key -out server/server.csr
openssl ca -in server/server.csr -cert private/ca.crt -keyfile private/ca.key -out server/server.crt -config "./openssl.conf"
~~~
執行sh new\_ca.sh命令,創建ca證書。生成的證書存放于server目錄中。
創建客戶端證書
在ca目錄下創建一個shell腳本,命名為new\_client.sh。文件內容如下:
~~~
#!/bin/sh
base="./"
mkdir -p $base/users/
openssl genrsa -des3 -out $base/users/client.key 1024
openssl req -new -key $base/users/client.key -out $base/users/client.csr
openssl ca -in $base/users/client.csr -cert $base/private/ca.crt -keyfile $base/private/ca.key -out $base/users/client.crt -config "./openssl.conf"
openssl pkcs12 -export -clcerts -in $base/users/client.crt -inkey $base/users/client.key -out $base/users/client.p12
~~~
執行sh new\_ca.sh命令,創建ca證書。生成的證書存放于users目錄中。 進入users目錄,可以看到有一個client.p12文件,這個就是客戶端可用的證書了,但是這個證書是不能在php中使用的,因此需要做一次轉換。命令如下:
~~~
openssl pkcs12 -clcerts -nokeys -out cer.pem -in client.p12
openssl pkcs12 -nocerts -out key.pem -in client.p12
~~~
以上兩個命令會生成cer.pem和key.pem兩個文件。其中,生成key.pem時會要求設置密碼,這里記為client\_pwd
注意?如果在創建客戶端證書時,就已經給client.p12設置了密碼,那么在轉換格式的時候,需要輸入密碼進行轉換
最終結果
以上步驟執行結束后,會得到不少文件,其中需要用的文件如下表所示:
| 文件名 | 路徑 | 說明 |
| --- | --- | --- |
| ca.crt | ca/private/ | ca證書 |
| server.crt | ca/server/ | 服務器端證書 |
| server.key | ca/server/ | 服務器端秘鑰 |
| cer.pem | ca/client/ | 客戶端證書 |
| key.pem | ca/client/ | 客戶端秘鑰 |
SSL單向認證
Swoole開啟SSL
Swoole開啟SSL功能需要如下參數:
~~~
$server = new swoole_server("127.0.0.1", "9501" , SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL );
$server = new swoole_http_server("127.0.0.1", "9501" , SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL );
~~~
并在swoole的配置選項中增加如下兩個選項:
~~~
$server->set(array(
'ssl_cert_file' => '/path/to/server.crt',
'ssl_key_file' => '/path/to/server.key',
));
~~~
這時,swoole服務器就已經開啟了單向SSL認證,可以通過https://127.0.0.1:9501/進行訪問。
SSL雙向認證
服務器端設置
雙向認證指服務器也要對發起請求的客戶端進行認證,只有通過認證的客戶端才能進行訪問。 為了開啟SSL雙向認證,swoole需要額外的配置參數如下:
~~~
$server->set(array(
'ssl_cert_file' => '/path/to/server.crt',
'ssl_key_file' => '/path/to/server.key',
'ssl_client_cert_file' => '/path/to/ca.crt',
'ssl_verify_depth' => 10,
));
~~~
客戶端設置
這里我們使用CURL進行https請求的發起。 首先,需要配置php.ini,增加如下配置:
`curl.cainfo=/path/to/ca.crt`
發起curl請求時,增加如下配置項:
~~~
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, '2');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // 只信任CA頒布的證書
curl_setopt($ch, CURLOPT_SSLCERT, "/path/to/cer.pem");
curl_setopt($ch, CURLOPT_SSLKEY, "/path/to/key.pem");
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLCERTPASSWD, '******'); // 創建客戶端證書時標記的client_pwd密碼
~~~
這時,就可以發起一次https請求,并且被swoole服務器驗證通過了。
服務端域名 要開啟那個ssl:
~~~
listen 443 ssl;
ssl_certificate server.crt;
ssl_certificate_key server.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
~~~
這樣 443 后ssl? 去掉ssl on? 可以 混合訪問
生成配置中的一些設置,經過測試不填host可以任意域名:
~~~
Country Name (2 letter code) []:CN
State or Province Name (full name) []:Zhejiang
Locality Name (eg, city) []:Hangzhou
Organization Name (eg, company) []:weiwoju
Organizational Unit Name (eg, section) []:ws
password = weiwoju
~~~
- 代碼規范
- 編程規范(psr-1,2)
- 編程規范(原作者的建議)
- JS篇
- 正則校驗
- 檢測密碼強度
- 常用方法
- 頁面下載文件
- 手機類型判斷
- 字符串截取方法
- 全選/全不選
- js 判斷瀏覽器
- JS判斷兩個日期大小
- JS 執行計時器
- 回車提交
- 阻止冒泡
- js每3位用逗號隔開的形式
- JS跟APP端交互
- 常用的工具類
- PHP地理位置計算
- 百度地圖兩點坐標距離計算
- 生成唯一ID
- 身份證驗證類
- 阿拉伯數字轉化為大寫
- 獲取漢字首個拼音
- PHP中文轉拼音
- Rand類庫
- PHP Date()函數詳細參數
- 時間
- PHP每3位用逗號隔開的形式
- Elasticsearch全文搜索引擎
- 全文搜索引擎 Elasticsearch
- 設計模式
- 單例模式
- 依賴注入VS控制反轉
- 工廠模式
- Gitlab
- git常用命令
- PHPStorm關聯gitlab
- Thinkphp5
- 工具類
- 擴展
- think-queue——ThinkPHP隊列擴展
- qr-code——好用的二維碼生成類庫
- ThinkPHP5 社會化登錄組件
- PHP SDK——助力支付寶小程序后端開發
- tp5.0使用predis訪問redis集群
- tp5+swoole
- 網絡知識
- HTTP知識
- 小程序
- 知識推薦