V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
awanganddong
V2EX  ›  PHP

用 PHP 实现聊天室出现问题,请教下问题所在。

  •  
  •   awanganddong · 2019-10-11 16:56:31 +08:00 · 4259 次点击
    这是一个创建于 1650 天前的主题,其中的信息可能已经有所发展或是发生改变。

    问题描述:

         1.用 telnet  127.0.0.1  9999 首次访问,则显示第一个用户访问
         2.依次打开另外界面进行,则不显示内容
         3.但是如果我在第一个 telnet 输入内容 这时候才会出现第二个用户访问
         4.发送消息,也是如此
         举例:比如 A 发消息,B、C、D 可以收到。但是实际上呢,A 发消息后,B、C、D 必须也发一条消息后,才能收到其他人的消息。
    
            $host = "0.0.0.0";
            $port = "9999";
            $fd = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
            socket_bind($fd, $host, $port);
            socket_listen($fd);
            $conn = [];
            $writeFds = [];
            $e = null;
            echo PHP_EOL . PHP_EOL . "欢迎来到 select 聊天室" . PHP_EOL . PHP_EOL;
            echo "           tcp://{$host}:{$port}" . PHP_EOL;
            echo $fd;
            while (true) {
                $readFds = array_merge($conn, [$fd]);
                if (socket_select($readFds, $writeFds, $e, null) > 0) {
                    if (in_array($fd, $readFds)) {
                        $newConn = socket_accept($fd);
                        $i = intval($newConn);
                        $conn[$i] = $newConn;
                        $readFds[$i] = $newConn;
                        $writeFds[$i] = $newConn;
                        $key = array_search($fd, $readFds);
                        unset($readFds[$key]);
                        echo "第" . $i . "个用户来到聊天室" . PHP_EOL;
                    }
                    if (count($readFds) > 0) {
                        foreach ($readFds as $rfd) {
                            $line = socket_read($rfd, 2048);
                            foreach ($conn as $value) {
                                if ($rfd == $value) {
                                    continue;
                                }
                                socket_write($value, $line, strlen($line));
                            }
                        }
    
                    }
    
                } else {
                    continue;
                }
            }
    
    17 条回复    2019-10-12 11:43:20 +08:00
    lllllliu
        1
    lllllliu  
       2019-10-11 17:15:11 +08:00
    我没记错的话,一次只能处理一个 Client 吧。要 fork 子进程来处理通信吧。
    有条件的话直接用现成的库吧。
    awanganddong
        2
    awanganddong  
    OP
       2019-10-11 17:21:04 +08:00
    专门为了研究 socket 写的 demo。不是为了用在线上的。
    haiyang416
        3
    haiyang416  
       2019-10-11 18:01:09 +08:00
    注释掉这行:$readFds[$i] = $newConn;
    你刚 accept 就把它加入到 $readFds 数组,这时它还没有数据读取,接着马上在来一个阻塞的 socket_read,
    你的程序会一直阻塞到这个连接有数据发送才会执行后面的代码。
    并且每个新连接过来都会出现这个情况。
    awanganddong
        4
    awanganddong  
    OP
       2019-10-11 18:25:46 +08:00
    @haiyang416

    你的意思是说,我将套接字放入$readFds 数组中,这时候会执行 count($readFds)>0 里边的逻辑( A ),这时候对进程来说是阻塞的,同时如果有新的连接进入,就必须等 A 处理结束,才可以进行。?
    liqihang
        5
    liqihang  
       2019-10-11 18:27:40 +08:00
    @lllllliu 如果用 IO 多路复用的话,一个进程也能服务多个 client,看上去应该是 @haiyang416 说的阻塞问题
    wo642436249
        6
    wo642436249  
       2019-10-11 18:37:52 +08:00 via Android
    浪费时间,浪费生命,直接上 swoole 吧
    awanganddong
        7
    awanganddong  
    OP
       2019-10-11 18:39:37 +08:00
    就是 @haiyang416 说的问题

    我大概想了下

    其实是分两种情况的

    1.客户端首次连接,然后执行代码块( in_array($fd,$readFds))
    2.客户端再次连接,然后执行代码块( count($readFds) > 0 )
    awanganddong
        8
    awanganddong  
    OP
       2019-10-11 18:46:25 +08:00
    但是现在又出现让我困惑的问题
    telnet 连接后,发送消息,代码是从那个位置开始走的。
    按照实际是从 socket_select 这里开始走的
    haiyang416
        9
    haiyang416  
       2019-10-11 18:50:58 +08:00
    @awanganddong 跟情况无关,你的理解有问题。在 socket_select 之后 $readFds 里都是可以用于读取的“句柄”,它已经被 socket_select 函数修改了,这时你不应该自己往这个数组里加入新的数据,除非你可以确定它是有数据可读的。你只需要把新的连接加入到 $conn 数组,等待 while 循环再次调用 socket_select 即可。
    awanganddong
        10
    awanganddong  
    OP
       2019-10-11 21:19:12 +08:00
    @haiyang416 你能帮我解释下下边这个情况吗。

    就是 telnet 初次连接的时候,打印 socket_select 下$readFds 里边为服务器的 fd 与客户端 fd,
    然后 telnet 发送消息,就只剩下客服端的 fd。


    socket_select 处理第一次连接和发送消息有什么不同呢。不理解
    haiyang416
        11
    haiyang416  
       2019-10-11 23:21:03 +08:00   ❤️ 1
    @awanganddong
    $readFds 里面装的就是所有需要监听的描述符,可能有服务器的 fd,也可能有客户端连接的 fd,在经过 socket_select 之后,该函数会删除 $readFds 里暂时不可读的 fd。

    $readFds = [$fd];
    当前有新连接
    socket_accept 之后增加了 $conn1,再次进入 while 循环。
    --------------------------------------------------------
    $readFds = [$fd, $conn1];
    当前又有新连接
    socket_accept 之后增加了 $conn2,再次进入 while 循环。
    --------------------------------------------------------
    $readFds = [$fd, $conn1, $conn2];
    比如当前没有新连接,$conn1 收到了消息,$conn2 没有收到消息,
    那么 socket_select 函数就会把 $fd 和 $conn2 从数组中删除,即 $readFds = [$conn1];
    处理完 $conn1 后会再次进入 while 循环。
    --------------------------------------------------------
    $readFds = [$fd, $conn1, $conn2];
    当前又有新连接,$conn1 和 $conn2 没有收到消息
    那么 socket_select 函数就会把 $conn1 和 $conn2 从数组中删除,即 $readFds = [$fd];
    socket_accept 之后增加了 $conn3,再次进入 while 循环。
    --------------------------------------------------------
    $readFds = [$fd, $conn1, $conn2, $conn3];
    Seanfuck
        12
    Seanfuck  
       2019-10-11 23:36:28 +08:00 via iPhone
    读和写要分开,可读不一定能写的,写要单独 foreach 那个 writefds。读写的数据要暂存一下,可写时才写出去
    simonlu9
        13
    simonlu9  
       2019-10-12 00:14:52 +08:00
    @haiyang416 他的代码有 $readFds = array_merge($conn, [$fd]); 这句,$conn 是一个 sokect 数组,所以可以保持每次 select 都是在链接的客户端,我测试过没问题,a 发消息,b,c,d 都可以收到
    simonlu9
        14
    simonlu9  
       2019-10-12 00:32:30 +08:00
    @haiyang416 不好意思,你说的是对的, $readFds[$i] = $newConn; 这句代码有问题,如果新连接 A 到刚 accept 马上 read 会导致堵塞代码的,所以当之后的连接都感应不了,只有等 A 发消息后,其他的连接才能 accept,然后才能收到消息
    ,处理这种错误最好每次读完消息都把它剔除,然后再加入 select
    awanganddong
        15
    awanganddong  
    OP
       2019-10-12 09:34:33 +08:00
    @haiyang416

    整个流程明白了
    接下来我还要理解下 socket_select 更近一些


    谢谢大家了
    lolizeppelin
        16
    lolizeppelin  
       2019-10-12 09:50:40 +08:00
    这些都是基础的系统调用,任何语言都一样的,无非都是多路复用阻塞非阻塞的基础知识
    纠结到 php 上反而容易混乱,你应该去读 c 相关的文档
    其实这些去看 python 文档也不错,比较接近 c 的语法也容易理解
    awanganddong
        17
    awanganddong  
    OP
       2019-10-12 11:43:20 +08:00
    php 和 c 实现是大致一样,只不过 c 指针不可控。从 php 入手可以浅入深出(重要的是我学的就是 php 啊)。
    毕竟牵扯到底层函数 php 都是移植 c 的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3427 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 00:06 · PVG 08:06 · LAX 17:06 · JFK 20:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.