PHP网络编程Socket-TCP长连接组播、单播、广播的实现与应用

1.概念介绍

声明:我们这里说的概念是在socket层使用TCP,用业务逻辑实现了这些功能。非UDP中的组播、广播的概念。

单播:网络通信中,一台客户机通过服务器与另一台客户机实现一对一的全双工通讯。

组播:网络通信中,一台客户机通过服务器与一组客户机实现一对多的全双工通讯。

广播:网络通信中,一台客户机通过服务器与所有客户机实现一对全部的全双工通讯。

2.实现前的知识储备

1.了解面向有连接的TCP长连接与socket客户机与服务器通讯的流程。

2.了解多进程/多线程的使用

3.了解事件机制与epoll模型

3.PHP中的实现思路

3.1服务端的实现

1.首先通讯的前提就是建立长连接,当客户机连接成功后,不能关闭客户机的连接(心跳检测不在本文的讨论范围,所以没有实现)

2.存储客户机连接的socket资源,标识客户机的状态信息

3.单播、组播、广播就是通过遍历标识的客户机状态信息,按需发送通讯消息

3.2客户端的实现

1.通讯方式长连接,连接服务端成功后不要断开与服务端的连接 (心跳检测不在本文的讨论范围,所以没有实现)

2.准备两个进程,1个进程用于显示服务端发送过来的消息,1个进程用于发送服务端来自用户输入的消息

4.实现效果

4.1实现的逻辑:

1.当新客户机连接成功后,提示新客户机输入昵称和分组id用于后续的通讯使用

2.当新的客户机输入完有效的昵称和分组id后,已连接的客户机会收到新客户机的连接提示

3.单播:通过@nickname实现1对1的私聊

4.组播:通过!group_id实现1对多的群聊

5.广播:普通的聊天会发送给所有的已连接的客户机

6.退出提示:当某一个客户机断开连接时,会提示所有客户机该用户已下线

4.2客户机的登录

客户机登录效果图

4.3客户机单播

go 正在私聊php

4.4客户机组播

python 群发给了group_id为1的所有客户机

4.5客户机广播

go普通发言就是广播的实现

5.代码

5.1 客户端的实现

<?php

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if (!$socket) {
    printf('创建失败,原因:%s', socket_strerror(socket_last_error($socket)));
}

while(socket_connect($socket, '127.0.0.1', 8080)) {
    while($recv = socket_read($socket, 1024)) {
        $pid = pcntl_fork();
        if ($pid === -1) {
            exit;
        } else if ($pid) {
            $child = $pid;
            echo $recv . PHP_EOL;
        } else {
            while($msg = fgets(STDIN)) {
                socket_write($socket, $msg, strlen($msg));
            }
        }
    }
}

pcntl_wait($status);

function formatMsg($msg) {
    return sprintf("客户端发送时间:%s\t%s ", date('Y-m-d H:i:s', time()), $msg);
}

5.2服务端的实现

<?php

function formatMsg($msg)
{
    return sprintf("服务端发送时间:%s  %s ", date('Y-m-d H:i:s', time()), $msg);
}

$clients = [];

$context = stream_context_create([
    'socket' => [
        'backlog' => 10000,
    ]]);

stream_context_set_option($context, 'socket', 'so_reuseaddr', 1);

$socket = stream_socket_server(
    'tcp://0.0.0.0:8080',
    $errno,
    $errstr,
    STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
    $context);

stream_set_blocking($socket, false);

$base = new EventBase();
$event = new Event($base,
    $socket,
    Event::PERSIST | Event::READ | Event::WRITE,
    function ($socket) use (&$clients, &$base) {
        $client = stream_socket_accept($socket);
        $clients[(int)$client]['nickname'] = '匿名';
        $clients[(int)$client]['client_socket'] = $client;
        $clients[(int)$client]['group_id'] = null;
        $msg = formatMsg('欢迎连接服务,请输入[昵称]-[group_id]');
        fwrite($client, $msg, strlen($msg));
        $event = new Event($base, $client, Event::PERSIST | Event::READ, function () {
        });
        $event->set($base,
            $client,
            Event::PERSIST | Event::READ,
            function ($client, $what, $event) use (&$clients) {
                set_error_handler(function () {
                });
                $msg = fread($client, 1024);
                if (empty($msg)) {
                    if (feof($client) || !is_resource($client)) {
                        fclose($client);
                        $event->del();
                    }
                }
                echo $msg . PHP_EOL;
                if ($clients[(int)$client]['nickname'] === '匿名' && is_null($client[(int)$client]['group_id'])) {
                    $welcome = "请输入正确格式的[昵称]-[group_id]";
                    if (preg_match('/(\w+)-(\d+)/', $msg, $matches)) {
                        $nickname = $matches[1];
                        $group_id = $matches[2];
                        $clients[(int)$client]['nickname'] = $nickname;
                        $clients[(int)$client]['group_id'] = $group_id;
                        $welcome = formatMsg("欢迎进入World频道,您所在的分组为" . $group_id);
                        fwrite($clients[(int)$client]['client_socket'], $welcome, strlen($welcome));
                        $notify_join = formatMsg($nickname . "加入World频道,大家欢迎!");
                        foreach ($clients as $client_id => $allClients) {
                            if (!in_array($allClients['nickname'], [$nickname, '匿名'])) {
                                fwrite($allClients['client_socket'], $notify_join, strlen($notify_join));
                            }
                        }
                    }
                    return;
                }

                $template_msg = '来自玩家:' . $clients[(int)$client]['nickname'] . '的消息:{msg}';

                $_msg = formatMsg(str_replace('{msg}', $msg, $template_msg));

                foreach ($clients as $client_id => $allClients) {
                    set_error_handler(function () {
                    });
                    if (!is_resource($allClients['client_socket']) || feof($allClients['client_socket'])) {
                        $_msg = formatMsg('玩家:' . $allClients['nickname'] . '退出了World频道');
                        fclose($allClients['client_socket']);
                        $event->del();
                    }
                    //单播 单发
                    if (preg_match('/^@(\w+)\s+(\w+)/', $msg, $matches)) {
                        $nickname = $matches[1];
                        $_msg = formatMsg(str_replace('{msg}', $matches[2], $template_msg));
                        if ($allClients['nickname'] === $nickname) {
                            return fwrite($allClients['client_socket'], $_msg, strlen($_msg));
                        } else {
                            continue;
                        }
                    }
                    //组播 群发
                    if (preg_match('/^!(\w+)\s+(\w+)/', $msg, $matches)) {
                        $group_id = $matches[1];
                        $_msg = formatMsg(str_replace('{msg}', $matches[2], $template_msg));
                        if ($allClients['group_id'] === $group_id) {
                            fwrite($allClients['client_socket'], $_msg, strlen($_msg));
                        }
                        continue;
                    }
                    //广播 全发
                    fwrite($allClients['client_socket'], $_msg, strlen($_msg));
                }
                restore_error_handler();
            }, $event);
        $event->add();
    });
$event->add();
$base->loop();
如无特殊说明,文章均为本站原创,转载请注明出处。如发现有什么不对的地方,希望得到您的指点。

发表评论

电子邮件地址不会被公开。 必填项已用*标注