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();