一、前言
闲来无事,最近捣鼓了下websocket,但是不希望安装第三方类库,所以打算用socket基础函数创建个服务。
二、构建websocket服务端
<?php
class SocketService
{
// 默认的监听地址和端口
private $address = '0.0.0.0';
private $port = 8083;
private $_sockets;
/**
* 构造函数,初始化地址和端口
*
* @param string $address 监听的地址,默认 '0.0.0.0'
* @param int $port 监听的端口,默认 8083
*/
public function __construct($address = '', $port = '')
{
if (!empty($address)) {
$this->address = $address;
}
if (!empty($port)) {
$this->port = $port;
}
}
/**
* 初始化服务,创建套接字并开始监听
*/
public function service()
{
// 获取 TCP 协议号
$tcp = getprotobyname("tcp");
// 创建 TCP 套接字
$sock = socket_create(AF_INET, SOCK_STREAM, $tcp);
// 设置套接字选项,允许地址重用
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);
// 如果创建失败,抛出异常
if ($sock < 0) {
throw new Exception("failed to create socket: " . socket_strerror($sock) . "\n");
}
// 绑定地址和端口
socket_bind($sock, $this->address, $this->port);
// 开始监听
socket_listen($sock, $this->port);
echo "listen on $this->address $this->port ... \n";
// 保存套接字
$this->_sockets = $sock;
}
/**
* 运行 WebSocket 服务
*
* 该方法会进入一个无限循环,处理所有客户端连接
*/
public function run()
{
// 启动服务
$this->service();
// 存储客户端套接字
$clients[] = $this->_sockets;
// 无限循环监听客户端连接
while (true) {
$changes = $clients;
$write = NULL;
$except = NULL;
// 监听可读的套接字
socket_select($changes, $write, $except, NULL);
// 处理每个连接的套接字
foreach ($changes as $key => $_sock) {
// 判断是否是新连接
if ($this->_sockets == $_sock) {
// 接受新连接
if (($newClient = socket_accept($_sock)) === false) {
die('failed to accept socket: ' . socket_strerror($_sock) . "\n");
}
// 读取客户端发送的数据
$line = trim(socket_read($newClient, 1024));
// 执行 WebSocket 握手
$this->handshaking($newClient, $line);
// 获取客户端 IP
socket_getpeername($newClient, $ip);
// 将新连接的客户端保存
$clients[$ip] = $newClient;
// 输出客户端 IP 和消息
echo "Client ip:{$ip} \n";
echo "Client msg:{$line} \n";
} else {
// 处理已连接的客户端消息
socket_recv($_sock, $buffer, 2048, 0);
// 解码接收到的消息
$msg = $this->message($buffer);
// 在这里处理业务逻辑
echo "{$key} client msg: {$msg}\n";
// 等待用户输入响应
fwrite(STDOUT, 'Please input a argument:');
$response = trim(fgets(STDIN));
// 发送响应给客户端
$this->send($_sock, $response);
echo "{$key} response to Client: {$response}\n";
}
}
}
}
/**
* WebSocket 握手处理
*
* @param resource $newClient 新连接的客户端套接字
* @param string $line 接收到的握手请求头
* @return int 返回写入的字节数
*/
public function handshaking($newClient, $line)
{
$headers = array();
$lines = preg_split("/\r\n/", $line);
// 解析请求头
foreach ($lines as $line) {
$line = chop($line);
if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
$headers[$matches[1]] = $matches[2];
}
}
// 获取客户端的 Sec-WebSocket-Key
$secKey = $headers['Sec-WebSocket-Key'];
// 生成 Sec-WebSocket-Accept
$secAccept = 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" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: $this->address\r\n" .
"WebSocket-Location: ws://$this->address:$this->port/websocket/websocket\r\n" .
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
// 发送握手响应
return socket_write($newClient, $upgrade, strlen($upgrade));
}
/**
* 解析接收到的 WebSocket 消息
*
* @param string $buffer 接收到的 WebSocket 数据
* @return string 解码后的消息
*/
public 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;
}
/**
* 发送 WebSocket 消息给客户端
*
* @param resource $newClient 新连接的客户端套接字
* @param string $msg 要发送的消息
* @return int 返回写入的字节数
*/
public function send($newClient, $msg)
{
// 封装消息为 WebSocket 数据帧
$msg = $this->frame($msg);
// 发送数据帧
socket_write($newClient, $msg, strlen($msg));
}
/**
* 将消息封装为 WebSocket 数据帧
*
* @param string $s 要封装的消息
* @return string 封装后的 WebSocket 数据帧
*/
public 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;
}
/**
* 关闭 WebSocket 连接
*
* @return bool 返回是否成功关闭
*/
public function close()
{
return socket_close($this->_sockets);
}
}
// 创建并运行 WebSocket 服务
$sock = new SocketService();
$sock->run();
三、构建websocket客户端
接下来写个前端页面,测试服务端是否正常,代码如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>WebSocket</title>
</head>
<body>
<input id="text" value="">
<input type="submit" value="发送" onclick="start()">
<input type="submit" value="关闭" onclick="close()">
<div id="msg"></div>
<script>
/**
* WebSocket的连接状态代码:
* 0: 未连接
* 1: 已连接,可以通讯
* 2: 正在关闭
* 3: 已关闭或无法打开
*/
// 创建WebSocket实例
var webSocket = new WebSocket("ws://127.0.0.1:8083");
// 监听错误事件
webSocket.onerror = function (event) {
onError(event);
};
// 监听连接成功事件
webSocket.onopen = function (event) {
onOpen(event);
};
// 监听消息事件
webSocket.onmessage = function (event) {
onMessage(event);
};
// 监听关闭事件
webSocket.onclose = function (event) {
onClose(event);
};
// 错误处理函数
function onError(event) {
document.getElementById("msg").innerHTML = "<p>连接错误</p>";
console.log("错误: " + event.data);
}
// 连接成功后的回调函数
function onOpen(event) {
console.log("连接成功: " + sockState());
document.getElementById("msg").innerHTML = "<p>已连接到服务</p>";
}
// 处理接收到的消息
function onMessage(event) {
console.log("接收到消息");
document.getElementById("msg").innerHTML += "<p>响应: " + event.data + "</p>";
}
// 连接关闭后的回调函数
function onClose(event) {
document.getElementById("msg").innerHTML = "<p>连接已关闭</p>";
console.log("关闭连接: " + sockState());
webSocket.close();
}
// 获取WebSocket连接状态
function sockState() {
var status = ['未连接', '已连接,可以通讯', '正在关闭', '已关闭或无法打开'];
return status[webSocket.readyState];
}
// 发送消息函数
function start(event) {
console.log(webSocket);
var msg = document.getElementById('text').value;
document.getElementById('text').value = ''; // 清空输入框
console.log("发送消息: " + sockState());
console.log("消息内容: " + msg);
webSocket.send("msg=" + msg); // 发送消息
document.getElementById("msg").innerHTML += "<p>请求: " + msg + "</p>";
}
// 关闭连接
function close(event) {
webSocket.close();
}
</script>
</body>
</html>
四、测试结果
出现已连接到服务,代表成功连接。