一、前言
闲来无事,最近捣鼓了下websocket,但是不希望安装第三方类库,所以打算用socket基础函数创建个服务。
二、构建websocket服务端
<?phpclass 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);// 获取客户端 IPsocket_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>
四、测试结果
出现已连接到服务,代表成功连接。