期权记-ETF期权开户导航 期权记是专业的50ETF300ETF期权开户投资交易导航

PHP socket编程

PHP回顾系列目录

  • PHP基础
  • web请求
  • cookie
  • web响应
  • session
  • 数据库操作
  • 加解密
  • Composer
  • 创建自己的Composer包
  • 发送邮件
  • IO

web开发一直是PHP的主战场,也是PHP最为被世人所熟知的一面。其实只要你愿意去发掘,PHP除了做网页在许多其他方面也是小能手。

本文简要介绍PHP的Socket编程。

准备知识

在开始之前,希望你已经知道网络编程中的一些基本概念。比如OSI七层模型、TCP/IP四层模型;TCP中的三次握手、四次挥手等。这些概念是网络编程的理论基础,实践中不一定用得到,但能让你把握整体脉络,更快的定位编程中出现的问题。

再说一下Socket。我们常说的网络编程就是指Socket编程,它既指代实现了TCP/IP协议簇的一套网络编程API,也指代一个客户端与服务器的连接。socket是插座/接口的意思,计算机中常翻译成“套接字”。实际中可以简单的认为网络编程与Socket编程等价,一个tcp连接的说法等价于一个socket。

PHP中的API

PHP中有以socket开头的一套函数API用于Socket编程,PHP5引入“流”的抽象概念后,以stream开头的一套API也可以用于网络编程。两者的主要区别是:

  1. 流是PHP中的核心概念,所以stream开头的函数总是可用;sockets是PHP的一个拓展,虽然大部分情况下都默认启用;
  2. socket系列函数相对底层,而stream系列函数是高层的抽象。

如果你想体验原味Socket编程,用socket开头的API比较适合;否则建议使用流函数。有关流的知识,请参考本人之前的博文:PHP回顾之流。

接下来我们用流函数实现一个简单的TCP客户端和服务端。

客户端

客户端网络编程可以归结为简单的三步:

  1. 连接服务端(connect);
  2. 收发消息(receive/send);
  3. 关闭连接(close)。

下面是客户端的代码,发送10条消息到服务端:

// client.php $host = "127.0.0.1"; $port = 8000;  $socket = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errMsg); if ($socket === false) {     throw new \RuntimeException("unable to create socket: " . $errMsg); } fwrite(STDOUT, "success connect to server: [{$host}:{$port}]...\n");  foreach (range(1, 10) as $i) {     if ($i % 5 === 0) {         $method = "broadcast";     } else {         $method = "echo";     }     $args = [sprintf("The %dth greeting", $i)];     $message = json_encode([         "method" => $method,         "args" => $args,     ]);     fwrite(STDOUT, "\nsend to server: $message\n");     $len = @fwrite($socket, $message);     if ($len === 0) {         fwrite(STDOUT, "socket closed\n");         break;     }      $msg = @fread($socket, 4096);     if ($msg) {         fwrite(STDOUT, "receive server: $msg\n");     }     elseif (feof($socket)) {        fwrite(STDOUT, "socket closed\n");        break;     }      sleep(2); }  fwrite(STDOUT, "close connnection...\n"); fclose($socket); 

客户端已经搞定,接下来看服务端。

服务端

服务端编程也很简单,四步搞定:

  1. 监听端口(listen);
  2. 接受新连接(accept);
  3. 收发网络消息(receive/send);
  4. 循环第二步和第三步(loop)。

由于服务端一般是长时间运行,除非重启或进程被杀死,极少会主动关闭服务。另外服务端一般需要长时间运行,所以应当运行在CLI模式下(短连的客户端代码可以在web中使用,例如代替CURL获取网页内容,连接redis/MQ等)。

我们简单的将收到的消息返回客户端(Echo服务器):

// server.php $port = 8000; $socket = @stream_socket_server("tcp://0.0.0.0:$port", $errno, $errMsg); if ($socket === false) {     throw new \RuntimeException("fail to listen on port: {$port}!"); } fwrite(STDOUT, "socket server listen on port: {$port}" . PHP_EOL);  while (true) {     $client = @stream_socket_accept($socket);     if ($client == false) {         continue;     }      fwrite(STDOUT, "client:" . (int)$client . " connnected.\n");     @fwrite($client, "Welcome aboard!\n");      while (true) {         $msg = @fread($client, 4096);         if ($msg) {             fwrite(STDOUT, "\nreceive client: $msg\n");             // echo             @fwrite($client, $msg);         } elseif (feof($client)) {             fwrite(STDOUT, "client:" . (int)$client . " disconnnect!\n");             fclose($client);             break;         }     } } 

先启动服务端脚本:php server.php, 然后打开新的窗口启动客户端:php client.php。可以看到消息被正确的发送和接收。客户端退出后,可多次重新运行客户端脚本查看效果。

并发

同时运行两个或以上客户端,会发现第二个起卡住,前面的客户端退出后才继续运行。回顾服务端代码,可以看到accept一个客户端后,服务端就专心为其服务,直到断开才服务下一个。

同时服务多个客户端,这才是我们期望的。默认情况下socket处于阻塞模式,无数据时fread函数会一直等待,导致程序不能抽身服务其他客户端。要同时服务多个客户端,第一步是设置非阻塞模式,第二步是更改轮询方式。流函数中的stream_set_blockingstream_select两个函数是我们想要的。

将服务端的代码更改如下:

// server.php <?php $port = 8000; $socket = @stream_socket_server("tcp://0.0.0.0:$port", $errno, $errMsg); if ($socket === false) {     throw new \RuntimeException("fail to listen on port: {$port}!"); } fwrite(STDOUT, "socket server listen on port: {$port}" . PHP_EOL); stream_set_blocking($socket, false);  $clients = []; $changed = []; while (true) {     checkMessage();     fwrite(STDOUT, "\nnew read message\n");     accept();     handleMessage(); }  function checkMessage() {     global $socket, $changed, $clients;     $changed = array_merge([$socket], $clients);     $write = null;     $except = null;     stream_select($changed, $write, $except, null); }  function accept() {     global $socket, $changed, $clients;     if (!in_array($socket, $changed)) {         return;     }      while ($client = @stream_socket_accept($socket, 0)) {         $clients[] = $client;          fwrite(STDOUT, "client:" . (int)$client . " connnected.\n");         fwrite($client, "welcome aboard!");         stream_set_blocking($client, false);          $key = array_search($client, $changed);         unset($changed[$key]);     } }  function handleMessage() {     global $changed, $clients;     foreach ($changed as $key => $client) {         while (true) {             $msg = @fread($client, 4096);             if ($msg) {                 fwrite(STDOUT, "receive client " . (int)$client . " message: $msg\n");                 $json = json_decode($msg, true);                 if ($json) {                     $method = $json["method"];                     if ($method === 'echo') {                         @fwrite($client, $msg);                     } else {                         foreach ($clients as $cl) {                             @fwrite($cl, "message from " . (int)$client . ": $msg");                         }                     }                 }             } else {                 if (feof($client)) {                     fwrite(STDOUT, "\nclient " . (int)$client . " closed.\n");                     fclose($client);                     $key = array_search($client, $clients);                     unset($clients[$key]);                 }                 break;             }         }     } } 

然后启动服务端:php server.php,再同时启动多个客户端,或者用多个进程同时发送消息(需安装pcntl拓展):

// client.php for ($index = 0; $index < 10; ++ $index) {     $pid = pcntl_fork();     if ($pid < 0) {         fwrite(STDERR, "fail to fork!\n");         exit;     }      if ($pid === 0) {         connectServer();  // connectServer就是上文中client.php中的代码         exit;     } } // 父进程先退出,不会出现僵尸进程,忽略孤儿进程的处理 

启动客户端后,可以看到服务端正确的同时处理多个客户端,这正是我们期待的。

缺憾

上述代码实现了客户端和可并发的服务端,作为演示基本够用。如果要投入到实践中使用,至少有以下方面的不足:

  1. 多进程/多线程/协程缺失,除处理网络消息外,不能(难)做其他逻辑业务;
  2. 没有协议解析,会导致多条信息合并成一条读取(或者一条信息被拆成多条);
  3. select低效且有并发连接数目限制,客户端量大时需要poll/epoll等技术;

每个方面展开来说至少都是一篇长文。本文目的是简要介绍PHP中的Socket编程,行文到此已经达到目的。由于网络协议十分繁杂,想深入网络编程请参阅更多权威文档。

总结

本文基于PHP5引入的流简要介绍了PHP中的Socket编程,并给出了一个简单并发服务器的实现。文中代码仅做演示用,在生产环境中,请使用成熟的网络框架/库。

参考

  1. http://php.net/manual/en/book.sockets.php
  2. http://www.unixguide.net/network/socketfaq/
  3. http://php.net/manual/en/book.stream.php

本站声明:网站内容来源于网络,如有侵权,请联系我们https://www.qiquanji.com,我们将及时处理。

微信扫码关注

更新实时通知

作者:期权小韭菜 分类:网络教程 浏览:
请先 登录 再评论,若不是会员请先 注册