WebSocket 服务器简单来说就是一个遵循特殊协议监听服务器任意端口的tcp应用,它用来监听客户端发送过来的请求,需要常驻内存执行。
首先,我们需要建立一个socket的连接 , 用来监听客户端发送的请求,需要设置监听的ip地址和端口
<?php$address="127.0.0.1"; // ip地址$port=8000; // 自定义端口$socket=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);socket_bind($socket,$address,$port);socket_listen($socket);
当客户端第一次向服务端发出请求时,请求头信息如下
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://127.0.0.1:8000
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
其中upgrade websocket用于告诉服务器此连接需要升级到websocket。
而下面的Sec-WebSocket-Key是客户端也就是浏览器或者其他终端随机生成一组16位的随机base64编码
最后Sec-WebSocket-Version就是当前使用协议的版本号了。
服务器在接受到上面的请求之后,需要返回一个response头包完成握手,由Sec-Websocket-Accept的key完成校验,具体算法如下。
<?php
/**
* 握手函数
* @param $client resource socket连接
* @param $content string 客户端发来的头信息
*/
function handshaking($client,$content)
{
// 定义头部信息
$headers=[];
if(preg_match('/Sec-WebSocket-Key:.*\r\n/',$content,$matchs))
{
$headers['Sec-WebSocket-Key'] =trim(chop(str_replace('Sec-WebSocket-Key:',"",$matchs[0])));
}
// 设置返回头
$secKey = $headers['Sec-WebSocket-Key'];
$websocket_accept=base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Sec-WebSocket-Version: 13\r\n".
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept:$websocket_accept\r\n\r\n";
// 返回客户端
socket_write($client,$upgrade,strlen($upgrade));
}
连接成功显示状态码为 101
此时前端在向服务端发送请求时,服务端需要监听前端发送的消息,如下
<?php
public function run()
{
// 定义写入监听连接池
$write=null;
// 定义权限接受连接池
$except=null;
// 定义超时时间
$time_out=null;
// 启动循环阻塞任务
while(true)
{
// 复制连接池
$changes=array_column($this->sockets, 'cli');
// 设置阻塞监听函数 将连接阻塞在此函数 直到有消息发送才疏通
socket_select($changes,$write,$except,$time_out);
// 监听端口可读后操作
foreach ($changes as $sock) {
// 如果监听到的是原端口
if($sock==$this->socket)
{
// 服务端取请求客户端
$client = socket_accept($this->socket);
// 客户端存入客户端数组
$this->sockets[(int)$client]=[
'cli'=>$client,
'hand'=>false // 初始握手为否
];
continue;
}else{
// 接收客户端的请求
$bytes = @socket_recv($sock, $buffer, 2048, 0);
// 一旦客户端请求接到为false 代表用户下线
if(!$bytes){
unset($this->sockets[(int)$sock]); // 删除用户池中用户
continue;
}
if(!$this->sockets[(int)$sock]['hand']){
// 握手
$this->handshaking($sock,$buffer);
// 服务端提示
echo "客户端已加入连接池\n";
}else{
echo $this->message($buffer)."\n";
$this->send($this->sockets[(int)$sock]['cli'],"给孩子的");
}
}
}
}
}
客户端每次在发送消息时,固定回复,给孩子的
总结代码如下 :
<?php
class webSocket
{
private $socket; // 服务端
private $sockets=[]; // 全部客户端+服务端
public function __construct($address='127.0.0.1',$port=8000)
{
$socket=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($socket,$address,$port);
socket_listen($socket);
$this->socket=$socket;
$this->sockets[0]=['cli'=>$socket]; // 0 为服务端
echo "已启动websockt \n服务器地址:".$address."\n端口:".$port."\n";
}
public function run()
{
// 定义写入监听连接池
$write=null;
// 定义权限接受连接池
$except=null;
// 定义超时时间
$time_out=null;
// 启动循环阻塞任务
while(true)
{
// 复制连接池
$changes=array_column($this->sockets, 'cli');
// 设置阻塞监听函数 将连接阻塞在此函数 直到有消息发送才疏通
socket_select($changes,$write,$except,$time_out);
// 监听端口可读后操作
foreach ($changes as $sock) {
// 如果监听到的是原端口
if($sock==$this->socket)
{
// 服务端取请求客户端
$client = socket_accept($this->socket);
// 客户端存入客户端数组
$this->sockets[(int)$client]=[
'cli'=>$client,
'hand'=>false // 初始握手为否
];
continue;
}else{
// 接收客户端的请求
$bytes = @socket_recv($sock, $buffer, 2048, 0);
// 一旦客户端请求接到为false 代表用户下线
if(!$bytes){
unset($this->sockets[(int)$sock]); // 删除用户池中用户
continue;
}
if(!$this->sockets[(int)$sock]['hand']){
// 握手
$this->handshaking($sock,$buffer);
// 服务端提示
echo "客户端已加入连接池\n";
}else{
echo $this->message($buffer)."\n";
$this->send($this->sockets[(int)$sock]['cli'],"给孩子的");
}
}
}
}
}
/**
* 握手函数
* @param $client resource socket连接
* @param $content string 客户端发来的头信息
*/
function handshaking($client,$content)
{
// 定义头部信息
$headers=[];
if(preg_match('/Sec-WebSocket-Key:.*\r\n/',$content,$matchs))
{
$headers['Sec-WebSocket-Key'] =trim(chop(str_replace('Sec-WebSocket-Key:',"",$matchs[0])));
}
// 设置返回头
$secKey = $headers['Sec-WebSocket-Key'];
$websocket_accept=base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Sec-WebSocket-Version: 13\r\n".
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept:$websocket_accept\r\n\r\n";
// 写入缓冲
socket_write($client,$upgrade,strlen($upgrade));
$this->sockets[(int)$client]['hand']=true;
}
/**
* 解析函数
* @param $buffer string 客户端发送的原数据
* @return null|string 解析后的数据
*/
function message($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;
}
/**
* 处理帧数据
* @param $s string 要发送的数据
* @return string 客户端需要的数据
*/
function frame($s)
{
$a = str_split($s, 125);
if (count($a) == 1)
{
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o)
{
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}
/**
* 发送数据
* @param $clinet resource socket资源
* @param $msg string 发送的消息
*/
function send($clinet, $msg){
$msg = $this->frame($msg);
socket_write($clinet, $msg, strlen($msg));
}
}
$socketobj=new webSocket();
$socketobj->run(); // 开始监听
先将PHP设置为环境变量,再使用cmd执行刚写好的PHP脚本即可实现监听
