<?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();