php实现web服务器

【php实现web服务器】使用php(非swoole)实现tcp/http服务器。
php内置的stream系列函数 和 socket扩展提供了对网络编程的支持。socket扩展需要在编译时通过配置--enable-sockets开启,而strem系列函数则完全是php核心内置的函数。php社区中的workman框架底层就是基于stream函数来实现的。以下代码通过stream系列函数演示php如何实现简单的tcp/http等服务器
1:单进程阻塞模式

socket = @stream_socket_server($address, $errNo, $errMessage); if (!is_resource($this->socket)) { throw new InvalidArgumentException("参数异常:" . $errMessage); } }public function isHttp(bool $bool): self { $this->isHttp = $bool; return $this; }/** * 启动服务器 */ public function run() { while (true) { $conn = @stream_socket_accept($this->socket); if ($conn) { if ($this->isHttp) { //http server $data= "https://www.it610.com/article/Hello World"; $response = "HTTP/1.1 200 OK\r\n"; $response .= "Content-Type: text/html; charset=UTF-8\r\n"; $response .= "Server: MyServer1\r\n"; $response .= "Content-length: " . strlen($data) . "\r\n\r\n"; $response .= $data; fwrite($conn, $response); fclose($conn); } else { // tcp server while ($message = fread($conn, 1024)) { // 主动退出 if (trim($message) == 'quit') { echo "close\n"; fclose($conn); break; } echo 'I have received that : ' . $message; fwrite($conn, "OK\n"); } } } } } }$server = new Server("0.0.0.0:2345"); // 启动 tcp 服务器 $server->run(); // 启动 http服务器 //$server->isHttp(true)->run();

http服务浏览器访问http://127.0.0.1:2345/看可以看到输出hello world
php实现web服务器
文章图片

tcp服器通过telnet进行测试
由于连接和read都是阻塞的,此时一次只能处理一个请求,只能当当前请求处理结束后才能处理下一个请求,不具备并发能力。
2:多进程改造
socket = @stream_socket_server($address, $errNo, $errMessage); if (!is_resource($this->socket)) { throw new InvalidArgumentException("参数异常:" . $errMessage); } }public function isHttp(bool $bool): self { $this->isHttp = $bool; return $this; }/** * 启动服务器 */ public function run() { while (true) { $conn = @stream_socket_accept($this->socket); if ($conn) { if ($this->isHttp) { if (pcntl_fork() == 0) { //http server $data= "https://www.it610.com/article/Hello World"; $response = "HTTP/1.1 200 OK\r\n"; $response .= "Content-Type: text/html; charset=UTF-8\r\n"; $response .= "Server: MyServer1\r\n"; $response .= "Content-length: " . strlen($data) . "\r\n\r\n"; $response .= $data; fwrite($conn, $response); fclose($conn); exit; } } else { // tcp server if (pcntl_fork() == 0) { while ($message = fread($conn, 1024)) { echo 'I have received that : ' . $message; fwrite($conn, "OK\n"); } exit; } } } } } }$server = new Server("0.0.0.0:2345"); // 启动 tcp 服务器 $server->run(); // 启动 http服务器 //$server->isHttp(true)->run();

此时,由于通过子进程处理原本阻塞的read函数,所以主进程可以accept新的请求,提高并发能力。
php实现web服务器
文章图片

由于创建进程/线程会带来很多新的问题,比如系统开销增大等,所以,这种方法基本不会在生产环境中使用。
3:IO多路复用(select方式)
socket = @stream_socket_server($address, $errNo, $errMessage); if (!is_resource($this->socket)) { throw new InvalidArgumentException("参数异常:" . $errMessage); } // stream_set_blocking 设置非阻塞io stream_set_blocking($this->socket, false); // 添加当前监听的socket $this->socketList[(int)$this->socket] = $this->socket; }public function isHttp(bool $bool): self { $this->isHttp = $bool; return $this; }/** * 启动服务器 */ public function run() { while (true) { $read= $this->socketList; $write = $except = []; //阻塞监听 设置null阻塞监听 $count = @stream_select($read, $write, $except, null); if ($count) { foreach ($read as $k => $v) { if ($v == $this->socket) { // connect success // accept Connection @$conn = stream_socket_accept($this->socket, null, $remote_address); echo "connect fd:" . $k . PHP_EOL; $this->socketList[(int)$conn] = $conn; } else { //receive if ($this->isHttp) { $this->receiveHttp($v); } else { $this->receiveTcp($v); } } } } } }protected function receiveHttp($stream) { $buffer = fread($stream, 1024); if (empty($buffer) && (feof($stream) || !is_resource($stream))) { fclose($stream); unset($this->socketList[(int)$stream]); echo "退出成功" . PHP_EOL; return; } //http server $data= "https://www.it610.com/article/Hello World"; $response = "HTTP/1.1 200 OK\r\n"; $response .= "Content-Type: text/html; charset=UTF-8\r\n"; $response .= "Server: MyServer1\r\n"; $response .= "Content-length: " . strlen($data) . "\r\n\r\n"; $response .= $data; fwrite($stream, $response); }/** * 处理tcp请求 */ private function receiveTcp($stream) { $buffer = fread($stream, 1024); if (feof($stream) || !is_resource($stream)) { unset($this->socketList[(int)$stream]); fclose($stream); echo "退出成功" . PHP_EOL; return; } echo 'onReceive ' . $buffer . " ->" . $stream . PHP_EOL; $message = 'I have received that : ' . $buffer; fwrite($stream, "{$message}"); } }$server = new Server("0.0.0.0:2345"); // 启动 tcp 服务器 //$server->run(); // 启动 http服务器 $server->isHttp(true)->run();

通过IO多路复用是目前解决高并发(C10k)问题的主要思路。
通过stream_select函数调用操作系统提供的select方法,将接收到的文件描述符号传递给操作系统底层,由操作系统遍历并且返回需要处理的文件描述符号,从而不需要在应用层面一直阻塞,提高并发能力。
4:IO多路费用(poll epoll)
由于select方法存在种种问题,比如性能差,数量限制等。应对小规模并发没有问题,但是大规模并发就显得捉襟见肘,这个时候就需要poll,epoll(推荐)方法实现多路复用。
具体到php里面需要安装event或者libevent扩展,详细代码可以参考workman源码,这里不再展示。
实例代码
  • https://github.com/tim1116/ph...
参考:
  • https://www.php.net/manual/zh... (php支持的Socket Transports)
  • https://www.php.net/manual/zh...
  • https://www.php.net/manual/zh...

    推荐阅读