Skip to content

Instantly share code, notes, and snippets.

@showsky
Last active April 9, 2019 07:05
Show Gist options
  • Select an option

  • Save showsky/b15c92ac099e79217b526b60c98aa335 to your computer and use it in GitHub Desktop.

Select an option

Save showsky/b15c92ac099e79217b526b60c98aa335 to your computer and use it in GitHub Desktop.
PHP Swoole + Redis = live chats room
<?php
define('SWOOLE_SERVER', '0.0.0.0');
define('SWOOLE_SERVER_PORT', '9502');
define('REDIS_SERVER', '127.0.0.1');
define('REDIS_SERVER_PORT', '6379');
define('REDIS_CHATS_KEY', 'chats');
define('TEMPLATE_REDIS_ROOM_KEY', 'chats_room:%d');
define('TEMPLATE_SEND', '[%s][%s] %s');
define('TEMPLATE_WELCOME', '目前在線人數 %d');
define('LOG_PATH', './log');
define('DEFAULT_ROOM', '0');
define('DEFAULT_NAME', '承德路上的承德');
define('DEFAULT_AVATAR_URL', 'https://avatars3.githubusercontent.com/u/2949145?s=400&v=4');
define('SSL_CERT_FILE', '/etc/letsencrypt/live/gameprice.tw/fullchain.pem');
define('SSL_KEY_FILE', '/etc/letsencrypt/live/gameprice.tw/privkey.pem');
class Server {
private $_redis = NULL;
private $_swoole = NULL;
private $_is_ssl = FALSE;
private $_worker_num = 2;
public function __construct($is_ssl = FALSE, $worker_num = 2) {
$this->_is_ssl = $is_ssl;
$this->_worker_num = $worker_num;
$this->init_redis();
$this->init_server();
}
public function start() {
$this->_swoole->start();
}
private function init_redis() {
$this->_redis = new Redis();
$this->_redis->pconnect(REDIS_SERVER, REDIS_SERVER_PORT);
}
private function init_server() {
// reset hash
$this->_redis->del(REDIS_CHATS_KEY);
$this->_redis->del(sprintf(TEMPLATE_REDIS_ROOM_KEY, '*'));
// swoole config
$swoole_config['worker_num'] = $this->_worker_num;
if ($this->_is_ssl) {
$swoole_config['ssl_key_file'] = SSL_KEY_FILE;
$swoole_config['ssl_cert_file'] = SSL_CERT_FILE;
}
$this->_swoole = new swoole_websocket_server(SWOOLE_SERVER, SWOOLE_SERVER_PORT, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$this->_swoole->set($swoole_config);
$this->_swoole->on('workerstart', array($this, 'onSwooleWorkerstart'));
$this->_swoole->on('open', array($this, 'onSwooleOpen'));
$this->_swoole->on('close', array($this, 'onSwooleClose'));
$this->_swoole->on('message', array($this, 'onSwooleMessage'));
}
public function onSwooleWorkerstart($server, $id) {
$this->init_redis();
}
public function onSwooleClose($server, $fd) {
echo 'close: ' . $fd;
echo PHP_EOL;
// remove redis key
$room_key = $this->_redis->hGet(REDIS_CHATS_KEY, $fd);
$this->_redis->hDel(REDIS_CHATS_KEY, $fd);
$this->_redis->hDel($room_key, $fd);
if ($this->_redis->hLen($room_key) == 0) {
$this->_redis->hDel($room_key);
}
}
public function onSwooleOpen($server, $req) {
$name = (empty($req->get['name'])) ? DEFAULT_NAME : $req->get['name'];
$room = (empty($req->get['room'])) ? DEFAULT_ROOM : $req->get['room'];
$avatar_url = (empty($req->get['avatar'])) ? DEFAULT_AVATAR_URL : $req->get['avatar'];
// load old message
$array_last_line = $this->read_last_line(
$this->get_log_filename(),
10
);
if (count($array_last_line) > 0) {
$message = implode(PHP_EOL, $array_last_line);
$server->push(
$req->fd,
$message
);
}
$room_key = sprintf(
TEMPLATE_REDIS_ROOM_KEY,
$room
);
// welcome
$conn_size = $this->_redis->hLen($room_key);
$server->push(
$req->fd,
sprintf(TEMPLATE_WELCOME, $conn_size)
);
echo 'open fd: ' . $req->fd . PHP_EOL;
echo $name . PHP_EOL;
$this->_redis->hSet(REDIS_CHATS_KEY, $req->fd, $room_key);
$this->_redis->hSet(
$room_key, $req->fd,
$this->build_user_json_data($name, $avatar_url)
);
}
public function onSwooleMessage($server, $frame) {
echo 'message fd: ' . $frame->fd;
echo PHP_EOL;
echo $frame->data;
echo PHP_EOL;
$it = NULL;
$this->_redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);
$room_key = $this->_redis->hGet(REDIS_CHATS_KEY, $frame->fd);
$user_data = json_decode($this->_redis->hGet($room_key, $frame->fd), TRUE);
$name = $user_data['name'];
while ($array_keys = $this->_redis->hScan($room_key, $it)) {
foreach ($array_keys as $key => $value) {
$message = sprintf(
TEMPLATE_SEND,
date("Y-m-d H:i:s"),
$name,
$frame->data
);
// avoid send own message
if ($key == $frame->fd) {
// log
$this->write_log(
$this->get_log_filename(),
$message
);
continue;
}
echo 'k ' . $key . ' v ' . $value . PHP_EOL;
$server->push(
$key,
$message
);
}
}
}
private function build_user_json_data($name, $avatar_url = null) {
$data = array(
'name' => $name,
'avatar_url' => $avatar_url,
'create_time' => date("Y-m-d H:i:s"),
);
return json_encode($data);
}
private function check_log_path($filename) {
if ( ! file_exists(LOG_PATH)) {
mkdir(LOG_PATH, 0777, TRUE);
}
$path_filename = sprintf(
'%s/%s',
LOG_PATH,
$filename
);
return $path_filename;
}
private function write_log($filename, $message) {
$path_filename = $this->check_log_path($filename);
file_put_contents(
$path_filename,
$message . PHP_EOL,
FILE_APPEND
);
}
private function read_last_line($filename, int $line = 5) {
$result = array();
$path_filename = $this->check_log_path($filename);
if ( ! file_exists($path_filename)) {
return $result;
}
$file = new SplFileObject($path_filename);
$result = array();
$total_lines = $file->key();
$start_seek = $total_lines - $line;
if ($start_seek < 0) {
$start_seek = 0;
}
$file->seek($start_seek);
while ( ! $file->eof()) {
$line = trim($file->current());
$file->next();
$result[] = $line;
}
return $result;
}
private function get_log_filename($room = 0) {
return $filename = sprintf(
'chats_%s_%s.log',
$room,
date("Y-m-d")
);
}
}
/* start */
$server = new Server(TRUE, 2);
$server->start();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment