🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
IO 多路复用的 epoll 利器,epoll 是高性能程序的根基。PHP 中如何将 Socket 与 Event 结合使用的案例。这里的 Event 可以理解为是对 epoll 的高度封装,底层采用的就是 epoll 利器 这段代码就是提炼了 Workerman 对事件循环的实现原理。stream\_socket\_server 函数把创建、绑定、监听一并实现了,让代码显得更加简洁,不像之前的 socket\_create、socket\_bind、socket\_listen 搞了三个步骤略显繁琐。 因为使用了事件循环,所以需要对 Socket 设置成非阻塞模式,只有当有读或写的通知时才会调用相应的回调函数。还有一点需要额外注意的,需要针对客户端 Socket 创建的 Event 需要定义成静态变量或全局变量,不然无法持久化连接到内存,会造成客户端无法建立连接传输数据,我看到网上很多人都踩到了这个坑上。最后启动事件循环 EventLoop 自此开启了 Socket 监听和事件循环双操作 函数参考/其它服务/[Event](https://www.php.net/manual/zh/book.event.php) 函数参考/其它基本扩展/Stream/[Stream 函数](https://www.php.net/manual/zh/ref.stream.php) ``` // 创建 TCP 服务器套接字 $server = stream_socket_server("tcp://0.0.0.0:8080", $errno, $error); echo "正在监听 8080 端口...". PHP_EOL; // 设置为非阻塞,在 $server 对象没有数据可以读取或写入时不会阻塞其执行 stream_set_blocking($server, 0); // 创建事件基础对象 $event_base = new EventBase(); // 建立事件监听服务端 Socket 可读事件 public Event::__construct( EventBase $base ,//要关联的事件库 mixed $fd ,//流资源、套接字资源或数字文件描述符。计时器事件传递-1。对于信号事件,请传递信号编号,例如SIGHUP。 int $what ,//事件标志。请参阅Event flags callable $cb ,//事件回调。查看Event callbacks mixed $arg = NULL//自定义数据。如果指定,它将在事件触发时传递给回调 ); $event = new Event($event_base, $server, Event::READ | Event::PERSIST, function ($server) use ($event_base) { // 获取新的连接,由于设置了非阻塞模式,那么这里即使没有新的连接,也不会一直阻塞在这 $client = @stream_socket_accept($server, 0); if ($client) { echo "客户端(" . $client . ")连接建立". PHP_EOL; // 针对客户端过来的连接,也要设置成非阻塞模式 stream_set_blocking($client, 0); // 客户端连接创建监听可读事件 // 这里需要特别注意:客户端事件需要定义成静态变量或全局变量 static $client_event; $client_event = new Event($event_base, $client, Event::READ | Event::PERSIST, function ($client) { // 从客户端连接中读取数据,每次只读取 1024 字节数据 $buffer = fread($client, 1024); // 如果没有读取到数据或者客户端已经不是资源句柄,则关闭客户端连接 if ($buffer == false || !is_resource($client)) { // 关闭客户端连接 fclose($client); echo "客户端(" . $client . ")连接关闭" . PHP_EOL; return; } echo "收到客户端(" . $client . ")数据: $buffer" . PHP_EOL; // 回写数据给客户端 $msg = "HTTP/1.0 200 OK\r\nContent-Length: 10\r\n\r\nServerOK\r\n"; fwrite($client, $msg); }, $client); $client_event->add(); } }, $server); // 添加事件 $event->add(); // 执行事件循环(上面设置非阻塞模式,只有当socket读取或写入时触发回调函数) $event_base->loop(); ``` 使用 CURL 工具访问http://127.0.0.1:8080便能正确返回结果 ServerOK 这表明事件循环可以进入正常运行状态。 ~~~ Copy[manongsen@root php_event]$ curl -i http://127.0.0.1:8080 HTTP/1.0 200 OK Content-Length: 10 ServerOK ~~~ 下面这段代码是引至 Workerman 的示例,通过 Worker 类构造了一个 HTTP 服务。onMessage 参数定义了一个回调函数,当有事件通知时,会回调到此处,之后就是用户自行实现后续的处理逻辑了。runAll 函数会整体启动整个服务,其中包括进程的创建、事件的循环等 ~~~ // 引用 Worker 类 use Workerman\Worker; // 自动加载 Composer require_once __DIR__ . '/vendor/autoload.php'; // 定义 HTTP 服务并监听 8081 端口 $http_worker = new Worker('http://0.0.0.0:8081'); // 定义回调函数 $http_worker->onMessage = function ($connection, $request) { //$request->get(); //$request->post(); //$request->header(); //$request->cookie(); //$request->session(); //$request->uri(); //$request->path(); //$request->method(); // Send data to client $connection->send("Hello World"); }; // 启动服务 Worker::runAll(); ~~~ 在 Worker.php 文件的 2367 行,使用 stream\_socket\_server 函数创建了服务端 Socket 并且绑定、监听了 8081 端口。 ~~~ Copy// workerman/Worker.php:2367 $this->_mainSocket = \stream_socket_server($local_socket, $errno, $errmsg, $flags, $this->_context); ~~~ 在 Worker.php 文件的 2394 行,使用 stream\_set\_blocking 函数将 服务端 Socket 设置成非阻塞模式。 ~~~ Copy// workerman/Worker.php:2394 \stream_set_blocking($this->_mainSocket, false); ~~~ 在 Worker.php 文件的 2417 行,将服务端的 \_mainSocket 添加到事件循序中,并且设置回调函数为 acceptConnection 。 ~~~ Copy// workerman/Worker.php:2417 static::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptConnection')); ~~~ 在 Worker.php 文件的 2561 行,使用 stream\_socket\_accept 接收到来自客户端的连接 $new\_socket ,其中这个操作是在 acceptConnection 回到函数中所进行的。 ~~~ Copy// workerman/Worker.php:2561 $new_socket = \stream_socket_accept($socket, 0, $remote_address); ~~~ 在 TcpConnection.php 文件的 285 行,使用 stream\_set\_blocking 函数将客户端的 \_socket 设置成非阻塞模式,这里的 \_socket 和上面的 new\_socket 是同一个。 ~~~ Copy// workerman/Connection/TcpConnection.php:285 \stream_set_blocking($this->_socket, 0); ~~~ 在 TcpConnection.php 文件的 290 行,将客户端的 \_socket 添加到事件循环中,并且设置其的回调函数为 baseRead 。 ~~~ Copy// workerman/Connection/TcpConnection.php:290 Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); ~~~ 在 Worker.php 文件的 1638 行,启动事件循环。 ~~~ Copy// workerman/Worker.php:1638 static::$globalEvent->loop(); ~~~ 启动事件循环后,当有客户端连接时便可以读取数据了。因此在 TcpConnection.php 文件的 583 行,使用 fread 函数读取客户端 $socket 的数据。 ~~~ Copy// workerman/Connection/TcpConnection.php:583 $buffer = @\fread($socket, self::READ_BUFFER_SIZE); ~~~ 在 TcpConnection.php 文件的 647 行,使用 parser::decode 函数将上面读取到的 buffer 数据解析成 $request 对象,还有 $this 表示的是 $connection 对象,这个 $this->onMessage 是最开始用户自定义的回调函数。最终通过 call\_user\_func 函数,将 $connection、$request 参数回调到 onMessage 方法。 ~~~ Copy// workerman/Connection/TcpConnection.php:647 \call_user_func($this->onMessage, $this, $parser::decode($one_request_buffer, $this)); ~~~ 最后我们使用 CURL 工具调用一下[http://127.0.0.1:8081](http://127.0.0.1:8081/)通过返回的数据,可以看出正确的回调到了 onMessage 函数。 ~~~ Copy[manongsen@root workerman]$ curl -i http://127.0.0.1:8081 HTTP/1.1 200 OK Server: workerman Connection: keep-alive Content-Type: text/html;charset=utf-8 Content-Length: 13 Hello World ~~~ 看到这里相信你已经对 Workerman 源码中的事件循环有些了解了,如果有时间最好能够实践下最开始的那段案例代码,然后再结合着看 Workerman 的源代码会颇有收获。Workerman 的高性能是站在了巨人 epoll 的肩膀上来实现,没有了 epoll 则啥也不是。这里再重申一下 PHP 中的 Event 是对 epoll 的封装,epoll 是 Linux 的底层技术。我们在日常的编程中是不会直接接触到 epoll 的,最后回归一下主题 epoll 技术才是 Workerman 的立命之本 [这才是 PHP 高性能框架 Workerman 的立命之本 - Yxh\_blogs - 博客园](https://www.cnblogs.com/yxhblogs/p/18319851)