server.php
阿新 • • 發佈:2018-12-22
<?php
//設定超時的時間為無限
set_time_limit(0);
class WebSocket {
//使用者儲存伺服器socket
private $socket;
//用於儲存客戶端socket
private $accept = [];
//用於儲存不同聊天室的客戶端
private $clientRoom = [];
//用於儲存客戶端的連結狀態。是否握手
private $ishand = [];
//當前線上使用者
private $onlineUser = [];
//用於儲存不同聊天室的使用者列表
private $userRoom = [];
//連結最大值
private $maxConnect = 10;
public function __construct() {
//建立socket
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, TRUE);
//繫結埠
socket_bind($this->socket, '127.0.0.1', 9795);
//開始監聽埠
socket_listen($this->socket, $this->maxConnect);
}
/**
* 處理socket
*/
public function start() {
while (true) {
//將所有socket存入一個數組:包括伺服器與所有的客戶端socket
$sockets = $this->accept;
$sockets[] = $this->socket;
//使用select檢視當前活躍的socket
socket_select($sockets, $write, $except, NULL);
//當程式走到這一步的時候,證明有活躍的socket,需要處理
foreach ($sockets as $socket) {
//判斷當前活躍的socket是否為伺服器的socket,如果是,證明有新的連結
if ($socket == $this->socket) {
$accept = socket_accept($this->socket);
// echo 'get new socket:' . $accept . "\r\n";
//獲取新的客戶端socket並存入accept陣列中
$this->accept[] = $accept;
//設定當前客戶端握手狀態為false
$this->ishand[(int) $accept] = false;
continue;
}
//從當前活躍的客戶端socket獲取內容
$content = @socket_read($socket, 4096);
//當socket中的內容長度小於7的時候,我們就需要斷開與客戶端的連結
if (strlen($content) < 7) {
// echo 'close socket:' . $socket . "\r\n";
socket_close($socket);
$key = array_search($socket, $this->accept);
unset($this->accept[$key]);
unset($this->ishand[(int) $socket]);
$userData = $this->onlineUser[(int) $socket];
unset($this->onlineUser[(int) $socket]);
unset($this->clientRoom[$userData['room']][(int) $socket]);
unset($this->userRoom[$userData['room']][(int) $socket]);
$this->sendLogoutMessage($userData['nickname'], $userData['room']);
continue;
}
if (!$this->ishand[(int) $socket]) {
// echo 'hand socket:' . $socket . "\r\n";
//如果當前客戶端沒有握手,執行握手流程,建立連結
$this->dohandshake($socket, $content);
$this->ishand[(int) $socket] = true;
} else {
$this->message($content, $socket);
}
}
}
}
private function sendLogoutMessage($username, $room) {
$data = [
'msg_type' => 'logout',
'msg' => '【' . $username . '】離開了我們!',
'users' => $this->userRoom[$room]
];
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
$encodeData = $this->encode($json);
foreach ($this->clientRoom[$room] as $client) {
socket_write($client, $encodeData, strlen($encodeData));
}
}
/*
* 處理資料
*/
private function message($content, $client) {
$jsonData = $this->decode($content);
$dataArr = json_decode($jsonData, true);
if (!$dataArr) {
return null;
}
switch ($dataArr['act']) {
case '@': $this->privateMessage($dataArr, $client);
break;
default: $this->baseMessage($dataArr, $client);
break;
}
}
private function baseMessage($dataArr, $client) {
$room = $dataArr['room'];
if ($dataArr['act'] == 'login') {
$data = [
'msg_type' => 'login',
'msg' => '歡迎【' . $dataArr['nickname'] . '】登入!',
];
$this->onlineUser[(int) $client] = [
'nickname' => $dataArr['nickname'],
'login_time' => date('Y-m-d H:i:s'),
'client' => $client,
'room' => $room
];
//處理當前客戶端在哪個聊天室
$this->clientRoom[$room][(int) $client] = $client;
$this->userRoom[$room][(int) $client] = $dataArr['nickname'];
} else {
$data = [
'msg_type' => 'text',
'nickname' => $dataArr['nickname'],
'msg' => $dataArr['con']
];
}
$data['users'] = $this->userRoom[$room];
$encodeData = $this->encode(json_encode($data, JSON_UNESCAPED_UNICODE));
foreach ($this->clientRoom[$room] as $accept) {
socket_write($accept, $encodeData, strlen($encodeData));
}
return;
}
private function privateMessage($dataArr, $client) {
$room = $dataArr['room'];
//取出所有的客戶端列表,並且使用客戶端的暱稱作為下標
$user = array_column($this->onlineUser, 'client', 'nickname');
if($this->onlineUser[(int) $user[$dataArr['to']]]['room'] != $room){
return null;
}
//先組裝個數據
$data = [
'msg_type' => '@me',
'from' => $dataArr['from'],
'to' => $dataArr['to'],
'msg' => $dataArr['con']
];
//賦值一個線上使用者列表
$data['users'] = $this->userRoom[$room];
//將資料轉換為json
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
//將json轉換為資料幀
$encodeData = $this->encode($json);
//給接收人的客戶端傳送訊息
socket_write($user[$dataArr['to']], $encodeData, strlen($encodeData));
//給傳送人的客戶端傳送訊息
socket_write($client, $encodeData, strlen($encodeData));
return;
}
/*
* 響應客戶端握手頭
* @param $sock 客戶端資源
* @param $data 客戶端請求頭
*/
private function dohandshake($sock, $data) {
//擷取客戶端請求頭中Sec-WebSocket-Key的值
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) {
//將key值後面追加一個固定字串258EAFA5-E914-47DA-95CA-C5AB0DC85B11 然後sha1加密再轉base64編碼
$response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
//拼接一個握手頭 注意每一個引數後面都需要使用 \r\n 換行
$upgrade = "HTTP/1.1 101 Switching Protocol\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " . $response . "\r\n\r\n";
//將內容寫入客戶端套接字,客戶端將會接收到資料並驗證成功即可與伺服器建立連結
socket_write($sock, $upgrade, strlen($upgrade));
}
}
/**
* 解碼過程:可以解析客戶端傳送的資料幀為一個json
*/
private function decode($buffer) {
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;
if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return $decoded;
}
/**
* 編碼過程:可以將json資料轉換為客戶端可以解析的資料幀
*/
private function encode($buffer) {
$length = strlen($buffer);
if ($length <= 125) {
return "\x81" . chr($length) . $buffer;
} else if ($length <= 65535) {
return "\x81" . chr(126) . pack("n", $length) . $buffer;
} else {
return "\x81" . char(127) . pack("xxxxN", $length) . $buffer;
}
}
}
$socket = new WebSocket();
$socket->start();
//設定超時的時間為無限
set_time_limit(0);
class WebSocket {
//使用者儲存伺服器socket
private $socket;
//用於儲存客戶端socket
private $accept = [];
//用於儲存不同聊天室的客戶端
private $clientRoom = [];
//用於儲存客戶端的連結狀態。是否握手
private $ishand = [];
//當前線上使用者
private $onlineUser = [];
//用於儲存不同聊天室的使用者列表
private $userRoom = [];
//連結最大值
private $maxConnect = 10;
public function __construct() {
//建立socket
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, TRUE);
//繫結埠
socket_bind($this->socket, '127.0.0.1', 9795);
//開始監聽埠
socket_listen($this->socket, $this->maxConnect);
}
/**
* 處理socket
*/
public function start() {
while (true) {
//將所有socket存入一個數組:包括伺服器與所有的客戶端socket
$sockets = $this->accept;
$sockets[] = $this->socket;
//使用select檢視當前活躍的socket
socket_select($sockets, $write, $except, NULL);
//當程式走到這一步的時候,證明有活躍的socket,需要處理
foreach ($sockets as $socket) {
//判斷當前活躍的socket是否為伺服器的socket,如果是,證明有新的連結
if ($socket == $this->socket) {
$accept = socket_accept($this->socket);
// echo 'get new socket:' . $accept . "\r\n";
//獲取新的客戶端socket並存入accept陣列中
$this->accept[] = $accept;
//設定當前客戶端握手狀態為false
$this->ishand[(int) $accept] = false;
continue;
}
//從當前活躍的客戶端socket獲取內容
$content = @socket_read($socket, 4096);
//當socket中的內容長度小於7的時候,我們就需要斷開與客戶端的連結
if (strlen($content) < 7) {
// echo 'close socket:' . $socket . "\r\n";
socket_close($socket);
$key = array_search($socket, $this->accept);
unset($this->accept[$key]);
unset($this->ishand[(int) $socket]);
$userData = $this->onlineUser[(int) $socket];
unset($this->onlineUser[(int) $socket]);
unset($this->clientRoom[$userData['room']][(int) $socket]);
unset($this->userRoom[$userData['room']][(int) $socket]);
$this->sendLogoutMessage($userData['nickname'], $userData['room']);
continue;
}
if (!$this->ishand[(int) $socket]) {
// echo 'hand socket:' . $socket . "\r\n";
//如果當前客戶端沒有握手,執行握手流程,建立連結
$this->dohandshake($socket, $content);
$this->ishand[(int) $socket] = true;
} else {
$this->message($content, $socket);
}
}
}
}
private function sendLogoutMessage($username, $room) {
$data = [
'msg_type' => 'logout',
'msg' => '【' . $username . '】離開了我們!',
'users' => $this->userRoom[$room]
];
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
$encodeData = $this->encode($json);
foreach ($this->clientRoom[$room] as $client) {
socket_write($client, $encodeData, strlen($encodeData));
}
}
/*
* 處理資料
*/
private function message($content, $client) {
$jsonData = $this->decode($content);
$dataArr = json_decode($jsonData, true);
if (!$dataArr) {
return null;
}
switch ($dataArr['act']) {
case '@': $this->privateMessage($dataArr, $client);
break;
default: $this->baseMessage($dataArr, $client);
break;
}
}
private function baseMessage($dataArr, $client) {
$room = $dataArr['room'];
if ($dataArr['act'] == 'login') {
$data = [
'msg_type' => 'login',
'msg' => '歡迎【' . $dataArr['nickname'] . '】登入!',
];
$this->onlineUser[(int) $client] = [
'nickname' => $dataArr['nickname'],
'login_time' => date('Y-m-d H:i:s'),
'client' => $client,
'room' => $room
];
//處理當前客戶端在哪個聊天室
$this->clientRoom[$room][(int) $client] = $client;
$this->userRoom[$room][(int) $client] = $dataArr['nickname'];
} else {
$data = [
'msg_type' => 'text',
'nickname' => $dataArr['nickname'],
'msg' => $dataArr['con']
];
}
$data['users'] = $this->userRoom[$room];
$encodeData = $this->encode(json_encode($data, JSON_UNESCAPED_UNICODE));
foreach ($this->clientRoom[$room] as $accept) {
socket_write($accept, $encodeData, strlen($encodeData));
}
return;
}
private function privateMessage($dataArr, $client) {
$room = $dataArr['room'];
//取出所有的客戶端列表,並且使用客戶端的暱稱作為下標
$user = array_column($this->onlineUser, 'client', 'nickname');
if($this->onlineUser[(int) $user[$dataArr['to']]]['room'] != $room){
return null;
}
//先組裝個數據
$data = [
'msg_type' => '@me',
'from' => $dataArr['from'],
'to' => $dataArr['to'],
'msg' => $dataArr['con']
];
//賦值一個線上使用者列表
$data['users'] = $this->userRoom[$room];
//將資料轉換為json
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
//將json轉換為資料幀
$encodeData = $this->encode($json);
//給接收人的客戶端傳送訊息
socket_write($user[$dataArr['to']], $encodeData, strlen($encodeData));
//給傳送人的客戶端傳送訊息
socket_write($client, $encodeData, strlen($encodeData));
return;
}
/*
* 響應客戶端握手頭
* @param $sock 客戶端資源
* @param $data 客戶端請求頭
*/
private function dohandshake($sock, $data) {
//擷取客戶端請求頭中Sec-WebSocket-Key的值
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) {
//將key值後面追加一個固定字串258EAFA5-E914-47DA-95CA-C5AB0DC85B11 然後sha1加密再轉base64編碼
$response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
//拼接一個握手頭 注意每一個引數後面都需要使用 \r\n 換行
$upgrade = "HTTP/1.1 101 Switching Protocol\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " . $response . "\r\n\r\n";
//將內容寫入客戶端套接字,客戶端將會接收到資料並驗證成功即可與伺服器建立連結
socket_write($sock, $upgrade, strlen($upgrade));
}
}
/**
* 解碼過程:可以解析客戶端傳送的資料幀為一個json
*/
private function decode($buffer) {
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;
if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return $decoded;
}
/**
* 編碼過程:可以將json資料轉換為客戶端可以解析的資料幀
*/
private function encode($buffer) {
$length = strlen($buffer);
if ($length <= 125) {
return "\x81" . chr($length) . $buffer;
} else if ($length <= 65535) {
return "\x81" . chr(126) . pack("n", $length) . $buffer;
} else {
return "\x81" . char(127) . pack("xxxxN", $length) . $buffer;
}
}
}
$socket = new WebSocket();
$socket->start();