PHP原生socket详解

PHP原生WebSocket服务

来源

https://www.cnblogs.com/loveyoume/p/6076101.html
https://blog.csdn.net/weixin_42133420/article/details/80444661
https://blog.csdn.net/zhang197093/article/details/77366407
http://www.cppblog.com/kenkao/archive/2016/08/30/214241.html
以及个人测试结果和见解

注解

首先,解释一下目前 Socket 领域比较易于混淆的概念有:阻塞/非阻塞、同步/异步、多路复用等。

1、阻塞/非阻塞:这两个概念是针对 IO 过程中进程的状态来说的,阻塞 IO是指调用结果返回之前,当前线程会被挂起;相反,非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
2、同步/异步:这两个概念是针对调用如果返回结果来说的,所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回;相反,当一个异步过程调用发出后,调用者不能立刻得到结果,实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
3、多路复用(IO/Multiplexing):为了提高数据信息在网络通信线路中传输的效率,在一条物理通信线路上建立多条逻辑通信信道,同时传输若干路信号的技术就叫做多路复用技术。对于 Socket 来说,应该说能同时处理多个连接的模型都应该被称为多路复用,目前比较常用的有 select/poll/epoll/kqueue 这些 IO 模型(目前也有像 Apache 这种每个连接用单独的进程/线程来处理的 IO 模型,但是效率相对比较差,也很容易出问题,所以暂时不做介绍了)。在这些多路复用的模式中,异步阻塞/非阻塞模式的扩展性和性能最好。

Epoll是基于 PHP 的 libevent 扩展实现的,需要运行的话要先安装此扩展.

PHP原生socket的socket_set_option详解_使用可重复监听使每次的重启进程不会报错如地址被占用

// $this->master_socket = null? socket_create()返回resource-套接字或者false
$this->master_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 

// 设置IP和端口重用,在重启服务器后能重新使用此端口;
socket_set_option($this->master_socket, SOL_SOCKET, SO_REUSEADDR, 1);\

// 将IP和端口绑定在服务器socket上;
socket_bind($this->master_socket, $host, $port);

//Warning: socket_bind(): unable to bind address [48]: Address already in use in XXX.php on line 13
// listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。
// 在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接,其中的能存储的请求不明的socket数目。
socket_listen($this->master_socket, self::LISTEN_SOCKET_NUM);//最多在线人数,超过的客户端连接会返回WSAECONNREFUSED错误

常用参数

注意:

socket_create,socket_bind, socket_listen, socket_accept三个函数的执行顺序不可更改,
也就是说必须先执行socket_create,socket_bind,再执行socket_listen,最后才执行socket_accept
在做守护进程时也是在listen的后面做while(true)

// $this->master_socket = null? socket_create()返回resource-套接字或者false
$this->master_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 

// 设置IP和端口重用,在重启服务器后能重新使用此端口;
socket_set_option($this->master_socket, SOL_SOCKET, SO_REUSEADDR, 1);\

// 将IP和端口绑定在服务器socket上;
socket_bind($this->master_socket, $host, $port);

// listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。
// 在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接,其中的能存储的请求不明的socket数目。
socket_listen($this->master_socket, self::LISTEN_SOCKET_NUM);//最多在线人数,超过的客户端连接会返回WSAECONNREFUSED错误

参数

socket_accept() 接受一个Socket连接
socket_bind() 把socket绑定在一个IP地址和端口上
socket_clear_error() 清除socket的错误或者最后的错误代码
socket_close() 关闭一个socket资源
socket_connect() 开始一个socket连接
socket_create_listen() 在指定端口打开一个socket监听
socket_create_pair() 产生一对没有区别的socket到一个数组里
socket_create() 产生一个socket,相当于产生一个socket的数据结构
socket_get_option() 获取socket选项
socket_getpeername() 获取远程类似主机的ip地址
socket_getsockname() 获取本地socket的ip地址
socket_iovec_add() 添加一个新的向量到一个分散/聚合的数组
socket_iovec_alloc() 这个函数创建一个能够发送接收读写的iovec数据结构
socket_iovec_delete() 删除一个已经分配的iovec
socket_iovec_fetch() 返回指定的iovec资源的数据
socket_iovec_free() 释放一个iovec资源
socket_iovec_set() 设置iovec的数据新值
socket_last_error() 获取当前socket的最后错误代码
socket_listen() 监听由指定socket的所有连接
socket_read() 读取指定长度的数据			# 仅仅读取数据
socket_readv() 读取从分散/聚合数组过来的数据
socket_recv() 从socket里结束数据到缓存	# 读取数据同时保存在缓存去中, 同时返回内容的字符串
socket_recvfrom() 接受数据从指定的socket,如果没有指定则默认当前socket
socket_recvmsg() 从iovec里接受消息
socket_select() 多路选择
socket_send() 这个函数发送数据到已连接的socket
socket_sendmsg() 发送消息到socket
socket_sendto() 发送消息到指定地址的socket
socket_set_block() 在socket里设置为块模式
socket_set_nonblock() socket里设置为非块模式
socket_set_option() 设置socket选项
socket_shutdown() 这个函数允许你关闭读、写、或者指定的socket
socket_strerror() 返回指定错误号的详细错误
socket_write() 写数据到socket缓存
socket_writev() 写数据到分散/聚合数组

参数详解

socket_create

socket_create($net参数1,$stream参数2,$protocol参数3)
作用:创建一个socket套接字,说白了,就是一个网络数据流。
返回值:一个套接字,或者是false,参数错误发出E_WARNING警告

php的在线手册那里说得更清楚:
socket_create创建并返回一个套接字,也称作一个通讯节点。一个典型的网络连接由 2 个套接字构成,一个运行在客户端,另一个运行在服务器端。
上面一句话是从php在线手册那里复制过来的。

参数1是:网络协议,
网络协议有哪些?它的选择项就下面这三个:
AF_INET:     IPv4 网络协议。TCP 和 UDP 都可使用此协议。一般都用这个,你懂的。
AF_INET6:   IPv6 网络协议。TCP 和 UDP 都可使用此协议。
AF_UNIX:      本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。

参数2:套接字流,选项有:
SOCK_STREAM  SOCK_DGRAM  SOCK_SEQPACKET  SOCK_RAW  SOCK_RDM。
这里只对前两个进行解释:
SOCK_STREAM  TCP 协议套接字。
SOCK_DGRAM   UDP协议套接字。
欲了解更多请链接这里:http://php.net/manual/zh/function.socket-create.php

参数3:protocol协议,选项有:
SOL_TCP:  TCP 协议。
SOL_UDP:  UDP协议。
从这里可以看出,其实socket_create函数的第二个参数和第三个参数是相关联的。
比如,假如你第一个参数应用IPv4协议:AF_INET,然后,第二个参数应用的是TCP套接字:SOCK_STREAM,
那么第三个参数必须要用SOL_TCP,这个应该不难理解。
TCP 协议套接字嘛,当然只能用TCP协议了,是不是?如果你应用UDP套接字,那么第三个参数该怎么选择我就不说了,呵呵,你懂的。

socket_connect

socket_connect($socket参数1,$ip参数2,$port参数3)
作用:连接一个套接字,返回值为true或者false
参数1:socket_create的函数返回值
参数2:ip地址
参数3:端口号

socket_bind

socket_bind($socket参数1,$ip参数2,$port参数3)
作用:绑定一个套接字,返回值为true或者false
参数1:socket_create的函数返回值
参数2:ip地址
参数3:端口号

socket_listen

socket_listen($socket参数1,$backlog 参数2)
作用:监听一个套接字,返回值为true或者false
参数1:socket_create的函数返回值
参数2:最大监听套接字个数,即最大支持有几个客户端在等来.
        比如设置为3,当前已有连接,后面的3个连接会进入等待状态, 第四个连接会直接失败.

socket_accept

socket_accept($socket)
作用:接收套接字的资源信息,成功返回套接字的信息资源,失败为false
参数:socket_create的函数返回值

socket_read

socket_read($socket参数1,$length参数2)
作用:读取套接字的资源信息,
返回值:成功把套接字的资源转化为字符串信息,失败为false
参数1:socket_create或者socket_accept的函数返回值
参数2:读取的字符串的长度

socket_write

socket_write($socket参数1,$msg参数2,$strlen参数3)
作用:把数据写入套接字中
返回值:成功返回字符串的字节长度,失败为false
参数1:socket_create或者socket_accept的函数返回值
参数2:字符串
参数3:字符串的长度

socket_close

socket_close($socket)
作用:关闭套接字
返回值:成功返回true,失败为false
参数:socket_create或者socket_accept的函数返回值

socket_set_option

socket_set_option($socket参数1 ,$level 参数2,$optname 参数3,$optval 参数4)
这个函数的作用是给套接字设置数据流选项,还是一个很重要的函数。

参数1:socket_create或者socket_accept的函数返回值
参数2:SOL_SOCKET,好像只有这个选项
参数3与参数4是相关联的,
参数3可为:SO_REUSEADDR  SO_RCVTIMEO     S0_SNDTIMEO

解释一下:
SO_REUSEADDR  是让套接字端口释放后立即就可以被再次使用, 参数3假如是这个,则参数4可以为true或者false

SO_RCVTIMEO     是套接字的接收资源的最大超时时间
SO_SNDTIMEO		是套接字的发送资源的最大超时时间
参数3假如是这两个,则参数4是一个这样的数组array('sec'=>1,'usec'=>500000)
数组里面都是设置超时的最大时间,不过,一个是秒为单位,一个是微秒单位,作用都一样

如下:
//接收套接流的最大超时时间1秒,后面是微秒单位超时时间,设置为零,表示不管它
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 1, "usec" => 0));
//发送套接流的最大超时时间为6秒
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 6, "usec" => 0));
//设置端口重用端口
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);

错误处理

这两个函数在socket编程中还是很重要的,在写socket编程的时候,我觉得你还是得利用起来,特别是新手,可以当做调试用

socket_last_error($socket),参数为socket_create的返回值,作用是获取套接字的最后一条错误码号,返回值套接字code
socket_strerror($code),参数为socket_last_error函数的返回值,获取code的字符串信息,返回值也就是套接字的错误信息

socket_select

int socket_select(array &$read参数1, array &$write参数2, array &$except参数3, int $tv_sec参数4[,int $tv_usec=0])
作用:
	获取read数组中活动的socket,并且把不活跃的从read数组中删除,具体的看文档。。
	这是一个同步方法,必须得到响应之后才会继续下一步,常用在同步非阻塞IO
说明:
	1 新连接到来时,被监听的端口是活跃的,如果是新数据到来或者客户端关闭链接时,活跃的是对应的客户端socket而不是服务器上被监听的端口
	2 如果客户端发来数据没有被读走,则socket_select将会始终显示客户端是活跃状态并将其保存在readfds数组中
	3 如果客户端先关闭了,则必须手动关闭服务器上相对应的客户端socket,否则socket_select也始终显示该客户端活跃(这个道理跟"有新连接到来然后没有用socket_access把它读出来,导致监听的端口一直活跃"是一样的)

官方说明:
在具有指定超时的套接字数组上运行select()系统调用
在给定的几组sockets数组上执行 select() 系统调用,用一个特定的超时时间。
接受几组sockets数组作为参数,并监听它们改变状态
这些基于BSD scokets 能够识别这些socket资源数组实际上就是文件描述符集合。
三个不同的socket资源数组会被同时监听。
这三个资源数组不是必传的, 你可以用一个空数组或者NULL作为参数,不要忘记这三个数组是以引用的方式传递的,在函数返回后,这些数组的值会被改变。
socket_select() 调用成功返回这三个数组中状态改变的socket总数,如果设置了timeout,并且在timeout之内都没有状态改变,这个函数将返回0,出错时返回FALSE,可以用socket_last_error() 获取错误码。

参数1:	读取数组中列出的套接字将被监视,以查看字符是否可用于读取(更准确地说,是查看读取是否不会阻塞——特别是,在文件末尾也准备好了套接字资源,在这种情况下,socket_read()将返回一个零长度字符串)。
参数2:	写入数组中列出的套接字将被监视,以查看写入是否不会阻塞。
参数3:	除了数组中列出的套接字将被监视,以查看是否有异常。
参数4:	当没有套字节可以读写继续等待, 第四个参数为null为阻塞, 为0位非阻塞, 为 >0 为等待时间
tv_sec和tv_usec共同构成超时参数。超时是在socket_select()返回之前经过的时间的上限。tv_sec可能为零,导致socket_select()立即返回。这对于轮询很有用。如果tv_sec为空(没有超时),socket_select()可以无限期地阻塞。


示范代码解释
1. 当有两个客户端并发连接进下面的服务器时
    $readfds = array();  
    $writefds = array();
    $sock = socket_create_listen(2000);
    //socket_set_nonblock($sock); // 非阻塞
    //echo "sleep 10 second...\n";
    //sleep(10);
    socket_getsockname($sock, $addr, $port); 
    print "Server Listening on $addr:$port\n";
    $readfds[(int)$sock]=$sock;
    $conn=socket_accept($sock);
    $readfds[]=$conn;
    $conn=socket_accept($sock);
    $readfds[]=$conn;
    $e = null;
    $t=100;
    $i=1;
    while(true){
        echo "No.$i\n";
        //当select处于等待时,两个客户端中甲先发数据来,则socket_select会在readfds中保留甲的socket并往下运行,另一个客户端的socket就被丢弃了,所以再次循环时,变成只监听甲了,这个可以在新循环中把所有链接的客户端socket再次加进readfds中,则可以避免本程序的这个逻辑错误
        echo @socket_select($readfds, $writefds, $e, $t)."\n";
        var_dump($readfds);
        if(in_array($sock, $readfds)){
            echo "2000 port is activity";
            $readfds[]=socket_accept($sock);
        }
        //将读取到的资源输出
        foreach ($readfds as $s){
            if($s!=$sock){
                //新连接到来时,被监听的端口是活跃的,如果是新数据到来或者客户端关闭链接时,活跃的是对应的客户端socket而不是服务器上被监听的端口
                //如果客户端发来数据没有被读走,则socket_select将会始终显示客户端是活跃状态并将其保存在readfds数组中
                //如果客户端先关闭了,则必须手动关闭服务器上相对应的客户端socket,否则socket_select也始终显示该客户端活跃(这个道理跟"有新连接到来然后没有用socket_access把它读出来,导致监听的端口一直活跃"是一样的)
                $result=@socket_read($s, 1024,PHP_NORMAL_READ);
                if($result===false){
                    $err_code=socket_last_error();
                    $err_test=socket_strerror($err_code);
                    echo "client ".(int)$s." has closed[$err_code:$err_test]\n";
                    //手动关闭客户端,最好清除一下$readfds数组中对应的元素
                    socket_shutdown($s);
                    socket_close($s);
                }else{
                    echo $result;
                }
            }
        }
        usleep(3000000);
        $readfds[(int)$sock]=$sock;
        $i++;
    }  

PHP原生同步阻塞socket

但其实这个TCP服务器是有问题的,它一次只能处理一个客户端的连接和数据传输,
这是因为一个客户端连接过来后,进程就去负责读写客户端数据,
当客户端没有传输数据时,tcp服务器处于阻塞读状态,无法再去处理其他客户端的连接请求了。

解决这个问题的一种办法就是采用多进程服务器,每当一个客户端连接过来,服务器开一个子进程专门负责和该客户端的数据传输,而父进程仍然监听客户端的连接,但是起进程的代价是昂贵的,这种多进程的机制显然支撑不了高并发。

另一个解决办法是使用IO多路复用机制,使用php为我们提供的socket_select方法,它可以监听多个socket,如果其中某个socket状态发生了改变,比如从不可写变为可写,从不可读变为可读,这个方法就会返回,从而我们就可以去处理这个socket,处理客户端的连接,读写操作等等。来看php文档中对该socket_select的介绍

简单的测试服务器,phptcpserver.php 代码

<?php
 
$servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);  // 创建一个socket
 
if (FALSE === $servsock)
{
    $errcode = socket_last_error();
    fwrite(STDERR, "socket create fail: " . socket_strerror($errcode));
    exit(-1);
}
 
if (!socket_bind($servsock, '127.0.0.1', 8888))    // 绑定ip地址及端口
{
    $errcode = socket_last_error();
    fwrite(STDERR, "socket bind fail: " . socket_strerror($errcode));
    exit(-1);
}
 
if (!socket_listen($servsock, 128))      // 允许多少个客户端来排队连接
{
    $errcode = socket_last_error();
    fwrite(STDERR, "socket listen fail: " . socket_strerror($errcode));
    exit(-1);
}
 
while (1)
{
    $connsock = socket_accept($servsock);  //响应客户端连接
 
    if ($connsock)
    {
        socket_getpeername($connsock, $addr, $port);  //获取连接过来的客户端ip地址和端口
        echo "client connect server: ip = $addr, port = $port" . PHP_EOL;
 
        while (1)
        {
            $data = socket_read($connsock, 1024);  //从客户端读取数据
 
            if ($data === '')
            {
                //客户端关闭
                socket_close($connsock);
                echo "client close" . PHP_EOL;
                break;
            }
            else
            {
                echo 'read from client:' . $data;
                $data = strtoupper($data);  //小写转大写
                socket_write($connsock, $data);  //回写给客户端
            }
        }
    }
}
 
socket_close($servsock);

PHP原生多进程socket

<?php

$socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($socket,'127.0.0.1',10000) or die('error');
socket_listen($socket,5);

$child = 0; //初始化子进程数
while(true){
    $client = socket_accept($socket);

    $pid = pcntl_fork();
    if ($pid == -1) {
        die('could not fork');
    } else if ($pid) {
        socket_close($client);
        $child++;
        if($child >= 3){ //假设最大进程数为3
            pcntl_wait($status); //等待上一个进程结束
            $child--;
        }
    } else {
        $buf = socket_read($client,1024);
        echo $buf;
        if(preg_match('/sleep/i',$buf)){
            sleep(10);
            $html = 'HTTP/1.1 200 OK'.PHP_EOL
                .'Content-Type: text/html;charset=utf-8'.PHP_EOL.PHP_EOL;
            socket_write($client,$html);
            socket_write($client,"this is server,休克了10秒,模拟很繁忙的样子");
        }else{
            socket_write($client,"this is server");
        }
        socket_close($client);
        exit;//关闭子进程
    }
}

socket_close($socket);



PHP原生多路复用socket

使用 socket_select() 优化之前 phptcpserver.php 代码

<?php
 
$servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);  // 创建一个socket
 
if (FALSE === $servsock)
{
    $errcode = socket_last_error();
    fwrite(STDERR, "socket create fail: " . socket_strerror($errcode));
    exit(-1);
}
 
if (!socket_bind($servsock, '127.0.0.1', 8888))    // 绑定ip地址及端口
{
    $errcode = socket_last_error();
    fwrite(STDERR, "socket bind fail: " . socket_strerror($errcode));
    exit(-1);
}
 
if (!socket_listen($servsock, 128))      // 允许多少个客户端来排队连接
{
    $errcode = socket_last_error();
    fwrite(STDERR, "socket listen fail: " . socket_strerror($errcode));
    exit(-1);
}
 
/* 要监听的三个sockets数组 */
$read_socks = array();
$write_socks = array();
$except_socks = NULL;  // 注意 php 不支持直接将NULL作为引用传参,所以这里定义一个变量
 
$read_socks[] = $servsock;
 
while (1)
{
    /* 这两个数组会被改变,所以用两个临时变量 */
    $tmp_reads = $read_socks;
    $tmp_writes = $write_socks;
 
    // int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )
    $count = socket_select($tmp_reads, $tmp_writes, $except_socks, NULL);  // timeout 传 NULL 会一直阻塞直到有结果返回
 
    foreach ($tmp_reads as $read)
    {
 
        if ($read == $servsock)
        {
            /* 有新的客户端连接请求 */
            $connsock = socket_accept($servsock);  //响应客户端连接, 此时不会造成阻塞
            if ($connsock)
            {
                socket_getpeername($connsock, $addr, $port);  //获取远程客户端ip地址和端口
                echo "client connect server: ip = $addr, port = $port" . PHP_EOL;
 
                // 把新的连接sokcet加入监听
                $read_socks[] = $connsock;
                $write_socks[] = $connsock;
            }
        }
        else
        {
            /* 客户端传输数据 */
            $data = socket_read($read, 1024);  //从客户端读取数据, 此时一定会读到数组而不会产生阻塞
 
            if ($data === '')
            {
                //移除对该 socket 监听
                foreach ($read_socks as $key => $val)
                {
                    if ($val == $read) unset($read_socks[$key]);
                }
 
                foreach ($write_socks as $key => $val)
                {
                    if ($val == $read) unset($write_socks[$key]);
                }
 
 
                socket_close($read);
                echo "client close" . PHP_EOL;
 
            }
            else
            {
                socket_getpeername($read, $addr, $port);  //获取远程客户端ip地址和端口
 
                echo "read from client # $addr:$port # " . $data;
 
                $data = strtoupper($data);  //小写转大写
 
                if (in_array($read, $tmp_writes))
                {
                    //如果该客户端可写 把数据回写给客户端
                    socket_write($read, $data);
                }
            }
        }
    }
}
 
socket_close($servsock);

基于select的socket多路复用服务,未作守护任务,只处理单次请求然后自动关闭

<?php
# 设置报错级别
error_reporting(E_ALL);
# 开启绝对刷新
ob_implicit_flush();
# 设置时区
date_default_timezone_set('Asia/Shanghai');

/**
 * Class socket 类
 */
class onesocket
{
    public static $sockets;    # socket连接池
    public static $users;      # 所有client连接进来的信息,包括socket,client名字等.
    public static $master;     # socket 初始化资源
    
    /**
     * 处理数据
     */
    public static function run($ip, $port)
    {
        $write=null;
        $except=null;
        
        # 创建socket
        $socket = socket_create(AF_INET, SOCK_STREAM, 0) or die("Could not create socket\n");
        $socketArr[] = $socket;
        echo "句柄创建完毕\n";
        # 创建的socket资源绑定到IP地址和端口号
        $result = socket_bind($socket, $ip, $port) or die("Could not bind to socket\n");
        echo "绑定地址完毕\n";
        
        # 在绑定到IP和端口后,服务端开始等待客户端的连接。在没有连接之前它就一直等下去。
        $result = socket_listen($socket, 5) or die("Could not set up socket listener\n");
        echo "监听完毕\n";
        # 处理数据
        socket_select($socketArr, $write, $except, null);
        echo "select数据\n";
        
        # 第5步:接受连接
        $spawn = socket_accept($socket) or die("Could not accept incoming connection\n");
        echo "接受连接\n";
        # 第6步:从客户端socket读取消息
        $input = socket_recv($spawn, $buf, 1000, 0) or die("Could not read input\n");
        echo "读取信息\n";
        var_dump($buf); # 握手的话在此处读取到了 websocket首次握手时用到的header头
        
        
        
        # 第7步:反转消息
        $output = strrev($input) . "成功收到消息\n";
        echo "翻转信息\n";
        
        # 第8步:发送消息给客户端socket
        socket_write($spawn, $output, strlen($output)) or die("Could not write output\n");
        echo "发送信息\n";
        
        # 关闭socket
        socket_close($spawn);
        socket_close($socket);
        echo "关闭\n";
    }
}

$ip = '127.0.0.1';
$port = '10000';

onesocket::run($ip, $port);

基于select的socket多路复用服务

<?php
# 设置报错级别
error_reporting(E_ALL);
# 开启绝对刷新
ob_implicit_flush();
# 设置时区
date_default_timezone_set('Asia/Shanghai');

/**
 * Class socket 类
 */
class onesocket
{
    public static $sockets;    # socket连接池
    public static $users;      # 所有client连接进来的信息,包括socket,client名字等.
    public static $master;     # socket 初始化资源
    
    /**
     * 处理数据
     */
    public static function run($ip, $port)
    {
        $write=null;
        $except=null;
        
        # 创建socket
        $socket = socket_create(AF_INET, SOCK_STREAM, 0) or die("Could not create socket\n");
        $socketArr[] = $socket;
        echo "句柄创建完毕\n";
        # 创建的socket资源绑定到IP地址和端口号
        $result = socket_bind($socket, $ip, $port) or die("Could not bind to socket\n");
        echo "绑定地址完毕\n";
        
        # 在绑定到IP和端口后,服务端开始等待客户端的连接。在没有连接之前它就一直等下去。
        $result = socket_listen($socket, 5) or die("Could not set up socket listener\n");
        echo "监听完毕\n";
        # 处理数据
        socket_select($socketArr, $write, $except, null);
        echo "select数据\n";
        
        # 第5步:接受连接
        $spawn = socket_accept($socket) or die("Could not accept incoming connection\n");
        echo "接受连接\n";
        # 第6步:从客户端socket读取消息
        $input = socket_recv($spawn, $buf, 1000, 0) or die("Could not read input\n");
        echo "读取信息\n";
        var_dump($buf); # 握手的话在此处读取到了 websocket首次握手时用到的header头
        
        
        
        # 第7步:反转消息
        $output = strrev($input) . "成功收到消息\n";
        echo "翻转信息\n";
        
        # 第8步:发送消息给客户端socket
        socket_write($spawn, $output, strlen($output)) or die("Could not write output\n");
        echo "发送信息\n";
        
        # 关闭socket
        socket_close($spawn);
        socket_close($socket);
        echo "关闭\n";
    }
}

$ip = '127.0.0.1';
$port = '10000';

onesocket::run($ip, $port);

基于select多路复用的PHP原生WebSocket服务

<?php
//确保在连接客户端时不会超时
set_time_limit(0);

$ip = '192.168.80.131';
$port = 2000;

/*
    +-------------------------------
    *    @socket通信整个过程
    +-------------------------------
    *    @socket_create
    *    @socket_bind
    *    @socket_listen
    *    @socket_accept
    *    @socket_read
    *    @socket_write
    *    @socket_close
    +--------------------------------
    */
#句柄
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() failed");

#设置选项 !! !
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1) or die("set option 失败");

#绑定
socket_bind($socket, $ip, $port) or die("绑定出错");

#监听
socket_listen($socket, 2) or die("侦听出错");

socket_select($this-> sockets, $write, $except, NULL);#判断连接
while (true) {
    $write = NULL;
    $except = NULL;
    $this -> sockets[] = $socket;


    foreach($socket as $sock) {#接受传入的套接字
        $client = socket_accept($socket);#处理错误
        if ($client < 0) {
            echo "socket_accept() failed";
            continue;
        } else {
            array_push($this-> sockets, $client);
            echo "connect client\n";
        }

        $buff = socket_read($socket, 2048);
        if (!$this-> handshake) {
            // 如果没有握手,先握手回应
            $this -> doHandShake($socket, $buffer);
            // 获取加密key
            $acceptKey = $this -> encry($buffer);
            $upgrade = "HTTP/1.1 101 Switching Protocols\r\n".
            "Upgrade: websocket\r\n".
            "Connection: Upgrade\r\n".
            "Sec-WebSocket-Accept: ".$acceptKey.
            "\r\n".
            "\r\n";

            echo "dohandshake ".$upgrade.chr(0);
            // 写入socket
            socket_write($socket, $upgrade.chr(0), strlen($upgrade.chr(0)));
            // 标记握手已经成功,下次接受数据采用数据帧格式
            $this -> handshake = true;
            echo "shakeHands\n";
        } else {

            // 如果已经握手,直接接受数据,并处理
            $buffer = $this -> decode($buffer);
            //process($socket, $buffer);
            echo "send file\n";
        }

    }

}

socket_close($socket);

基于select多路复用的PHP原生WebSocket聊天室和私聊服务

<?php
error_reporting(E_ALL ^ E_NOTICE);
ob_implicit_flush();

//下面是sock类
class Sock
{
    public  $sockets; //socket的连接池,即client连接进来的socket标志
    public  $users;   //所有client连接进来的信息,包括socket、client名字等
    public  $master;  //socket的resource,即前期初始化socket时返回的socket资源
    private $sda  = array();   //已接收的数据
    private $slen = array();  //数据总长度
    private $sjen = array();  //接收数据的长度
    private $ar   = array();    //加密key
    private $n    = array();

    public function __construct($address, $port)
    {
        //创建socket并把保存socket资源在$this->master
        $this->master = $this->WebSocket($address, $port);
        //创建socket连接池
        $this->sockets = array($this->master);
    }

    //对创建的socket循环进行监听,处理数据
    function run()
    {
        //死循环,直到socket断开
        while (true) {
            $changes = $this->sockets;
            $write = null;
            $except = null;

            /*
            //这个函数是同时接受多个连接的关键,我的理解它是为了阻塞程序继续往下执行。
            socket_select ($sockets, $write = NULL, $except = NULL, NULL);
            $sockets可以理解为一个数组,这个数组中存放的是文件描述符。当它有变化(就是有新消息到或者有客户端连接/断开)时,socket_select函数才会返回,继续往下执行。
            $write是监听是否有客户端写数据,传入NULL是不关心是否有写变化。
            $except是$sockets里面要被排除的元素,传入NULL是”监听”全部。
            最后一个参数是超时时间
            如果为0:则立即结束
            如果为n>1: 则最多在n秒后结束,如遇某一个连接有新动态,则提前返回
            如果为null:如遇某一个连接有新动态,则返回
            */
            socket_select($changes, $write, $except, null);
            foreach ($changes as $sock) {

                //如果有新的client连接进来,则
                if ($sock == $this->master) {


                    //接受一个socket连接
                    $client = socket_accept($this->master);


                    //给新连接进来的socket一个唯一的ID
                    $key = uniqid();
                    $this->sockets[] = $client;  //将新连接进来的socket存进连接池
                    $this->users[$key] = array(
                        'socket' => $client,  //记录新连接进来client的socket信息
                        'shou'   => false       //标志该socket资源没有完成握手
                    );
                    //否则1.为client断开socket连接,2.client发送信息
                } else {
                    $len = 0;
                    $buffer = '';
                    //读取该socket的信息,注意:第二个参数是引用传参即接收数据,第三个参数是接收数据的长度
                    do {
                        $l = socket_recv($sock, $buf, 1000, 0);
                        $len += $l;
                        $buffer .= $buf;
                    } while ($l == 1000);


                    //根据socket在user池里面查找相应的$k,即健ID
                    $k = $this->search($sock);


                    //如果接收的信息长度小于7,则该client的socket为断开连接
                    if ($len < 7) {
                        //给该client的socket进行断开操作,并在$this->sockets和$this->users里面进行删除
                        $this->send2($k);
                        continue;
                    }
                    //判断该socket是否已经握手
                    if (!$this->users[$k]['shou']) {
                        //如果没有握手,则进行握手处理
                        $this->woshou($k, $buffer);
                    } else {
                        //走到这里就是该client发送信息了,对接受到的信息进行uncode处理
                        $buffer = $this->uncode($buffer, $k);
                        if ($buffer == false) {
                            continue;
                        }
                        //如果不为空,则进行消息推送操作
                        $this->send($k, $buffer);
                    }
                }
            }

        }

    }

    //指定关闭$k对应的socket
    function close($k)
    {
        //断开相应socket
        socket_close($this->users[$k]['socket']);
        //删除相应的user信息
        unset($this->users[$k]);


        //重新定义sockets连接池
        $this->sockets = array($this->master);
        foreach ($this->users as $v) {
            $this->sockets[] = $v['socket'];
        }
        //输出日志
        $this->e("key:$k close");
    }

    //根据sock在users里面查找相应的$k
    function search($sock)
    {
        foreach ($this->users as $k => $v) {
            if ($sock == $v['socket']) {
                return $k;
            }
        }

        return false;
    }

    //传相应的IP与端口进行创建socket操作
    function WebSocket($address, $port)
    {
        $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);//1表示接受所有的数据包
        socket_bind($server, $address, $port);
        socket_listen($server);
        $this->e('Server Started : ' . date('Y-m-d H:i:s'));
        $this->e('Listening on   : ' . $address . ' port ' . $port);

        return $server;
    }


    /*
    * 函数说明:对client的请求进行回应,即握手操作
    * @$k clien的socket对应的健,即每个用户有唯一$k并对应socket
    * @$buffer 接收client请求的所有信息
    */
    function woshou($k, $buffer)
    {


        //截取Sec-WebSocket-Key的值并加密,其中$key后面的一部分258EAFA5-E914-47DA-95CA-C5AB0DC85B11字符串应该是固定的
        $buf = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key:') + 18);
        $key = trim(substr($buf, 0, strpos($buf, "\r\n")));
        $new_key = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));

        //按照协议组合信息进行返回
        $new_message = "HTTP/1.1 101 Switching Protocols\r\n";
        $new_message .= "Upgrade: websocket\r\n";
        $new_message .= "Sec-WebSocket-Version: 13\r\n";
        $new_message .= "Connection: Upgrade\r\n";
        $new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
        socket_write($this->users[$k]['socket'], $new_message, strlen($new_message));


        //对已经握手的client做标志
        $this->users[$k]['shou'] = true;

        return true;

    }

    //解码函数
    function uncode($str, $key)
    {
        $mask = array();
        $data = '';
        $msg = unpack('H*', $str);
        $head = substr($msg[1], 0, 2);
        if ($head == '81' && !isset($this->slen[$key])) {
            $len = substr($msg[1], 2, 2);
            $len = hexdec($len);//把十六进制的转换为十进制
            if (substr($msg[1], 2, 2) == 'fe') {
                $len = substr($msg[1], 4, 4);
                $len = hexdec($len);
                $msg[1] = substr($msg[1], 4);
            } else {
                if (substr($msg[1], 2, 2) == 'ff') {
                    $len = substr($msg[1], 4, 16);
                    $len = hexdec($len);
                    $msg[1] = substr($msg[1], 16);
                }
            }
            $mask[] = hexdec(substr($msg[1], 4, 2));
            $mask[] = hexdec(substr($msg[1], 6, 2));
            $mask[] = hexdec(substr($msg[1], 8, 2));
            $mask[] = hexdec(substr($msg[1], 10, 2));
            $s = 12;
            $n = 0;
        } else {
            if ($this->slen[$key] > 0) {
                $len = $this->slen[$key];
                $mask = $this->ar[$key];
                $n = $this->n[$key];
                $s = 0;
            }
        }

        $e = strlen($msg[1]) - 2;
        for ($i = $s; $i <= $e; $i += 2) {
            $data .= chr($mask[$n % 4] ^ hexdec(substr($msg[1], $i, 2)));
            $n++;
        }
        $dlen = strlen($data);

        if ($len > 255 && $len > $dlen + intval($this->sjen[$key])) {
            $this->ar[$key] = $mask;
            $this->slen[$key] = $len;
            $this->sjen[$key] = $dlen + intval($this->sjen[$key]);
            $this->sda[$key] = $this->sda[$key] . $data;
            $this->n[$key] = $n;

            return false;
        } else {
            unset($this->ar[$key], $this->slen[$key], $this->sjen[$key], $this->n[$key]);
            $data = $this->sda[$key] . $data;
            unset($this->sda[$key]);

            return $data;
        }

    }

    //与uncode相对
    function code($msg)
    {
        $frame = array();
        $frame[0] = '81';
        $len = strlen($msg);
        if ($len < 126) {
            $frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
        } else {
            if ($len < 65025) {
                $s = dechex($len);
                $frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s;
            } else {
                $s = dechex($len);
                $frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s;
            }
        }
        $frame[2] = $this->ord_hex($msg);
        $data = implode('', $frame);

        return pack("H*", $data);
    }

    function ord_hex($data)
    {
        $msg = '';
        $l = strlen($data);
        for ($i = 0; $i < $l; $i++) {
            $msg .= dechex(ord($data{$i}));
        }

        return $msg;
    }

    //用户加入或client发送信息
    function send($k, $msg)
    {
        //将查询字符串解析到第二个参数变量中,以数组的形式保存如:parse_str("name=Bill&age=60",$arr)
        parse_str($msg, $g);
        $ar = array();


        if ($g['type'] == 'add') {
            //第一次进入添加聊天名字,把姓名保存在相应的users里面
            $this->users[$k]['name'] = $g['ming'];
            $this->users[$k]['zhuangtai'] = $g['zhuangtai1'];//是否在线状态
            $ar['type'] = 'add';
            $ar['name'] = $g['ming'];
            $key = 'all';
        } else {
            //发送信息行为,其中$g['key']表示面对大家还是个人,是前段传过来的信息
            $ar['nrong'] = $g['nr'];
            $key = $g['key'];


        }
        //推送信息
        $this->send1($k, $ar, $key);
        //创建本地记录
        if ($g['key']) {
            $myfile = fopen("newfile.txt", "a+") or die("Unable to open file!");
            $userMessage = $k . "-" . $g['nr'] . "-" . $g['key'] . "\r\n";
            fwrite($myfile, $userMessage);
            fclose($myfile);
        }

    }

    //对新加入的client推送已经在线的client
    function getusers()
    {
        $ar = array();
        foreach ($this->users as $k => $v) {
            $ar[] = array('code' => $k, 'name' => $v['name'], 'zhuangtai' => $v['zhuangtai']);
        }

        return $ar;
    }

    //$k 发信息人的socketID $key接受人的 socketID ,根据这个socketID可以查找相应的client进行消息推送,即指定client进行发送
    function send1($k, $ar, $key = 'all')
    {
        $ar['code1'] = $key;
        $ar['code'] = $k;
        $ar['time'] = date('m-d H:i:s');
        $ar['users'] = $this->getusers();
        //对发送信息进行编码处理
        $str = $this->code(json_encode($ar));


        //面对大家即所有在线者发送信息
        if ($key == 'all') {
            $users = $this->users;
            //如果是add表示新加的client
            if ($ar['type'] == 'add') {
                $ar['type'] = 'madd';
                $ar['users'] = $this->getusers();


                //取出所有在线者,用于显示在在线用户列表中
                $str1 = $this->code(json_encode($ar)); //单独对新client进行编码处理,数据不一样
                //对新client自己单独发送,因为有些数据是不一样的
                socket_write($users[$k]['socket'], $str1, strlen($str1));
                //上面已经对client自己单独发送的,后面就无需再次发送,故unset
                unset($users[$k]);
            }
            //除了新client外,对其他client进行发送信息。数据量大时,就要考虑延时等问题了
            if ($ar['type'] == 'remove') {


            } else {
                foreach ($users as $v) {
                    if ($v['socket']) {
                        socket_write($v['socket'], $str, strlen($str));
                    }
                }
            }
        } else {
            //单独对个人发送信息,即双方聊天
            socket_write($this->users[$k]['socket'], $str, strlen($str));
            if ($this->users[$key]['socket']) {
                socket_write($this->users[$key]['socket'], $str, strlen($str));
            }


        }
    }

    //用户退出向所用client推送信息
    function send2($k)
    {
        $this->close($k);
        $ar['type'] = 'rmove';
        $ar['nrong'] = $k;
        // 设置离线
        $this->users[$k]['zhuangtai'] = 'lixian';
        $this->send1(false, $ar, 'all');
    }

    //记录日志
    function e($str)
    {
        //$path=dirname(__FILE__).'/log.txt';
        $str = $str . "\n";
        //error_log($str,3,$path);
        //编码处理
        echo iconv('utf-8', 'gbk//IGNORE', $str);
    }
}

//地址与接口,即创建socket时需要服务器的IP和端口
$sk = new Sock('127.0.0.1', 8000);
//对创建的socket循环进行监听,处理数据
$sk->run();

转载于:https://my.oschina.net/chinaliuhan/blog/3063752

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值