PHP Swoole 实现 WebSocket 聊天室

PHP Swoole 实现 WebSocket 聊天室

随便写写 PHP 1933 字 / 4 分钟

实时的,在线的

一直以来我都在回避去做一些需要“实时”的项目。比如在线自动匹配、在线发送消息等。 究其原因还是没太接触过这方面的知识,并不知道用什么技术实现,总感觉会很麻烦。 但是最近学校有个微信小程序的项目,有用户实时聊天的需求。 抱着多学习一点东西的想法,我居然给接下来了! 刚开始是在百度上粗略地找了下聊天室的实现原理,发现了一个用 Java 写的在线 Web 聊天室。体验了一下,还不错,甚至还找出了几个 XSS 的洞。给作者提了 issue 后也是蛮开心的。这个项目使用 WebSocket 这个持久化的协议实现的。居然不是 HTTP 轮询! 我便天真的搜了下 PHP WebSocket,还真发现了一个叫 Swoole 的东西! 今天花了一上午的时间用 Swoole 做了一个十分简陋的聊天室 DEMO,效果居然还不错!

环境搭建

Swoole 是用 C / C++ 写的网络通信引擎。他以一个 PHP 扩展的形式整合进 PHP 中。Swoole 不仅可以用来写 WebSocket 服务器,还可以提供 TCP、UDP、HTTP 服务!同时,从官网的文档来看,调用 Swoole 十分的方便简单,感觉冗余的代码完全没有,十分的优雅。

我是在自己的腾讯云服务器上用 Docker 搭了一个 WebSocket Server。 首先,我们先把相关的 Docker 镜像拉下来。之所以使用镜像,是因为 Swoole 作为 PHP 的扩展,官网的安装方式是自己手动编译安装,这就有些麻烦了;不如用现成的镜像。

docker pull xlight/docker-php7-swoole

然后使用镜像创建一个容器:

docker run -d -t --name swoole -v /home/swoole:/home/swoole -p 5901:5901 xlight/docker-php7-swoole

这里我们是准备将 WebSocket 服务器开放在 5901 端口,因此需要先在腾讯云的控制台中配置安全组规则放出端口。

Server

这里我是写了一个十分简陋的 WebSocket 聊天室,毕竟只是个 DEMO 嘛。

$server = new Swoole\WebSocket\Server("0.0.0.0", 9501);
$server->start();	//开启服务器

首先创建一个$server变量,这将是我们的 WebSocket 服务器,设置它的 IP 为本地,端口为 9501。 后面是三个十分常用的侦听器(这么叫应该没问题吧):

客户端连接

$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
	echo "server: handshake success with fd{$request->fd}\n";
});

当有客户端连接是,将会被触发。此时会在命令行中输出客户端的 ID。

客户端发送消息

$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
	echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
});

当接收到客户端发向服务端的消息时会触发。fd为客户端的 ID,data为客户端发送的消息。

客户端断开连接

$server->on('close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});

当客户端断开连接时触发,fd为客户端 ID。

注意这里的客户端 ID 是不会被对应保存的,同一个客户端,第一次连上,ID 为 1;断开后再连上,ID 会向后顺延为 2。因此不能使用fd来作为客户端的唯一标识符。 我的想法是,在实际的场景中,使用诸如 Redis 这类的数据库,存储当前用户的在线情况与fd。若用户上线,则更新数据库中用户的状态并保存当次连接的fd。因为用户之间发送消息,是用户A发送消息到服务器,服务器再发送给用户B。 服务器是通过fd来确定发送的用户的,因此我们需要保存这个值。

服务端发送消息

$server->push($fd, "this is server");

这里$fd即为客户端的 ID,第二个参数是发送的信息。

之后保存为ws.php,在 Docker 容器中输入php ws.php即可运行。在实际使用中,我们期望这个 PHP 服务器能一直在后台一直不断运行,这就需要使用:

nohup php ws.php &

同时会生成一个nohup.out文件,里面是 PHP 的输出,我们可以将其当做日志文件使用。

Client

令我感到吃惊的是,客户端居然全部用 JavaScript 即可实现。(见识太短

var ws = new WebSocket("ws://233.233.233.233:9501");

首先是建立一个 WebSocket 连接。注意这里的协议是ws://

客户端接受消息

ws.onmessage= function(event){
	console.log(event.data);
}

发送消息到服务端

ws.send('Hello Server.');

断开连接

ws.onclose = function(event){
	console.log("连接已关闭");
};

完善一下出个 DEMO

现在,一个超级超级基本的 WebSocket 服务端 + 客户端就已经做好了。让我们再完善一下,做一个小的聊天室玩玩。 服务端代码:

<?php 
$users = [];	//存储用户名称与 fd 对于的数组

$server = new Swoole\WebSocket\Server("0.0.0.0", 9501);		//WebSocket 服务器

$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
	echo "server: handshake success with fd{$request->fd}\n";
});

//收到发送的信息
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
	global $users;

	echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
	
    $data = json_decode($frame->data, true);	//发送过来的数据是 JSON 格式的
	$users[$data['name']] = $frame->fd;		//将用户名与对于的 fd 保存到数组中

	switch($data['action']){		//判断发送过来的信息的类型
		case 'connect':		//初次连接的信息
			$server->push($frame->fd, 'Server Connected!!');		//发送消息给客户端,告诉已经连接上了
		break;
			
		case 'to':		//收到客户端发送的信息
			$server->push($users[$data['to']], $data['name'] . ' : ' . $data['data']);		//发送消息给对应的接收者
		break;
	}
});

$server->on('close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});

$server->start();		//开启服务器

客户端代码:

var ws = new WebSocket("ws://233.233.233.233:9501");
var isConnect = false;

function btnclick(){
	console.log(111)
	if(!isConnect){

		onConnect();
	}else{
		onSend();
	}
}

function onConnect(){
	var myName = document.getElementById('myName').value;

	var data = {
		'name': myName,
		'action':'connect',
		'data':'connect'
	};
	showmsg("初始化!!");
	isConnect = true;

	ws.send(JSON.stringify(data));
}

function onSend(){
	var myName = document.getElementById('myName').value;
	var toUser = document.getElementById('toUser').value;
	var myData = document.getElementById('sendData').value;

	var data = {
		'name': myName,
		'action':'to',
		'to':toUser,
		'data':myData
	};

	showmsg('我 : ' + myData);
	ws.send(JSON.stringify(data));
}

ws.onmessage= function(event){
	showmsg(event.data);
}

ws.onclose = function(event){
	showmsg("连接已关闭");
};

function showmsg(msg){
	var box = document.getElementById("msg").innerHTML;
	document.getElementById("msg").innerHTML = box + '<br><br>' + msg;
}

我这里是模仿之前看到的那个 Java 项目里的数据结构。与服务器的发送的消息全部用 JSON。 然后服务器解析一下。

就是这样

大概就是这样啦~今天又接触了一个新的东西。能逐渐开始写这些实时通信的东西,自己也是挺开心的。 最近正忙着学 Vue.js,因为学校有一个项目我写前端。从零开始学,踩了巨多坑,快要自闭了。 日后若想完成那个完整的聊天室,估计得用到 Redis,这也是要去学的。