Php Socket学习

php socket函数

https://www.php.net/manual/zh/ref.sockets.php

简单的server

netstat -an|grep 6001
telnet 127.0.0.1 6001
<?php
/*
 * AF_INET    IPv4网络协议,TCP和UDP都可用此协议
 * AF_INET6   IPv6网络协议,TCP和UDP都可用此协议
 * AF_UNIX    本地通讯协议,具有高性能和低成本的IPC(进程间通讯)
 *
 * SOCK_STREAM TCP协议套接字
 * SOCK_DGRAM  UDP协议套接字
 *
 * SOL_TCP     TCP协议
 * SOL_UDP     UDP协议
 */
$socket1 = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($socket1,'0.0.0.0',6001); //想让局域网内其他主机连接到该socket,监听ip为0.0.0.0即可

//socket1 负责监听
socket_listen($socket1,3);

//socket2 负责处理通信(接收、发送)
$socket2 = socket_accept($socket1);

//读取客户端发送的数据
$res=socket_read($socket2,1024);

echo $res."\n";

socket_write($socket2,'hello client');

socket_close($socket2);
socket_close($socket1);

简单的client

image-20230715121637803

image-20230715121616175

<?php
 /*
 * AF_INET    IPv4网络协议,TCP和UDP都可用此协议
 * AF_INET6   IPv6网络协议,TCP和UDP都可用此协议
 * AF_UNIX    本地通讯协议,具有高性能和低成本的IPC(进程间通讯)
 *
 * SOCK_STREAM TCP协议套接字
 * SOCK_DGRAM  UDP协议套接字
 *
 * SOL_TCP     TCP协议
 * SOL_UDP     UDP协议
 */
$host = '127.0.0.1';
//$host='192.168.0.102'; //SocketTool
$port = 6001;
$sock = null;

if (!$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) { //创建socket
    echo "socket_create() failed ,reason:" .
        socket_strerror(socket_last_error()) . "\n";
    return;
}
if ($con = socket_connect($sock, $host, $port) === false) {
    echo 'connect fail' . "\n";
    return;
} else {
    echo 'connect success' . "\n";

}

$message = "hello server";
if (socket_write($sock, $message, strlen($message)) === false) {
    echo 'fail to write' . socket_strerror(socket_last_error());
} else {
    echo 'client write success' . PHP_EOL . "\n";
    //读取服务端返回来的套接流信息
    while ($serverback = socket_read($sock, 1024)) {
        echo 'server return message is' . PHP_EOL . $serverback;
    }
}

调试工具:socketTool

服务端测试

image-20230715122100934

客户端测试

image-20230715122227011

浏览器连接socket

浏览器控制台:

ws=new WebSocket('ws://192.168.0.102:6001');

浏览器结果

image-20230715122824075

服务端结果

image-20230715122845275

这是因为服务端没有成功进行握手,我们需要对服务端代码进行改造

websocket01.php

<?php
/*
 * AF_INET    IPv4网络协议,TCP和UDP都可用此协议
 * AF_INET6   IPv6网络协议,TCP和UDP都可用此协议
 * AF_UNIX    本地通讯协议,具有高性能和低成本的IPC(进程间通讯)
 *
 * SOCK_STREAM TCP协议套接字
 * SOCK_DGRAM  UDP协议套接字
 *
 * SOL_TCP     TCP协议
 * SOL_UDP     UDP协议
 */
$socket1 = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($socket1,'0.0.0.0',6002);

//socket1 负责监听
socket_listen($socket1,3);

//socket2 负责处理通信(接收、发送)
$socket2 = socket_accept($socket1);

//读取客户端发送的数据
$res=socket_read($socket2,1024);
echo $res.PHP_EOL;
handshake($res,$socket2);
socket_write($socket2,encode('hello client'));

socket_close($socket2);
socket_close($socket1);

//websocket握手处理
function handshake($buffer,$client_socket){
    //截取Sec-WebSocket-Key的值并加密
    $temp_str=substr($buffer,strpos($buffer,'Sec-WebSocket-Key')+18);
    $client_key=trim(substr($temp_str,0,strpos($temp_str,"\r\n")));
    $server_key=base64_encode(sha1($client_key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
    echo 'client_key['.$client_key."]\n";
    echo 'server_key['.$server_key."]\n\n";

    //以下是响应包内容,浏览器会进行验证,成功后会建立连接
    $handshake_msg="HTTP/1.1 101 Switching Protocols\r\n";
    $handshake_msg.="Upgrade:websocket\r\n";
    $handshake_msg.="Sec-WebSocket-Version: 13\r\n";
    $handshake_msg.="Connection: Upgrade\r\n";
    $handshake_msg.="Sec-WebSocket-Accept: ".$server_key."\r\n\r\n";
    socket_write($client_socket,$handshake_msg,strlen($handshake_msg));
    echo $handshake_msg;
}
//数据帧处理
function encode($buffer){
    $len=strlen($buffer);
    if($len<=125){
        return "\x81".chr($len).$buffer;
    }else if ($len<=65535){
        return  "\x81".chr(126).pack("n",$len).$buffer;
    }else{
        return  "\x81".chr(127).pack("xxxxN",$len).$buffer;
    }
}
//数据帧处理
function decode($buffer)  {
    $len = $masks = $data = $decoded = null;
    $len = ord($buffer[1]) & 127;

    if ($len === 126)  {
        $masks = substr($buffer, 4, 4);
        $data = substr($buffer, 8);
    } else if ($len === 127)  {
        $masks = substr($buffer, 10, 4);
        $data = substr($buffer, 14);
    } else  {
        $masks = substr($buffer, 2, 4);
        $data = substr($buffer, 6);
    }
    for ($index = 0; $index < strlen($data); $index++) {
        $decoded .= $data[$index] ^ $masks[$index % 4];
    }
    return $decoded;
}

再浏览器输入

ws=new WebSocket('ws://192.168.0.102:6002');

image-20230715130307957

这个时候没有报错了,说明浏览器已经校验通过了服务端发来的信息

这里的问题是==服务器并没有保持监听而是断开了连接,我们需要再次修改服务端代码==

image-20230715130546478

websocket02.php

这个版本的服务端主要是解决了==持久连接的问题和多用户连接的问题==,注意这个版本不适配浏览器(搞定了持久化连接和诸多问题以后再去改成适配浏览器的版本)

建议debug看一下代码执行流程

注意这个socket_select($read, $write, $except, null),三个变量只有$read不是null,他只会监测$read,一旦$read里面的套接字有发生了变化的就进入if,并且让$read只剩下发生了变化的套接字,而我们的第二个if in_array($socket1, $read)就限定了只监测接收我们的$socket1

如果socket_select($read, $write, $except, null)没有监测到$read发生变化,就进入处理消息的那端代码了,就是说if (socket_select($read, $write, $except, null) > 0)这段代码是管建立连接的,if (count($read) > 0)这段代码是管处理消息的

<?php
/*
 * AF_INET    IPv4网络协议,TCP和UDP都可用此协议
 * AF_INET6   IPv6网络协议,TCP和UDP都可用此协议
 * AF_UNIX    本地通讯协议,具有高性能和低成本的IPC(进程间通讯)
 *
 * SOCK_STREAM TCP协议套接字
 * SOCK_DGRAM  UDP协议套接字
 *
 * SOL_TCP     TCP协议
 * SOL_UDP     UDP协议
 */
$socket1 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket1, '0.0.0.0', 6003);

//socket1 负责监听
socket_listen($socket1, 3);

$clients = array($socket1);
while (1) {
    //socket_select对读写套接字的数字是引用,为了保证clients不被改变,拷贝一份
    $read = $clients;
    $write = null;
    $except = null;

    /*当read发生变化,说明:有客户端进行连接操作了,
    $writr和$except均为null,所以只检测read数组的变化
    socket_select的第四个参数null,表示阻塞,它会阻塞一直到发生变化,
                    0,表示为非阻塞,调用socket_select()后,立即返回,然后继续调用socket_select()不断循环
    返回值是修改后的数组中包含套接字资源的数量
    */
    if (socket_select($read, $write, $except, null) > 0) {
        if (in_array($socket1, $read)) {
            //socket2负责处理通信(接收、发送)
            $socket2 = socket_accept($socket1);
            $clients[] = $socket2;
            socket_write($socket2, 'You are' . (count($clients) - 1) . "clients connect\r\n");
            socket_getpeername($socket2, $ip, $port);
            echo "\nnew client[" . $ip . ":" . $port . "]\n";
            $key = array_search($socket1, $read);
            unset($read[$key]);
        }

        if (count($read) > 0) {
            foreach ($read as $socket_item) {
                //读取客户端发来的数据
                $msg = socket_read($socket_item, 1024);

                //发给自己:$socket_item
                socket_write($socket_item, 'received' . $msg);
                foreach ($clients as $client_socket) {
                    //发给别人(除去监听和自己)
                    if ($client_socket != $socket1 && $client_socket != $socket_item) {
                        sleep(1);
                        socket_write($client_socket, $msg, strlen($msg));
                    }
                }
            }
        }
    } else {
        continue;
    }
}

image-20230715220850020

这个服务端可以持久连接,但是还有一个新的问题,就是==有连接退出时我们没有进行处理,导致它会一直尝试连接==

image-20230715221254097

websocket03.php

前面说上个版本的代码有连接退出时我们没有进行处理,导致它会一直尝试连接

这个版本完成==监测客户端断开并处理==

它做的事情就是给每次写数据都加了if,如果判断不通过就把这次不通过的套接字在数组里删掉

if (socket_select($read, $write, $except, null) > 0)这段代码没变,if (count($read) > 0)这段代码的foreach里面加了三个if

<?php
/*
 * AF_INET    IPv4网络协议,TCP和UDP都可用此协议
 * AF_INET6   IPv6网络协议,TCP和UDP都可用此协议
 * AF_UNIX    本地通讯协议,具有高性能和低成本的IPC(进程间通讯)
 *
 * SOCK_STREAM TCP协议套接字
 * SOCK_DGRAM  UDP协议套接字
 *
 * SOL_TCP     TCP协议
 * SOL_UDP     UDP协议
 */
$socket1 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket1, '0.0.0.0', 6004);

//socket1 负责监听
socket_listen($socket1, 3);

$clients = array($socket1);
while (1) {
    //socket_select对读写套接字的数字是引用,为了保证clients不被改变,拷贝一份
    $read = $clients;
    $write = null;
    $except = null;

    /*当read发生变化,说明:有客户端进行连接操作了,
    $write和$except均为null,所以只检测read数组的变化
    socket_select的第四个参数null,表示阻塞,它会阻塞一直到发生变化,
                    0,表示为非阻塞,调用socket_select()后,立即返回,然后继续调用socket_select()不断循环
    返回值是修改后的数组中包含套接字资源的数量
    */
    if (socket_select($read, $write, $except, null) > 0) {
        /*socket_select 接受三个套接字数组,分别检查数组中的套接字是否处于可以操作的状态(返回时只保留可操作的套接字)
        使用最多的是 $read,因此以读为例,在套接字数组 $read 中最初应保有一个服务端监听套接字,每当该套接字可读时,就表示有一个用户发起了连接。此时你需要对该连接创建一个套接字,并加入到 $read 数组中
        当然,并不只是服务端监听的套接字会变成可读的,用户套接字也会变成可读的,此时你就可以读取用户发来的数据了
        socket_select 只在套接字数组发生了变化时才返回。也就是说,一旦执行到 socket_select 的下一条语句,则必有一个套接字是需要你操作的*/
        if (in_array($socket1, $read)) {
            //socket2负责处理通信(接收、发送)
            $socket2 = socket_accept($socket1);
            $clients[] = $socket2;
            socket_write($socket2, 'You are  ' . (count($clients) - 1) . "  clients connect\r\n");
            socket_getpeername($socket2, $ip, $port);
            echo "\nnew client[" . $ip . ":" . $port . "]\n";
            $key = array_search($socket1, $read);
            unset($read[$key]);
        }

        if (count($read) > 0) {
            foreach ($read as $socket_item) {
                //读取客户端发来的数据
                if (false === $msg = @socket_read($socket_item, 1024)) {
                    socket_close($socket_item);
                    $key = array_search($socket_item, $read);
                    unset($read[$key]);
                    unset($clients[$key]);
                    echo "READ ERROR client $key is closed!\n";
                    continue;
                }


                //发给自己:$socket_item
                if (false === @socket_write($socket_item, 'I post : ' . $msg)) {
                    socket_close($socket_item);
                    $key1 = array_search($socket_item, $read);
                    unset($read[$key1]);
                    $key2 = array_search($socket_item, $clients);
                    unset($clients[$key2]);
                    echo "MYclient $key is closed!\n";
                    continue;
                }
                foreach ($clients as $client_socket) {
                    //发给别人(除去监听和自己)
                    if ($client_socket != $socket1 && $client_socket != $socket_item) {
                        sleep(1);
                        //socket_write($client_socket, $msg, strlen($msg));
                        if (false === @socket_write($client_socket, $msg, strlen($msg))) {
                            socket_close($client_socket);
                            $key1 = array_search($client_socket, $read);
                            unset($read[$key1]);
                            $key2 = array_search($client_socket, $clients);
                            unset($clients[$key2]);
                            echo "otherclient $key is closed!\n";
                        }
                    }
                }
            }
        }
    } else {
        continue;
    }
}

但是可以看到这种判断方法并不优雅,实际上有更好的解决方法

websocket04.php

socket_recv

socket_recv() 函数用于从一个已连接的 socket收数据。

socket_recv($socket, &$buf, $, $flags);

参数说明:

  • $socket: 已连接的 socket 资源。
  • &$buf: 接收到的数据将存储在该变量中。
  • $len: 期望接收的数据的最大字节数。
  • $flags:可选参数,用于指定接收操作的一些特殊选项。

返回值:

  • 成功时,返回接收到的数据的字节数。
  • 失败时,返回 false,并且可以通过调用 socket_last_error() 函数获取错误码。

示例代码如下:

$socket=socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 将 $socket 连接到服务器

$buf = ""; // 存储接收到的数据
$len = 1024; // 最大字节数
$flags = 0; // 没有特殊选项

$bytesReceived = socket_recv($socket, $buf, $len, $flags);

if ($bytesReceived === false) {
    echo "接收数据失败: " . socket_strerror(socket_last_error()) . "\n";
} else {
    echo "接收到的数据: " . $buf . "\n";
    echo "接收到的字节数:" . $bytesReceived . "\n";
}

socket_close($socket);

请注意,在使用 之前,你需要先创建一个已连接的 socket,并且通过 socket_connect()` 函数将其与远程服务器连接起来。

<?php
/*
 * AF_INET    IPv4网络协议,TCP和UDP都可用此协议
 * AF_INET6   IPv6网络协议,TCP和UDP都可用此协议
 * AF_UNIX    本地通讯协议,具有高性能和低成本的IPC(进程间通讯)
 *
 * SOCK_STREAM TCP协议套接字
 * SOCK_DGRAM  UDP协议套接字
 *
 * SOL_TCP     TCP协议
 * SOL_UDP     UDP协议
 */
$socket1 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket1, '0.0.0.0', 6005);

//socket1 负责监听
socket_listen($socket1, 3);

$clients = array($socket1);
while (1) {
    //socket_select对读写套接字的数字是引用,为了保证clients不被改变,拷贝一份
    $read = $clients;
    $write = null;
    $except = null;

    /*当read发生变化,说明:有客户端进行连接操作了,
    $write和$except均为null,所以只检测read数组的变化
    socket_select的第四个参数null,表示阻塞,它会阻塞一直到发生变化,
                    0,表示为非阻塞,调用socket_select()后,立即返回,然后继续调用socket_select()不断循环
    返回值是修改后的数组中包含套接字资源的数量
    */
    if (socket_select($read, $write, $except, null) > 0) {
        /*socket_select 接受三个套接字数组,分别检查数组中的套接字是否处于可以操作的状态(返回时只保留可操作的套接字)
        使用最多的是 $read,因此以读为例,在套接字数组 $read 中最初应保有一个服务端监听套接字,每当该套接字可读时,就表示有一个用户发起了连接。此时你需要对该连接创建一个套接字,并加入到 $read 数组中
        当然,并不只是服务端监听的套接字会变成可读的,用户套接字也会变成可读的,此时你就可以读取用户发来的数据了
        socket_select 只在套接字数组发生了变化时才返回。也就是说,一旦执行到 socket_select 的下一条语句,则必有一个套接字是需要你操作的*/
        if (in_array($socket1, $read)) {
            //socket2负责处理通信(接收、发送)
            $socket2 = socket_accept($socket1);
            $clients[] = $socket2;
            socket_write($socket2, 'You are  ' . (count($clients) - 1) . "  clients connect\r\n");
            socket_getpeername($socket2, $ip, $port);
            echo "\nnew client[" . $ip . ":" . $port . "]\n";
            $key = array_search($socket1, $read);

            unset($read[$key]);
        }

        if (count($read) > 0) {
            foreach ($read as $socket_item) {
                //读取客户端发来的数据
                //$msg = socket_read($socket_item, 1024);
                $result = socket_recv($socket_item, $msg, 1024, 0);
                if ($result === false) {
                    //no data 客户端未发来消息
                    echo "no data" . PHP_EOL;
                    continue;
                } elseif ($result === 0) {
                    //socket closed 客户端断开
                    socket_close($socket_item); //关闭服务器的socket
                    //清理已关闭的连接消息
                    $key1 = array_search($socket_item, $read);
                    unset($read[$key1]);
                    $key2 = array_search($socket_item, $clients);
                    unset($clients[$key2]);
                    echo "client [$socket_item] is closed!\n";
                } else {
                    echo "Server receive success [" . $msg . "]" . time() . PHP_EOL;
                }

                foreach ($clients as $client_socket) {
                    //发给别人(除去监听和自己)
                    if ($client_socket != $socket1 && $client_socket != $socket_item) {
                        sleep(1);
                        //socket_write($client_socket, $msg, strlen($msg));
                        if (false === @socket_write($client_socket, $msg, strlen($msg))) {
                            socket_close($client_socket);
                            $key1 = array_search($client_socket, $read);
                            unset($read[$key1]);
                            $key2 = array_search($client_socket, $clients);
                            unset($clients[$key2]);
                            echo "otherclient $key is closed!\n";
                        }
                    }
                }
            }
        }
    } else {
        continue;
    }
}

变化是

 if (false === $msg = @socket_read($socket_item, 1024)) {
                    socket_close($socket_item);
                    $key = array_search($socket_item, $read);
                    unset($read[$key]);
                    unset($clients[$key]);
                    echo "READ ERROR client $key is closed!\n";
                    continue;
                }


                //发给自己:$socket_item
                if (false === @socket_write($socket_item, 'I post : ' . $msg)) {
                    socket_close($socket_item);
                    $key1 = array_search($socket_item, $read);
                    unset($read[$key1]);
                    $key2 = array_search($socket_item, $clients);
                    unset($clients[$key2]);
                    echo "MYclient $key is closed!\n";
                    continue;
                }

变成了

$result = socket_recv($socket_item, $msg, 1024, 0);
                if ($result === false) {
                    //no data 客户端未发来消息
                    echo "no data" . PHP_EOL;
                    continue;
                } elseif ($result === 0) {
                    //socket closed 客户端断开
                    socket_close($socket_item); //关闭服务器的socket
                    //清理已关闭的连接消息
                    $key1 = array_search($socket_item, $read);
                    unset($read[$key1]);
                    $key2 = array_search($socket_item, $clients);
                    unset($clients[$key2]);
                    echo "client [$socket_item] is closed!\n";
                } else {
                    echo "Server receive success [" . $msg . "]" . time() . PHP_EOL;
                }

发现失去了查看自己发送的消息的功能,我又给加上了

<?php
/*
 * AF_INET    IPv4网络协议,TCP和UDP都可用此协议
 * AF_INET6   IPv6网络协议,TCP和UDP都可用此协议
 * AF_UNIX    本地通讯协议,具有高性能和低成本的IPC(进程间通讯)
 *
 * SOCK_STREAM TCP协议套接字
 * SOCK_DGRAM  UDP协议套接字
 *
 * SOL_TCP     TCP协议
 * SOL_UDP     UDP协议
 */
$socket1 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket1, '0.0.0.0', 6004);

//socket1 负责监听
socket_listen($socket1, 3);

$clients = array($socket1);
while (1) {
    //socket_select对读写套接字的数字是引用,为了保证clients不被改变,拷贝一份
    $read = $clients;
    $write = null;
    $except = null;

    /*当read发生变化,说明:有客户端进行连接操作了,
    $write和$except均为null,所以只检测read数组的变化
    socket_select的第四个参数null,表示阻塞,它会阻塞一直到发生变化,
                    0,表示为非阻塞,调用socket_select()后,立即返回,然后继续调用socket_select()不断循环
    返回值是修改后的数组中包含套接字资源的数量
    */
    if (socket_select($read, $write, $except, null) > 0) {
        /*socket_select 接受三个套接字数组,分别检查数组中的套接字是否处于可以操作的状态(返回时只保留可操作的套接字)
        使用最多的是 $read,因此以读为例,在套接字数组 $read 中最初应保有一个服务端监听套接字,每当该套接字可读时,就表示有一个用户发起了连接。此时你需要对该连接创建一个套接字,并加入到 $read 数组中
        当然,并不只是服务端监听的套接字会变成可读的,用户套接字也会变成可读的,此时你就可以读取用户发来的数据了
        socket_select 只在套接字数组发生了变化时才返回。也就是说,一旦执行到 socket_select 的下一条语句,则必有一个套接字是需要你操作的*/
        if (in_array($socket1, $read)) {
            //socket2负责处理通信(接收、发送)
            $socket2 = socket_accept($socket1);
            $clients[] = $socket2;
            socket_write($socket2, 'You are  ' . (count($clients) - 1) . "  clients connect\r\n");
            socket_getpeername($socket2, $ip, $port);
            echo "\nnew client[" . $ip . ":" . $port . "]\n";
            $key = array_search($socket1, $read);

            unset($read[$key]);
        }

        if (count($read) > 0) {
            foreach ($read as $socket_item) {
                //读取客户端发来的数据
                //$msg = socket_read($socket_item, 1024);
                $result = socket_recv($socket_item, $msg, 1024, 0);
                if ($result === false) {
                    //no data 客户端未发来消息
                    echo "no data" . PHP_EOL;
                    continue;
                } elseif ($result === 0) {
                    //socket closed 客户端断开
                    socket_close($socket_item); //关闭服务器的socket
                    //清理已关闭的连接消息
                    $key1 = array_search($socket_item, $read);
                    unset($read[$key1]);
                    $key2 = array_search($socket_item, $clients);
                    unset($clients[$key2]);
                    echo "client [$socket_item] is closed!\n";
                } else {
                    echo "Server receive success [" . $msg . "]" . time() . PHP_EOL;
                    if (false === @socket_write($socket_item, 'I post : ' . $msg)) {
                        socket_close($socket_item);
                        $key1 = array_search($socket_item, $read);
                        unset($read[$key1]);
                        $key2 = array_search($socket_item, $clients);
                        unset($clients[$key2]);
                        echo "MYclient $key is closed!\n";
                        continue;
                    }
                }


                foreach ($clients as $client_socket) {
                    //发给别人(除去监听和自己)
                    if ($client_socket != $socket1 && $client_socket != $socket_item) {
                        sleep(1);
                        //socket_write($client_socket, $msg, strlen($msg));
                        if (false === @socket_write($client_socket, $msg, strlen($msg))) {
                            socket_close($client_socket);
                            $key1 = array_search($client_socket, $read);
                            unset($read[$key1]);
                            $key2 = array_search($client_socket, $clients);
                            unset($clients[$key2]);
                            echo "otherclient $key is closed!\n";
                        }
                    }
                }
            }
        }
    } else {
        continue;
    }
}

websocket05.php

解决了php socket通信的诸多问题,我们现在来完成websocket聊天室雏形!

ws=new WebSocket('ws://192.168.10.106:6006');
ws.onmessage=function(msg){console.log(msg.data)};
ws.send('hello world!!');

image-20230716015221722

服务端代码:

<?php


$socket1 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket1, '0.0.0.0', 6006);

//socket1 负责监听
socket_listen($socket1, 3);

$clients = array($socket1);
while (true) {
    //socket_select对读写套接字的数字是引用,为了保证clients不被改变,拷贝一份
    $read = $clients;
    $write = null;
    $except = null;
    if (socket_select($read, $write, $except, null) > 0) {
        foreach ($read as $socket_item) {
            if ($socket_item == $socket1) {
                if (!$socket_client = socket_accept($socket_item)) continue;
                $hello = @socket_read($socket_client, 1024);
                if ($hello === false) {
                    socket_close($socket_client);
                    continue;
                }
                handshake($hello, $socket_client);
                socket_getpeername($socket_client, $ip, $port);
                $clients[$ip.":".$port] = $socket_client;
                echo "new client[".$ip.":".$port."]\n";
                echo "hello msg [".$hello."]\n";
            }
            else {
                $result = @socket_recv($socket_item, $msg, 1024, 0);
                if ($result === false) continue;
                if ($result === 0) {
                    socket_close($socket_item);
                    $key1 = array_search($socket_item, $read);
                    unset($read[$key1]);
                    $key2 = array_search($socket_item, $clients);
                    unset($clients[$key2]);
                    echo "client [$socket_item] is closed!\n";
                }
                else {
                    $web_msg = decode($msg);
                    $id = search($socket_item, $clients);
                    echo "client [" . $id . "] say:" . $web_msg . "\n";
                    $broadcast = encode($web_msg);

                    foreach ($clients as $client_socket) {
                        //发给别人(除去监听和自己)
                        if ($client_socket != $socket1) {
                            //socket_write($client_socket, $msg, strlen($msg));
                            if (false === @socket_write($client_socket, $broadcast, strlen($broadcast))) {
                                socket_close($client_socket);
                                $key1 = array_search($client_socket, $read);
                                unset($read[$key1]);
                                $key2 = array_search($client_socket, $clients);
                                unset($clients[$key2]);
                                echo "otherclient [$socket_item] is closed!\n";
                            }else{
                                echo '消息已发送给'.$client_socket."\n";
                            }
                        }
                    }
                }
            }
        }
    } else {
        continue;
    }
}
function handshake($buffer, $client_socket)
{
    //截取Sec-WebSocket-Key的值并加密
    $temp_str = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key') + 18);
    $client_key = trim(substr($temp_str, 0, strpos($temp_str, "\r\n")));
    $server_key = base64_encode(sha1($client_key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
    echo 'client_key[' . $client_key . "]\n";
    echo 'server_key[' . $server_key . "]\n\n";
    if ($client_key == '') return false;

    //以下是响应包内容,浏览器会进行验证,成功后会建立连接
    $handshake_msg = "HTTP/1.1 101 Switching Protocols\r\n";
    $handshake_msg .= "Upgrade: websocket\r\n";
    $handshake_msg .= "Sec-WebSocket-Version: 13\r\n";
    $handshake_msg .= "Connection: Upgrade\r\n";
    $handshake_msg .= "Sec-WebSocket-Accept: " . $server_key ."\r\n\r\n";
    socket_write($client_socket, $handshake_msg, strlen($handshake_msg));
    echo $handshake_msg;
    return true;
}

//数据帧处理
function encode($buffer)
{
    $len = strlen($buffer);
    if ($len <= 125) {
        return "\x81" . chr($len) . $buffer;
    } else if ($len <= 65535) {
        return "\x81" . chr(126) . pack("n", $len) . $buffer;
    } else {
        return "\x81" . chr(127) . pack("xxxxN", $len) . $buffer;
    }
}

function decode($buffer)
{
    $len = $masks = $data = $decoded = null;
    $len = ord($buffer[1]) & 127;

    if ($len === 126) {
        $masks = substr($buffer, 4, 4);
        $data = substr($buffer, 8);
    } else if ($len === 127) {
        $masks = substr($buffer, 10, 4);
        $data = substr($buffer, 14);
    } else {
        $masks = substr($buffer, 2, 4);
        $data = substr($buffer, 6);
    }
    for ($index = 0; $index < strlen($data); $index++) {
        $decoded .= $data[$index] ^ $masks[$index % 4];
    }
    return $decoded;
}

function search($socket_client, $clients)
{
    $search = array_search($socket_client, $clients, true);
    if ($search === null) $search = false;
    return $search;
}

下面我们来搞一个客户端,主要就是javascript

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>socket</title>
</head>

<body>
<div id="cc" style="width:500px;">
  <div id="content" style=" height:400px; overflow:auto; width:100%; border:1px solid #ccc;"></div>
  <form id="form">
    <textarea id="text" style="width:100%; height:200px;"></textarea>
    <input type="button" onclick="ab();" value="发送">
  </form>
</div>
<p id="a"></p>
<script src="https://code.jquery.com/jquery-2.1.3.min.js" type="text/javascript"></script>


</body>
</html>
<script>
  var wsServer = 'ws://192.168.111.1:6006';
  var websocket = new WebSocket(wsServer);
  websocket.onopen = function (evt) {
    console.log("Connected to WebSocket server.");
  };

  websocket.onclose = function (evt) {
    console.log("Disconnected");
  };

  websocket.onmessage = function (evt) {
    $('#content').append(evt.data+"<br>");

    // document.getElementById('div').style.background = evt.data;
    console.log('Retrieved data from server: ' + evt.data);
  };

  websocket.onerror = function (evt, e) {
    console.log('Error occured: ' + evt.data);
  };
  function ab(){
    var zhi = $('#text').val();
    websocket.send(zhi);
    $('#text').val('');

  }

</script>
0%