PHP socket编程


socket编程


什么是TCP/IP

TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。

UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。

TCP/IP协议族包括运输层、网络层、链路层。

什么是Socket

我们没有看到Socket的影子,那么它到底在哪里呢?

原来Socket在这里。

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式, 它把复杂的TCP /IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。 Socket使网络间的通信编程简单了许多。

什么是HTTP

你可能会好奇HTTP,HTTP是什么?

HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传送协议。

HTTP的发展是万维网协会(World Wide Web Consortium)和Internet工作小组IETF(Internet Engineering Task Force)合作的结果, 最终发布了一系列的RFC,RFC 1945定义了HTTP/1.0版本。其中最著名的就是RFC 2616。RFC 2616定义了今天普遍使用的一个版本——HTTP 1.1。

HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。HTTP是一个无状态的协议。

HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。

默认HTTP的端口号为80,HTTPS的端口号为443。

HTTP协议永远都是客户端发起请求,服务器回送响应。无法实现在客户端没有发起请求的时候,服务器将消息推送给客户端。

HTTP协议是一个无状态的协议,同一个客户端的这次请求和上次请求没有对应关系。

Socket编程的工作原理

Socket模型:

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待 客户端连接。 在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求, 服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

socket相关函数

   
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编程案例

服务器端

<?php
/*
 +-------------------------------
 *    @socket通信整个过程
 +-------------------------------
 *    @socket_create
 *    @socket_bind
 *    @socket_listen
 *    @socket_accept
 *    @socket_read
 *    @socket_write
 *    @socket_close
 +--------------------------------
 */

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

$ip = '127.0.0.1';
$port = 1935;

/*----------------    以下操作都是手册上的    -------------------*/
if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) < 0) {
    echo "socket_create() 失败的原因是:" . socket_strerror($sock) . "\n";
}

if (($ret = socket_bind($sock, $ip, $port)) < 0) {
    echo "socket_bind() 失败的原因是:" . socket_strerror($ret) . "\n";
}

if (($ret = socket_listen($sock, 4)) < 0) {
    echo "socket_listen() 失败的原因是:" . socket_strerror($ret) . "\n";
}

$count = 0;

do {
    if (($msgsock = socket_accept($sock)) < 0) {
        echo "socket_accept() failed: reason: " . socket_strerror($msgsock) . "\n";
        break;
    } else {

        //发到客户端
        $msg = "测试成功!\n";
        socket_write($msgsock, $msg, strlen($msg));

        echo "测试成功了啊\n";
        $buf = socket_read($msgsock, 8192);


        $talkback = "收到的信息:$buf\n";
        echo $talkback;

        if (++$count >= 5) {
            break;
        };


    }
    //echo $buf;
    socket_close($msgsock);

} while (true);

socket_close($sock);

使用 php server.php 执行脚本后,服务端的程序已经开始运行,端口已经开始监听了。运行 netstat -ano 可以查看端口情况,这里是1935端口

此时端口已经处于LISTENING状态了。接下来我们只要运行客户端程序即可连接上。

客户端

<?php
/*
 +-------------------------------
 *    @socket连接整个过程
 +-------------------------------
 *    @socket_create
 *    @socket_connect
 *    @socket_write
 *    @socket_read
 *    @socket_close
 +--------------------------------
 */
error_reporting(E_ALL);
set_time_limit(0);
echo "<h2>TCP/IP Connection</h2>\n";

$port = 1935;
$ip = "127.0.0.1";

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket < 0) {
    echo "socket_create() failed: reason: " . socket_strerror($socket) . "\n";
} else {
    echo "OK.\n";
}

echo "试图连接 '$ip' 端口 '$port'...\n";
$result = socket_connect($socket, $ip, $port);
if ($result < 0) {
    echo "socket_connect() failed.\nReason: ($result) " . socket_strerror($result) . "\n";
} else {
    echo "连接OK\n";
}

$in = "Ho\r\n";
$in .= "first blood\r\n";
$out = '';

if (!socket_write($socket, $in, strlen($in))) {
    echo "socket_write() failed: reason: " . socket_strerror($socket) . "\n";
} else {
    echo "发送到服务器信息成功!\n";
    echo "发送的内容为:<font color='red'>$in</font> <br>";
}

while ($out = socket_read($socket, 8192)) {
    echo "接收服务器回传信息成功!\n";
    echo "接受的内容为:", $out;
}


echo "关闭SOCKET...\n";
socket_close($socket);
echo "关闭OK\n";

运行看一下效果:

过程代码详解

服务端

<?php
// 设置一些基本的变量
$host = "192.168.1.99";
$port = 1234;
// 设置超时时间
set_time_limit(0);
// 创建一个Socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0) or die("Could not createsocket\n");
//绑定Socket到端口
$result = socket_bind($socket, $host, $port) or die("Could not bind tosocket\n");
// 开始监听链接
$result = socket_listen($socket, 3) or die("Could not set up socketlistener\n");
// accept incoming connections
// 另一个Socket来处理通信
$spawn = socket_accept($socket) or die("Could not accept incomingconnection\n");
// 获得客户端的输入
$input = socket_read($spawn, 1024) or die("Could not read input\n");
// 清空输入字符串
$input = trim($input);
//处理客户端输入并返回结果
$output = strrev($input) . "\n";
socket_write($spawn, $output, strlen($output)) or die("Could not write output\n");
// 关闭sockets
socket_close($spawn);
socket_close($socket);

下面是其每一步骤的详细说明:

1.第一步是建立两个变量来保存Socket运行的服务器的IP地址和端口.你可以设置为你自己的服务器和端口(这个端口可以是1到65535之间的数字), 前提是这个端口未被使用.

// 设置两个变量
$host = "192.168.1.99";
$port = 1234;

2.在服务器端可以使用set_time_out()函数来确保PHP在等待客户端连接时不会超时.

// 超时时间
set_time_limit(0);

3.在前面的基础上,现在该使用socket_creat()函数创建一个Socket了—这个函数返回一个Socket句柄,这个句柄将用在以后所有的函数中.

// 创建Socket
$socket = socket_create(AF_INET, SOCK_STREAM, 0) or die("Could not create socket\n");

原型:

socket_create ( int $domain , int $type , int $protocol ) : resource

创建并返回一个套接字,也称作一个通讯节点。一个典型的网络连接由 2 个套接字构成,一个运行在客户端,另一个运行在服务器端。

第一个参数$domain指定哪个协议用在当前套接字上:

Domain 描述
AF_INET IPv4 网络协议。TCP 和 UDP 都可使用此协议。
AF_INET6 IPv6 网络协议。TCP 和 UDP 都可使用此协议。
AF_UNIX 本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。

第二个参数$type用于选择套接字使用的类型:

类型 描述
SOCK_STREAM 提供一个顺序化的、可靠的、全双工的、基于连接的字节流。支持数据传送流量控制机制。TCP 协议即基于这种流式套接字。
SOCK_DGRAM 提供数据报文的支持。(无连接,不可靠、固定最大长度).UDP协议即基于这种数据报文套接字。
SOCK_SEQPACKET 提供一个顺序化的、可靠的、全双工的、面向连接的、固定最大长度的数据通信;数据端通过接收每一个数据段来读取整个数据包。
SOCK_RAW 提供读取原始的网络协议。这种特殊的套接字可用于手工构建任意类型的协议。一般使用这个套接字来实现 ICMP 请求(例如 ping)。
SOCK_RDM 提供一个可靠的数据层,但不保证到达顺序。一般的操作系统都未实现此功能。

第三个参数$protocol是设置指定 domain 套接字下的具体协议。 这个值可以使用 getprotobyname() 函数进行读取。如果所需的协议是 TCP 或 UDP,可以直接使用常量 SOL_TCP 和 SOL_UDP 。

常见协议:

名称 描述
icmp Internet Control Message Protocol 主要用于网关和主机报告错误的数据通信。例如“ping”命令(在目前大部分的操作系统中)就是使用 ICMP 协议实现的。
udp User Datagram Protocol 是一个无连接的、不可靠的、具有固定最大长度的报文协议。由于这些特性,UDP 协议拥有最小的协议开销。
tcp Transmission Control Protocol 是一个可靠的、基于连接的、面向数据流的全双工协议。TCP 能够保障所有的数据包是按照其发送顺序而接收的。如果任意数据包在通讯时丢失,TCP 将自动重发数据包直到目标主机应答已接收。因为可靠性和性能的原因,TCP 在数据传输层使用 8bit 字节边界。因此,TCP 应用程序必须允许传送部分报文的可能。

php预定义的一些协议常量:

define ('SOCKET_EMEDIUMTYPE', 124);
define ('IPPROTO_IP', 0);
define ('IPPROTO_IPV6', 41);
define ('SOL_TCP', 6);
define ('SOL_UDP', 17);
define ('IPV6_UNICAST_HOPS', 16);
define ('IPV6_RECVPKTINFO', 49);
define ('IPV6_PKTINFO', 50);
define ('IPV6_RECVHOPLIMIT', 51);
define ('IPV6_HOPLIMIT', 52);
define ('IPV6_RECVTCLASS', 66);
define ('IPV6_TCLASS', 67);
define ('SCM_RIGHTS', 1);
define ('SCM_CREDENTIALS', 2);
define ('SO_PASSCRED', 16);

因此,如果你想创建一个UDP Socket的话,你可以使用如下的代码:

// 创建 socket
$socket = socket_create(AF_INET, SOCK_DGRAM, 0) or die("Could not create socket\n");

4.一旦创建了一个Socket句柄,下一步就是指定或者绑定它到指定的地址和端口.这可以通过socket_bind()函数来完成.

// 绑定 socket to 指定地址和端口
$result = socket_bind($socket, $host, $port) or die("Could not bind to socket\n");

5.当Socket被创建好并绑定到一个端口后,就可以开始监听外部的连接了.PHP允许你由socket_listen()函数来开始一个监听, 同时你可以指定一个数字(在这个例子中就是第二个参数:3)

// 开始监听连接
$result = socket_listen($socket, 3) or die("Could not set up socket listener\n");

6.到现在,你的服务器除了等待来自客户端的连接请求外基本上什么也没有做.一旦一个客户端的连接被收到,socket_accept()函数便开始起作用了, 它接收连接请求并调用另一个子Socket来处理客户端–服务器间的信息.

//接受请求链接
// 调用子socket 处理信息
$spawn = socket_accept($socket) or die("Could not accept incoming connection\n");

这个子socket现在就可以被随后的客户端–服务器通信所用了。

7.当一个连接被建立后,服务器就会等待客户端发送一些输入信息,这些信息可以由socket_read()函数来获得,并把它赋值给PHP的$input变量。

// 读取客户端输入
$input = socket_read($spawn, 1024) or die("Could not read input\n");

socker_read的第二个参数用以指定读入的字节数,你可以通过它来限制从客户端获取数据的大小。

注意:socket_read函数会一直读取客户端数据,直到遇见\n,\t或者\0字符.PHP脚本把这写字符看做是输入的结束符。

8.现在服务器必须处理这些由客户端发来是数据(在这个例子中的处理仅仅包含数据的输入和回传到客户端)。 这部分可以由socket_write()函数来完成(使得由通信socket发回一个数据流到客户端成为可能)

// 处理客户端输入并返回数据
$output = strrev($input) . "\n";
socket_write($spawn, $output, strlen ($output)) or die("Could not write output\n");

9.一旦输出被返回到客户端,父/子socket都应通过socket_close()函数来终止

// 关闭 sockets
socket_close($spawn);

socket_close($socket);

客户端socket

<?php
if (!($socket = socket_create(AF_INET, SOCK_STREAM, 0))) {
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("Couldn't create socket");
}

if (!socket_connect($socket, '104.193.88.77', 80)) {
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("connect failed !");
}

$message = "GET / HTTP/1.1\r\n\r\n";
if (!socket_send($socket, $message, strlen($message), 0)) {
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("send failed !");
}

if (socket_recv($socket, $buf, 2045, MSG_WAITALL) === false) {
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("receive failed !");
}
echo $buf;

socket_close($socket);

1.创建socket

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

该函数返回socket描述符,三个参数分别是:

  • 地址协议:AF_INET (这里是ipv4)
  • 连接接类型:SOCK_STREAM(面向连接的TCP协议)
  • 协议:0|IPPROTO_IP (IP协议)

【错误处理】 如果任何socket函数失败,可以使用 socket_last_errr 和 socket_strerror 函数检索错误信息

if(!($socket = socket_create(AF_INET,SOCK_STREAM,0))){
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("Couldn't create socket");
}

【注意】 除了 SOCK_STREAM 类型的socket协议之外, 还有 SOCK_DGRAM 的类型 表示 UDP协议,因此需要知道远程服务器的 ip 地址

2.连接到服务器

if(!socket_connect($socket,'104.193.88.77',80)){
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("connect failed !");
}

到这里,我们运行 $ php socket.php 就能检测 ip 为104.193.88.77 端口号为 80 是否开启,由此可以构建一个端口扫描程序。

注意:这里连接必须是一个ip地址,有时候我们只知道域名,可以使用如下方式转换:

$ip_address = gethostbyname('www.baidu.com');

连接的概念适用于 SOCK_STREAM/TCP ,而 UDP,ICMP,ARP等其他基于非连接的通信没有连接的概念

3.发送数据

函数 send 只会发送数据,它需要socket描述符,发送的数据及其大小。

$message = "GET / HTTP/1.1\r\n\r\n";
if(!socket_send($socket,$message,strlen($message),0)){
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("send failed !");
}

该消息实际上是一个获取网站主页的http命令

【注意】 将数据发送到socket时,您基本上是将数据写入socket 类似于将数据写入文件,因此还可以使用 write 函数 将数据发送到socket。

4.接收数据

使用 recv 函数接收套接字上的数据

if(socket_recv($socket,$buf,2045,MSG_WAITALL) === false){
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("receive failed !");
}
echo $buf;

5.关闭socket

socket_close($socket);

总结一下:客户端发起socket请求流程

  • 》创建socket
  • 》连接到远程服务器
  • 》发送数据
  • 》接收数据
  • 》关闭socket

其实就是类似于打开浏览器访问www.baidu.com一样的整个流程

服务端socket

1.创建一个服务端master socket

if(!($socket = socket_create(AF_INET,SOCK_STAREAM,0))){
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("create socket failed ");
}

2.绑定到地址(和端口)

函数 bind 可用于套接字绑定到特定的地址和端口,他需要一个类似 connect 函数的结构 服务socket、ip 和端口

if(!socket_bind($socket,'127.0.0.1',5001)){
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("bind socket failed ");
}

如果你想接收所有的 ip 可以将 ip 改为 0.0.0.0

3.监听连接

socket_listen($socket, 10);

第二个参数是控制程序在已经很忙碌的时候保持等待的传入连接数,比如 已经有10个连接等待处理,第11个连接请求将会被拒绝

4.接受连接,关闭连接

$client = socket_accept($socket);
if(socket_getpeername($client,$address,$port)){
    echo "";
}
socket_close($client);
socket_close($socket);

php server.php

此时该程序正在等待端口5001上的传入连接我们新开一个窗口

telnet localhost 5001

【注意】 socket_getpeername 函数用于获取有关通过特定socket连接到服务器的客户端详细信息

我们建立了连接,并立即关闭了他,这并没有什么实际的用处,那我们回复客户端,我们可以使用socket_write 函数向传入的套接字 写入内容

5.读取/发送数据

$input = socket_read($client,102400);
$response = "ok ... $input";
socket_write($client,$response);
socket_close($client);

在终端中运行

$ telnet localhost 5001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
happy
OK .. h
Connection closed by foreign host

我们收到回复之后,连接被立即关闭了,而像百度这样的服务器总是一直在接收传入的链接,这意味着服务器应该一直在 运行,所以我们想让自己的服务器一直运行,最简单的方法是将 accept 置于一个循环中以便他能一直接收传入的连接

6.在线服务器

$address = '0.0.0.0';
$port = '5001';
while(true){
    $client = socket_accept($socket);
    if(socket_getpeername($client,$address,$port)){
      echo ''; 
    }
    $input = socket_read($client, 1024000);
    $response = "OK .. $input";
    socket_write($client, $response);
}

这里我们将 accpet 加入了循环,但是每个发起的客户端,仍然没有有效的通信,他无法一次处理多个连接

7.多连接处理

为了处理每个连接,我们需要一个单独的处理代码来与主服务器接受连接一起运行。实现此目的的一种方法是使用线程。 主服务器程序接受连接并创建一个新线程来处理连接的通信,然后服务器返回以接受更多连接。但是php不直接支持线程。

select

另一种方法是使用 select 函数。select 函数基本上轮询或观察一组套接字用于某些事件,比如它是否可读,可写或有问题等等。 因此 select 函数可用于监视多个客户端并检查哪个客户端发送了一条消息。

<?php
error_reporting(~E_NOTICE);
set_time_limit(0);

$address = "0.0.0.0";
$port = 5000;
$max_clients = 10;
//1. 创建连接
if (!($sock = socket_create(AF_INET, SOCK_STREAM, 0))) {
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("Couldn't create socket: [$errorcode] $errormsg \n");
}

echo "Socket created \n";

// 绑定连接
if (!socket_bind($sock, $address, $port)) {
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);

    die("Could not bind socket : [$errorcode] $errormsg \n");
}

echo "Socket bind OK \n";

//监听
if (!socket_listen($sock, 10)) {
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);

    die("Could not listen on socket : [$errorcode] $errormsg \n");
}

echo "Socket listen OK \n";

echo "Waiting for incoming connections... \n";

//array of client sockets
$client_socks = array();

//array of sockets to read
$read = array();

//开始循环接收进来的连接和已经存在的连接
while (true) {
    //准备一个存储socket的数组
    $read = array();

    //第一个是主服务的 socket
    $read[0] = $sock;

    //然后添加存在的 client sockets
    for ($i = 0; $i < $max_clients; $i++) {
        if ($client_socks[$i] != null) {
            $read[$i + 1] = $client_socks[$i];
        }
    }

    //调用 select - 遇到错误 终止调用
    if (socket_select($read, $write, $except, null) === false) {
        $errorcode = socket_last_error();
        $errormsg = socket_strerror($errorcode);

        die("Could not listen on socket : [$errorcode] $errormsg \n");
    }

    //如果包含了主服务的 socket, 那么就可以就收新的客户端socket连接
    if (in_array($sock, $read)) {
        for ($i = 0; $i < $max_clients; $i++) {
            if ($client_socks[$i] == null) {
                $client_socks[$i] = socket_accept($sock);//接受连接

                //显示连接信息
                if (socket_getpeername($client_socks[$i], $address, $port)) {
                    echo "Client $address : $port is now connected to us. \n";
                }

                //发送语句给客户端
                $message = "Welcome to php socket server version 1.0 \n";
                $message .= "Enter a message and press enter, and i shall reply back \n";
                socket_write($client_socks[$i], $message);
                break;
            }
        }
    }

    //检查每个客户端是否有数据发送
    for ($i = 0; $i < $max_clients; $i++) {
        if (in_array($client_socks[$i], $read)) {
            $input = socket_read($client_socks[$i], 1024);//读取数据

            if ($input == null) {
                //如果输入为空 那么意味着客户端失去连接,关闭客户端并移除
                unset($client_socks[$i]);
                socket_close($client_socks[$i]);
            }

            $n = trim($input);

            $output = "OK ... $input";

            echo "Sending output to client \n";

            //发送回复信息给客户端
            socket_write($client_socks[$i], $output);
        }
    }
}

运行上述服务器并像以前一样打开3个终端。现在,服务器将为连接到它的每个客户端创建一个线程。

总结一下: 服务端创建一个socket服务需要如下几步

  • 》1. 打开socket
  • 》2. 绑定到地址(和端口)
  • 》3. 监听传入的连接
  • 》4. 接受数据
  • 》5. 读取数据/发送数据

关于 socket_select 部分内容可以看 https://ibaiyang.github.io/blog/php/2020/07/01/PHP-Socket初探.html#11-select系统调用

epoll

epoll实现的IO多路复用。php的event扩展支持epoll。安装好后看一下用法:

<?php
$host = '0.0.0.0';
$port = 9999;
$fd = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($fd, $host, $port);
socket_listen($fd);
// 注意,将“监听socket”设置为非阻塞模式
socket_set_nonblock($fd);

// 这里值得注意,我们声明两个数组用来保存 事件 和 连接socket
$event_arr = [];
$conn_arr = [];

echo PHP_EOL . PHP_EOL . "欢迎来到ti-chat聊天室!发言注意遵守当地法律法规!" . PHP_EOL;
echo "        tcp://{$host}:{$port}" . PHP_EOL;

$event_base = new EventBase();
$event = new Event($event_base, $fd, Event::READ | Event::PERSIST, function ($fd) {
    // 使用全局的event_arr 和 conn_arr
    global $event_arr, $conn_arr, $event_base;

    // 非阻塞模式下,注意accpet的写法会稍微特殊一些。如果不想这么写,请往前面添加@符号,不过不建议这种写法
    if (($conn = socket_accept($fd)) != false) {
        echo date('Y-m-d H:i:s') . ':欢迎' . intval($conn) . '来到聊天室' . PHP_EOL;

        // 将连接socket也设置为非阻塞模式
        socket_set_nonblock($conn);

        // 此处值得注意,我们需要将连接socket保存到数组中去
        $conn_arr[intval($conn)] = $conn;

        $event = new Event($event_base, $conn, Event::READ | Event::PERSIST, function ($conn) {
            global $conn_arr;
            $buffer = socket_read($conn, 65535);
            foreach ($conn_arr as $conn_key => $conn_item) {
                if ($conn != $conn_item) {
                    $msg = intval($conn) . '说 : ' . $buffer;
                    socket_write($conn_item, $msg, strlen($msg));
                }
            }
        }, $conn);

        $event->add();

        // 此处值得注意,我们需要将事件本身存储到全局数组中,如果不保存,连接会话会丢失,也就是说服务端和客户端将无法保持持久会话
        $event_arr[intval($conn)] = $event;
    }
}, $fd);

$event->add();
$event_base->loop();






参考资料

PHP 手册 函数参考 其它服务 Sockets Socket 函数 https://www.php.net/manual/zh/ref.sockets.php

PHP Socket初探 https://ibaiyang.github.io/blog/php/2020/07/01/PHP-Socket%E5%88%9D%E6%8E%A2.html

深入浅出讲解:php的socket通信 https://www.cnblogs.com/aipiaoborensheng/p/6708963.html

PHP中socket的使用入门 https://blog.csdn.net/wujiangwei567/article/details/81144852

PHP 手册 函数参考 其它服务 https://www.php.net/manual/zh/book.sockets.php

深入理解HTTP协议、HTTP协议原理分析 https://www.cnblogs.com/chenliyang/p/6558756.html

php socket简单原理及实现笔记 https://blog.csdn.net/u012728971/article/details/80685796

PHP socket初探 先从一个简单的socket服务器开始 https://segmentfault.com/a/1190000016226578

PHP常用技术(五)之socket的简单使用 https://www.cnblogs.com/qiye5757/p/9721401.html

socket,tcp,http三者之间的区别和原理 https://www.jianshu.com/p/a24ba459e306


返回