十九、Swoole 基础学习笔记 - Swoole WebSocket 常见问题

作者: 温新

分类: 【Swoole 系列】

阅读: 1756

时间: 2023-03-13 11:36:19

hi,我是温新,一名PHPer

文章基于 Swoole 5.0.1 版本编写。

学习目标:了解 websocket 存在哪些问题及如何解决

说明:本篇文章结合官方文档编写及参考网络资料编写,虽非全部原创,但也是结合了自己的理解,若转载请附带本文 URL,编写不易,持续编写更不易,谢谢!

WebSocket 服务启动后就万事大吉了吗?答案当然是否定的,WebSocket 有如下几个问题需要了解:

  • 1、心跳检测的必要性;
  • 2、校验客户端连接的有效性;
  • 3、客户端的重连机制。

心跳检测

心跳检测,是不是很熟悉,没错,在之前的文章中进行过学习。关于心跳检测本篇文章不再啰嗦,详情可以参考 11、Swoole 基础学习笔记 - Swoole 心跳检测。

// 心跳配置
$server->set([
    'heartbeat_check_interval' => 10,
    'heartbeat_idle_time'	   => 60,
]);

校验客户端连接的有效性

new Swoole\WebSocket\Server('0.0.0.0', 9501); 它有问题吗?如果仅仅只在本地使用,当然没有问题。若是在生产环境,那问题就来了,0.0.0.0 意味着所有客户端都可以连接到我们的服务,这包含了有效客户端也包含了带有恶意的连接,对于这种结果,显示不是我们所需要的。

如何规避这种问题的?有很多种方法,如:

  • 连接的时候传递校验参数,vaild=1;
  • 只允许登录的用户进行连接;
  • token 校验。客户端每次发送消息时携带 token,服务端对 token 进行验证。

服务端代码

<?php
// 16-swoole-websocket-3.php
class WebSocketServer
{
	private $server = null;
	public $key = 'ziruchu';

	public function __construct()
	{
		$this->server = new Swoole\WebSocket\Server('0.0.0.0', 9501);

		// 设置心跳检测
		$this->server->set([
			'heartbeat_idle_time'      => 60,
			'heartbeat_check_interval' => 10,
		]);

		$this->server->on('Open', [$this, 'onOpen']);
		$this->server->on('Message', [$this, 'onMessage']);
		$this->server->on('Close', [$this, 'onClose']);
	}

	public function onOpen($server, $request)
	{
		$this->checkAccess($server, $request);
	}

	public function onMessage($server, $frame)
	{
		$server->push($frame->fd, '服务器返回数据:' . $frame->data);
	}

	public function onClose($server, $fd)
	{
		echo '客户端 ' . $fd . '已断开连接' . PHP_EOL;
	}

	public function start()
	{
		$this->server->start();
	}

	/**
	 * 验证客户端连接
	 * 
	 * @param  [type] $server  [description]
	 * @param  [type] $request [description]
	 * @return [type]          [description]
	 */
	public function checkAccess($server, $request)
	{
		// 若某个参数不存在,则关闭当前连接
		if (!isset($request->get) || !isset($request->get['id']) || !isset($request->get['token'])) {
			$server->close($request->fd);
			return false;
		}

		$id    = $request->get['id'];
		$token = $request->get['token'];

		
		// 校验 token
		if (md5(md5($id) . $this->key) != $token) {
			$server->close($request->fd);
			return false;
		}

	}
}

$server = new WebSocketServer();
$server->start();

客户端代码

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>简版聊天室</title>
</head>
<body>
	<div>
		<textarea name="content" id="content" cols="30" rows="10"></textarea>
		<button onclick="send();">发送</button>
	</div>
	<div>
		<ul id="messageList"></ul>
	</div>
    
	<script>
		let url = "ws://127.0.0.1:9501?id=10&token=27ebb6344564f1427f0f1f8a6d7c5a9c";
		let ws = new WebSocket(url);
		ws.onopen = function () {
			ws.send('my name is WebSocket');
		}

		ws.onmessage = function (event) {
			let data     = event.data;
			let ul       = document.getElementById('messageList');
			let li       = document.createElement('li');
			li.innerHTML = data;
			ul.appendChild(li);
		}

		ws.onclose = function (event) {
			console.log('客户端关闭连接:' . event);
		}

		function send() {
			let obj     = document.getElementById('content');
			let content = obj.value;
			ws.send(content)
			obj.value   = '';
		}
	</script>
</body>
</html>

客户端重连机制

客户端重连机制可以理解为一种保活机制,也可以和服务端的心跳检测在一起理解为双向心跳。即如何保证客户端与服务端的连接一直有效不断开。

对于客户端来说,只要触发了 error 或 close 或连接失败时,就主动重新连接服务端。

对于这个案例,服务端代码使用:16-swoole-websocket-2.php。

客户端代码如下;

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>重连机制</title>
</head>
<body>
	<div>
		<textarea name="content" id="content" cols="30" rows="10"></textarea>
		<button onclick="send();">发送</button>
	</div>
	<div>
		<ul id="messageList"></ul>
	</div>


	<script>
	
		// websocket 实例
		let ws;
		// 避免重复连接
		let lockReconnect = false;
		let url = "ws://127.0.0.1:9501";


		// 创建 websocket 实例
		function createWebSocket(url)
		{
			try {
				ws = new WebSocket(url);
				initEventHandle();
			} catch (e) {
				// 连接失败,重新连接
				reconnect(url);
			}
		}


		function initEventHandle()
		{
			ws.onclose = function () {
				reconnect(url);
			}
			ws.onerror = function () {
				reconnect(url);
			}
			ws.onopen = function () {
				// 心跳检测重置
				heartCheck.reset().start();
			}
			ws.onmessage = function (event) {
				let data     = event.data;
				let ul       = document.getElementById('messageList');
				let li       = document.createElement('li');
				li.innerHTML = data;
				ul.appendChild(li);

				heartCheck.reset().start();
			}
		}

		function reconnect(url)
		{
			if (lockReconnect) {
				return;
			}

			lockReconnect = true;

			setTimeout(function () {
				createWebSocket(url);
				lockReconnect = false;
			}, 2000);
		}


		let heartCheck = {
			timeout: 60000,
			timeoutObj: null,
			serverTimeoutObj: null,
			reset: function () {
				clearTimeout(this.timeoutObj);
				clearTimeout(this.serverTimeoutObj);
				return this;
			},
			start: function () {
				let self = this;
				this.timeoutObj = setTimeout(function () {
					// 发送心跳检测
					// 服务端收到后会返回一个心跳消息
					ws.send('');
					self.serverTimeoutObj = setTimeout(function () {
						ws.close();
					}, self.timeout);
				}, this.timeout);
			}

		};

		createWebSocket(url);

		function send() {
			let obj     = document.getElementById('content');
			let content = obj.value;
			ws.send(content)
			obj.value   = '';
		}

	</script>
</body>
</html>

本篇文章学习了 websocket 常见问题,我们下篇文章见。

请登录后再评论