swoole的socket实现聊天功能

前言

贵有恒,何必三更起五更睡;最无益,只怕一日暴十寒。
偶然打开CSDN,忽然发现距离上一篇文章整整过去了一年。感慨时间过去飞快。这一年忙忙碌碌,但又没什么变化。一周一篇文章的约定,不知从哪天的忙碌开始抛到脑后。
裸辞后,休息了一个月,最近开始整理知识点。突然想起来要记录。那就坚持吧。不知道能坚持几天,但有开始总是好的。

实现思路

  1. 实现socket服务器。能连接、接受消息并作出回应、中断退出。
  2. 能对所有连接到服务器的客户端广播信息。
  3. 使用昵称聊天。
  4. 一对一聊天。

实现简单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文件,连接后发送消息。终端可以接收到数据。
swoole的socket实现聊天功能
到这里,简单的echo 服务器实现了。
但是注意看web聊天页面。用户2发送的消息,用户1接收不到。

这时候到服务端代码,打印$this->connectList发现这个变量只存储了当前客户端的ID。
原因是swoole使用的多进程。 进程直接数据隔离。 不共享内存。 那如何解决这个问题呢。

实现服务器的广播功能

因为swoole是进程隔离的。那么如何在不同进程之间共享数据呢?
那就需要一个独立的存储介质。

  1. 文件
  2. 数据库 mysql等
  3. 缓存 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的socket实现聊天功能

结尾

相对来说,swoole来实现聊天功能还是很简单的。 核心功能已经封装好了,只需要实现对应的方法就可以了。