swoole的socket实现聊天功能
前言
贵有恒,何必三更起五更睡;最无益,只怕一日暴十寒。
偶然打开CSDN,忽然发现距离上一篇文章整整过去了一年。感慨时间过去飞快。这一年忙忙碌碌,但又没什么变化。一周一篇文章的约定,不知从哪天的忙碌开始抛到脑后。
裸辞后,休息了一个月,最近开始整理知识点。突然想起来要记录。那就坚持吧。不知道能坚持几天,但有开始总是好的。
实现思路
- 实现socket服务器。能连接、接受消息并作出回应、中断退出。
- 能对所有连接到服务器的客户端广播信息。
- 使用昵称聊天。
- 一对一聊天。
实现简单echo服务器
服务端代码实现
class Chat
{
const HOST = '0.0.0.0'; //允许所有ip的访问
const PART = 9501; //端口号
private $server = null; //服务对象
private $connectList = [];
public function __construct(){
$this->server = new swoole_websocket_server(self::HOST, self::PART);
//监听连接事件
$this->server->on('open', [$this, 'onOpen']);
//监听接收消息事件
$this->server->on('message', [$this, 'onMessage']);
//监听关闭事件
$this->server->on('close', [$this, 'onClose']);
//开启服务
$this->server->start();
}
/**
* 连接成功回调函数
* @param $server
* @param $request
*/
public function onOpen($server, $request)
{
echo $request->fd . '已连接' . PHP_EOL;//输出终端
$this->connectList[] = $request->fd; // 存储连接上的客户端
}
/**
* 接收到信息的回调函数
* @param $server
* @param $frame
* @return bool
*/
public function onMessage($server, $frame)
{
$data = $frame->data;
// 群发
echo $frame->fd . '说:' . $data . PHP_EOL;//打印到我们终端
//将这个用户的信息存入集合
foreach ($this->connectLis as $fd) {// 遍历客户端的集合
$server->push($fd, json_encode(['no' => $frame->fd , 'msg' => $data]));
}
}
/**
* 断开连接回调函数
* @param $server
* @param $fd
*/
public function onClose($server, $fd)
{
echo $fd . '断开连接' . PHP_EOL;//输出到终端终端
$this->connectList = array_diff($this->connectList, [$fd]); //移除断开连接的客户端
}
}
$obj = new Chat();
终端运行socket服务。 php test.php;
客户端代码实现
<html>
<head>
<meta charset="utf-8">
<title>聊天室</title>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
</head>
<body>
<textarea class="log" style="width: 100%; height: 500px;">
=======聊天室======
</textarea>
用户名 <input type="text" name="user_name" id="user_name">
<input type="button" value="连接" id="link_btn" onClick="check()">
<input type="button" value="断开" onClick="dis()">
<input type="text" id="text">
<input type="button" value="发送" onClick="send()">
<script>
function link(user_name){
var url='ws://127.0.0.1:9501';
socket=new WebSocket(url);
socket.onopen=function(){
log1('连接成功');
// 设置用户名 信息类型为2 为用户名
// send2(2,user_name);
// 关闭连接入口
$('#link_btn').attr("disabled",true);
}
socket.onmessage=function(msg){log(msg.data);console.log(msg);}
socket.onclose=function(){log1('断开连接')}
}
function check() {
var user_name=$('#user_name').val();
if (user_name=='') {
log1('设置用户名后才能进入聊天室!');
return false;
}
link(user_name);
}
function dis(){
socket.close();
socket=null;
}
function log1(var1) {
$('.log').append(var1+'\r\n');
}
function log(var1){
var v=$.parseJSON(var1)
$('.log').append('用户'+v['no']+'说:'+v['msg']+'\r\n');
}
function send(){
var text=$('#text').val();
$('#text').val('');
socket.send(text);
}
function send2(type,text=''){
if (text=='') {
var msg=$('#text').val();
}else{
var msg = text;
}
$('#text').val('');
var json = JSON.stringify({'type':type,'msg':msg,'customer_id':'0'})
socket.send(json);
}
</script>
</body>
</html>
打开html文件,连接后发送消息。终端可以接收到数据。
到这里,简单的echo 服务器实现了。
但是注意看web聊天页面。用户2发送的消息,用户1接收不到。
这时候到服务端代码,打印$this->connectList
发现这个变量只存储了当前客户端的ID。
原因是swoole使用的多进程。 进程直接数据隔离。 不共享内存。 那如何解决这个问题呢。
实现服务器的广播功能
因为swoole是进程隔离的。那么如何在不同进程之间共享数据呢?
那就需要一个独立的存储介质。
- 文件
- 数据库 mysql等
- 缓存 redis等
这里我们使用的redis来作为介质。 相对于普通数据库/文件 redis为内存缓存,I/O等待的时间相对短一些。
不过如果单纯只实现广播功能,还没必要使用redis。swoole 提供了一个对象属性,里面存储了所有在线的客户端。 Server::$connections
所以调整后test.php代码如下。
class Chat
{
const HOST = '0.0.0.0'; //允许所有ip的访问
const PART = 9501; //端口号
private $server = null; //服务对象
public function __construct(){
$this->server = new swoole_websocket_server(self::HOST, self::PART);
//监听连接事件
$this->server->on('open', [$this, 'onOpen']);
//监听接收消息事件
$this->server->on('message', [$this, 'onMessage']);
//监听关闭事件
$this->server->on('close', [$this, 'onClose']);
//开启服务
$this->server->start();
}
/**
* 连接成功回调函数
* @param $server
* @param $request
*/
public function onOpen($server, $request)
{
echo $request->fd . '已连接' . PHP_EOL;//输出终端
}
/**
* 接收到信息的回调函数
* @param $server
* @param $frame
* @return bool
*/
public function onMessage($server, $frame)
{
$data = $frame->data;
// 群发
echo $frame->fd . '说:' . $data . PHP_EOL;//打印到我们终端
//将这个用户的信息存入集合
foreach ($server->connections as $fd) {// 遍历客户端的集合
$server->push($fd, json_encode(['no' => $frame->fd , 'msg' => $data]));
}
}
/**
* 断开连接回调函数
* @param $server
* @param $fd
*/
public function onClose($server, $fd)
{
echo $fd . '断开连接' . PHP_EOL;//输出到终端终端
}
}
$obj = new Chat();
重新启动服务器,这时候,发送消息。不同客户端直接均能正常接收数据。
实现昵称和一对一聊天
功能比较简单,我就写一起了。拆分要造数据比较费劲。
前面说到,变量的问题。 所以,上面说的redis,这里就可以直接引入了。
服务端代码
class Chat
{
const HOST = '0.0.0.0'; //允许所有ip的访问
const PART = 9501; //端口号
private $server = null; //服务对象
private $redis = null; //redis 对象
public function __construct(){
//实例化swoole_websocket_server并存储在我们Chat类中的属性上,达到单例的设计
$this->server = new swoole_websocket_server(self::HOST, self::PART);
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
//监听连接事件
$this->server->on('open', [$this, 'onOpen']);
//监听接收消息事件
$this->server->on('message', [$this, 'onMessage']);
//监听关闭事件
$this->server->on('close', [$this, 'onClose']);
//开启服务
$this->server->start();
}
/**
* 连接成功回调函数
* @param $server
* @param $request
*/
public function onOpen($server, $request)
{
echo $request->fd . '已连接' . PHP_EOL;//输出终端
}
/**
* 接收到信息的回调函数
* @param $server
* @param $frame
* @return bool
*/
public function onMessage($server, $frame)
{
$data = json_decode($frame->data,true);
$msg = $data['msg'];
// 设置用户名逻辑 简单处理
if($data['type']==2){
$name = $msg;
$this->redis->hset('fdInfo',$frame->fd,$name);
return true;
}
$userName = $this->redis->hget('fdInfo',$frame->fd);
if ($data['customer_id']>0){
// 单用户对话
echo $frame->fd . '对'.$data['customer_id'].'说:' . $msg . PHP_EOL;//打印到我们终端
$tmpGrop = [$frame->fd,$data['customer_id']];
foreach ($tmpGrop as $fd){
$server->push($fd, json_encode(['no' => $frame->fd,'userName'=>$userName , 'msg' => $msg]));
}
}else{
// 群发
echo $frame->fd . '说:' . $msg . PHP_EOL;//打印到我们终端
//将这个用户的信息存入集合
foreach ($server->connections as $fd) {//遍历客户端的集合,拿到每个在线的客户端id
//将客户端发来的消息,推送给所有用户,也可以叫广播给所有在线客户端
$server->push($fd, json_encode(['no' => $frame->fd,'userName'=>$userName , 'msg' => $msg]));
}
}
}
/**
* 断开连接回调函数
* @param $server
* @param $fd
*/
public function onClose($server, $fd)
{
echo $fd . '断开连接' . PHP_EOL;//输出到终端终端
}
}
$obj = new Chat();
这里引入了redis。使用哈希数据类型, 针对不同客户端分开存储。
改动较大的 onMessage
回调函数。注意是接收的数据类型做了改动。 针对不同进来的类型进行了处理。同事支持一对一聊天。 只要传入聊天对象的customer即可(对于前端对话框,可能还需要再返回一个字段类型,标识是否是私密聊天。 )。
客户端代码
<html>
<head>
<meta charset="utf-8">
<title>聊天室</title>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
</head>
<body>
<textarea class="log" style="width: 100%; height: 500px;">
=======聊天室======
</textarea>
用户名 <input type="text" name="user_name" id="user_name">
<input type="button" value="连接" id="link_btn" onClick="check()">
<input type="button" value="断开" onClick="dis()">
<input type="text" id="text">
<input type="button" value="发送" onClick="send2(1)">
<script>
function link(user_name){
var url='ws://127.0.0.1:9501';
socket=new WebSocket(url);
socket.onopen=function(){
log1('连接成功');
// 设置用户名 信息类型为2 为用户名
send2(2,user_name);
// 关闭连接入口
$('#link_btn').attr("disabled",true);
}
socket.onmessage=function(msg){log(msg.data);console.log(msg);}
socket.onclose=function(){log1('断开连接')}
}
function check() {
var user_name=$('#user_name').val();
if (user_name=='') {
log1('设置用户名后才能进入聊天室!');
return false;
}
link(user_name);
}
function dis(){
socket.close();
socket=null;
}
function log1(var1) {
$('.log').append(var1+'\r\n');
}
function log(var1){
var v=$.parseJSON(var1)
$('.log').append('用户'+v['no']+v['userName']+'说:'+v['msg']+'\r\n');
}
function send(){
var text=$('#text').val();
$('#text').val('');
socket.send(text);
}
function send2(type,text=''){
if (text=='') {
var msg=$('#text').val();
}else{
var msg = text;
}
$('#text').val('');
var json = JSON.stringify({'type':type,'msg':msg,'customer_id':'1'})
socket.send(json);
}
</script>
</body>
</html>
客户端再方法send2()
中做了处理,写死了与用户1的对话。
效果如下(只截了终端图,聊天界面可以自己尝试,页面写得简单,需要手动改一对一对话的用户ID)。
结尾
相对来说,swoole来实现聊天功能还是很简单的。 核心功能已经封装好了,只需要实现对应的方法就可以了。