PHP SOCKET编程

1. 预备知识php

       一直以来不多看到有多少人使用PHP的socket模块来作一些事情,大概你们都把它定位在脚本语言的范畴内吧,可是其实php的socket模块能够作不少事情,包括作ftplist,http post提交,smtp提交,组包并进行特殊报文的交互(如smpp协议),whois查询。这些都是比较常见的查询。node

特别是php的socket扩展库能够作的事情简直不会比c差多少。
php的socket链接函数
一、集成于内核的socket
这个系列的函数仅仅只能作主动链接没法实现端口监听相关的功能。并且在4.3.0以前全部socket链接只能工做在阻塞模式下。
此系列函数包括
fsockopen,pfsockopen
这两个函数的具体信息能够查询php.net的用户手册
他们均会返回一个资源编号对于这个资源可使用几乎全部对文件操做的函数对其进行操做如fgets(),fwrite(), fclose()等单注意的是全部函数遵循这些函数面对网络信息流时的规律,例如:
fread() 从文件指针 handle 读取最多 length 个字节。 该函数在读取完 length 个字节数,或到达 EOF 的时候,或(对于网络流)当一个包可用时就会中止读取文件,视乎先碰到哪一种状况。 
能够看出对于网络流就必须注意取到的是一个完整的包就中止。
二、php扩展模块带有的socket功能。
php4.x 之后有这么一个模块extension=php_sockets.dll,Linux上是一个extension=php_sockets.so。
当打开这个此模块之后就意味着php拥有了强大的socket功能,包括listen端口,阻塞及非阻塞模式的切换,multi-client 交互式处理等
这个系列的函数列表参看http://www.php.net/manual/en/ref.sockets.php
看过这个列表以为是否是很是丰富呢?不过很是遗憾这个模块还很是年轻还有不少地方不成熟,相关的参考文档也很是少:(
我也正在研究中,所以暂时不具体讨论它,仅给你们一个参考文章react

http://www.zend.com/pecl/tutorials/sockets.phplinux

 

2. 使用PHP socket扩展程序员

 

服务器端代码:编程

 

[php] 
数组

  1. <?php  缓存

  2. /** 安全

  3.  * File name server.php 服务器

  4.  * 服务器端代码 

  5.  *  

  6.  * @author guisu.huang 

  7.  * @since 2012-04-11 

  8.  *  

  9.  */  

  10.   

  11. //确保在链接客户端时不会超时  

  12. set_time_limit(0);  

  13. //设置IP和端口号  

  14. $address = "127.0.0.1";  

  15. $port = 2046; //调试的时候,能够多换端口来测试程序!  

  16. /** 

  17.  * 建立一个SOCKET  

  18.  * AF_INET=是ipv4 若是用ipv6,则参数为 AF_INET6 

  19.  * SOCK_STREAM为socket的tcp类型,若是是UDP则使用SOCK_DGRAM 

  20. */  

  21. $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() 失败的缘由是:" . socket_strerror(socket_last_error()) . "/n");  

  22. //阻塞模式  

  23. socket_set_block($sock) or die("socket_set_block() 失败的缘由是:" . socket_strerror(socket_last_error()) . "/n");  

  24. //绑定到socket端口  

  25. $result = socket_bind($sock, $address, $port) or die("socket_bind() 失败的缘由是:" . socket_strerror(socket_last_error()) . "/n");  

  26. //开始监听  

  27. $result = socket_listen($sock, 4) or die("socket_listen() 失败的缘由是:" . socket_strerror(socket_last_error()) . "/n");  

  28. echo "OK\nBinding the socket on $address:$port ... ";  

  29. echo "OK\nNow ready to accept connections.\nListening on the socket ... \n";  

  30. do { // never stop the daemon  

  31.     //它接收链接请求并调用一个子链接Socket来处理客户端和服务器间的信息  

  32.     $msgsock = socket_accept($sock) or  die("socket_accept() failed: reason: " . socket_strerror(socket_last_error()) . "/n");  

  33.       

  34.     //读取客户端数据  

  35.     echo "Read client data \n";  

  36.     //socket_read函数会一直读取客户端数据,直到碰见\n,\t或者\0字符.PHP脚本把这写字符看作是输入的结束符.  

  37.     $buf = socket_read($msgsock, 8192);  

  38.     echo "Received msg: $buf   \n";  

  39.       

  40.     //数据传送 向客户端写入返回结果  

  41.     $msg = "welcome \n";  

  42.     socket_write($msgsock, $msg, strlen($msg)) or die("socket_write() failed: reason: " . socket_strerror(socket_last_error()) ."/n");  

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

  44.     socket_close($msgsock);  

  45. while (true);  

  46. socket_close($sock);  

 

客户端代码:

 

[php] 

  1. <?php  

  2. /** 

  3.  * File name:client.php 

  4.  * 客户端代码 

  5.  *  

  6.  * @author guisu.huang 

  7.  * @since 2012-04-11 

  8.  */  

  9. set_time_limit(0);  

  10.   

  11. $host = "127.0.0.1";  

  12. $port = 2046;  

  13. $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)or die("Could not create  socket\n"); // 建立一个Socket  

  14.    

  15. $connection = socket_connect($socket, $host, $port) or die("Could not connet server\n");    //  链接  

  16. socket_write($socket, "hello socket") or die("Write failed\n"); // 数据传送 向服务器发送消息  

  17. while ($buff = socket_read($socket, 1024, PHP_NORMAL_READ)) {  

  18.     echo("Response was:" . $buff . "\n");  

  19. }  

  20. socket_close($socket);  

 

使用cli方式启动server:

php server.php

 

这里注意socket_read函数:

可选的类型参数是一个命名的常数:
PHP_BINARY_READ - 使用系统recv()函数。用于读取二进制数据的安全。 (在PHP>“默认= 4.1.0)
PHP_NORMAL_READ - 读停在\ n或\r(在PHP <= 4.0.6默认)  

针对参数PHP_NORMAL_READ ,若是服务器的响应结果没有\ n。形成socket_read(): unable to read from socket

 

3.PHP的并发IO编程

原文:http://rango.swoole.com/archives/508

 

1) 多进程/多线程同步阻塞

 

最先的服务器端程序都是经过多进程、多线程来解决并发IO的问题。进程模型出现的最先,从Unix系统诞生就开始有了进程的概念。最先的服务器端程序通常都是Accept一个客户端链接就建立一个进程,而后子进程进入循环同步阻塞地与客户端链接进行交互,收发处理数据。

多线程模式出现要晚一些,线程与进程相比更轻量,并且线程之间是共享内存堆栈的,因此不一样的线程之间交互很是容易实现。好比聊天室这样的程序,客户端链接之间能够交互,比聊天室中的玩家能够任意的其余人发消息。用多线程模式实现很是简单,线程中能够直接读写某一个客户端链接。而多进程模式就要用到管道、消息队列、共享内存实现数据交互,统称进程间通讯(IPC)复杂的技术才能实现。

代码实例:

多进程/线程模型的流程是

  1. 建立一个 socket,绑定服务器端口(bind),监听端口(listen),在PHP中用stream_socket_server一个函数就能完成上面3个步骤,固然也可使用php sockets扩展分别实现。

  2. 进入while循环,阻塞在accept操做上,等待客户端链接进入。此时程序会进入随眠状态,直到有新的客户端发起connect到服务器,操做系统会唤醒此进程。accept函数返回客户端链接的socket

  3. 主进程在多进程模型下经过fork(php: pcntl_fork)建立子进程,多线程模型下使用pthread_create(php: new Thread)建立子线程。下文如无特殊声明将使用进程同时表示进程/线程。

  4. 子进程建立成功后进入while循环,阻塞在recv(php: fread)调用上,等待客户端向服务器发送数据。收到数据后服务器程序进行处理而后使用send(php: fwrite)向客户端发送响应。长链接的服务会持续与客户端交互,而短链接服务通常收到响应就会close。

  5. 当客户端链接关闭时,子进程退出并销毁全部资源。主进程会回收掉此子进程。

 

这种模式最大的问题是,进程/线程建立和销毁的开销很大。因此上面的模式没办法应用于很是繁忙的服务器程序。对应的改进版解决了此问题,这就是经典的Leader-Follower模型。

代码实例:

它的特色是程序启动后就会建立N个进程。每一个子进程进入Accept,等待新的链接进入。当客户端链接到服务器时,其中一个子进程会被唤醒,开始处理客户端请求,而且再也不接受新的TCP链接。当此链接关闭时,子进程会释放,从新进入Accept,参与处理新的链接。

这个模型的优点是彻底能够复用进程,没有额外消耗,性能很是好。不少常见的服务器程序都是基于此模型的,好比Apache、PHP-FPM。

多进程模型也有一些缺点。

  1. 这种模型严重依赖进程的数量解决并发问题,一个客户端链接就须要占用一个进程,工做进程的数量有多少,并发处理能力就有多少。操做系统能够建立的进程数量是有限的。

  2. 启动大量进程会带来额外的进程调度消耗。数百个进程时可能进程上下文切换调度消耗占CPU不到1%能够忽略不接,若是启动数千甚至数万个进程,消耗就会直线上升。调度消耗可能占到CPU的百分之几十甚至100%。

另外有一些场景多进程模型没法解决,好比即时聊天程序(IM),一台服务器要同时维持上万甚至几十万上百万的链接(经典的C10K问题),多进程模型就力不从心了。

还有一种场景也是多进程模型的软肋。一般Web服务器启动100个进程,若是一个请求消耗100ms,100个进程能够提供1000qps,这样的处理能力仍是不错的。可是若是请求内要调用外网Http接口,像QQ、微博登陆,耗时会很长,一个请求须要10s。那一个进程1秒只能处理0.1个请求,100个进程只能达到10qps,这样的处理能力就太差了。

有没有一种技术能够在一个进程内处理全部并发IO呢?答案是有,这就是IO复用技术。

IO复用/事件循环/异步非阻塞

其实IO复用的历史和多进程同样长,Linux很早就提供了select系统调用,能够在一个进程内维持1024个链接。后来又加入了poll系统调用,poll作了一些改进,解决了1024限制的问题,能够维持任意数量的链接。但select/poll还有一个问题就是,它须要循环检测链接是否有事件。这样问题就来了,若是服务器有100万个链接,在某一时间只有一个链接向服务器发送了数据,select/poll须要作循环100万次,其中只有1次是命中的,剩下的99万9999次都是无效的,白白浪费了CPU资源。

直到Linux 2.6内核提供了新的epoll系统调用,能够维持无限数量的链接,并且无需轮询,这才真正解决了C10K问题。如今各类高并发异步IO的服务器程序都是基于epoll实现的,好比Nginx、Node.js、Erlang、Golang。像node.js这样单进程单线程的程序,均可以维持超过1百万TCP链接,所有归功于epoll技术。

IO复用异步非阻塞程序使用经典的Reactor模型,Reactor顾名思义就是反应堆的意思,它自己不处理任何数据收发。只是能够监视一个socket句柄的事件变化。

Reactor有4个核心的操做:

  1. add添加socket监听到reactor,能够是listen socket也可使客户端socket,也能够是管道、eventfd、信号等

  2. set修改事件监听,能够设置监听的类型,如可读、可写。可读很好理解,对于listen socket就是有新客户端链接到来了须要accept。对于客户端链接就是收到数据,须要recv。可写事件比较难理解一些。一个SOCKET是有缓存区的,若是要向客户端链接发送2M的数据,一次性是发不出去的,操做系统默认TCP缓存区只有256K。一次性只能发256K,缓存区满了以后send就会返回EAGAIN错误。这时候就要监听可写事件,在纯异步的编程中,必须去监听可写才能保证send操做是彻底非阻塞的。

  3. del从reactor中移除,再也不监听事件

  4. callback就是事件发生后对应的处理逻辑,通常在add/set时制定。C语言用函数指针实现,JS能够用匿名函数,PHP能够用匿名函数、对象方法数组、字符串函数名。

Reactor只是一个事件发生器,实际对socket句柄的操做,如connect/accept、send/recv、close是在callback中完成的。具体编码可参考下面的伪代码:

 

Reactor模型还能够与多进程、多线程结合起来用,既实现异步非阻塞IO,又利用到多核。目前流行的异步服务器程序都是这样的方式:如

  • Nginx:多进程Reactor

  • Nginx+Lua:多进程Reactor+协程

  • Golang:单线程Reactor+多线程协程

  • Swoole:多线程Reactor+多进程Worker


 

4. PHP socket内部源码

          从PHP内部源码来看,PHP提供的socket编程是在socket,bind,listen等函数外添加了一个层,让其更加简单和方便调用。可是一些业务逻辑的程序仍是须要程序员本身去实现。
下面咱们以socket_create的源码实现来讲明PHP的内部实现。
前面咱们有说到php的socket是以扩展的方式实现的。在源码的ext目录,咱们找到sockets目录。这个目录存放了PHP对于socket的实现。直接搜索PHP_FUNCTION(socket_create),在sockets.c文件中找到了此函数的实现。以下所示代码:

 

[cpp] view plain copy print?

  1. /* {{{ proto resource socket_create(int domain, int type, int protocol) U 

  2.    Creates an endpoint for communication in the domain specified by domain, of type specified by type */  

  3. PHP_FUNCTION(socket_create)  

  4. {  

  5.         long            arg1, arg2, arg3;  

  6.         php_socket      *php_sock = (php_socket*)emalloc(sizeof(php_socket));  

  7.    

  8.         if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lll", &arg1, &arg2, &arg3) == FAILURE) {  

  9.                 efree(php_sock);  

  10.                 return;  

  11.         }  

  12.    

  13.         if (arg1 != AF_UNIX  

  14. #if HAVE_IPV6  

  15.                 && arg1 != AF_INET6  

  16. #endif  

  17.                 && arg1 != AF_INET) {  

  18.                 php_error_docref(NULL TSRMLS_CC, E_WARNING, "invalid socket domain [%ld] specified for argument 1, assuming AF_INET", arg1);  

  19.                 arg1 = AF_INET;  

  20.         }  

  21.    

  22.         if (arg2 > 10) {  

  23.                 php_error_docref(NULL TSRMLS_CC, E_WARNING, "invalid socket type [%ld] specified for argument 2, assuming SOCK_STREAM", arg2);  

  24.                 arg2 = SOCK_STREAM;  

  25.         }  

  26.    

  27.         php_sock->bsd_socket = socket(arg1, arg2, arg3);  

  28.         php_sock->type = arg1;  

  29.    

  30.         if (IS_INVALID_SOCKET(php_sock)) {  

  31.                 SOCKETS_G(last_error) = errno;  

  32.                 php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to create socket [%d]: %s", errno, php_strerror(errno TSRMLS_CC));  

  33.                 efree(php_sock);  

  34.                 RETURN_FALSE;  

  35.         }  

  36.    

  37.         php_sock->error = 0;  

  38.         php_sock->blocking = 1;  

  39.                                                                                                                                            1257,1-8      61%  

  40.         ZEND_REGISTER_RESOURCE(return_value, php_sock, le_socket);  

  41. }  

  42. /* }}} */  

 

Zend API实际对c函数socket作了包装,供PHP使用。 而在c的socket编程中,咱们使用以下方式初始化socket。

 

[cpp] view plain copy print?

  1. //初始化Socket    

  2.     if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){    

  3.          printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);    

  4.          exit(0);    

  5.     }    

5. 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() 写数据到分散/聚合数组

 

 

 

6. PHP Socket模拟请求

咱们使用stream_socket来模拟:

 

 

[php] view plain copy print?

  1. /** 

  2.  *  

  3.  * @param $data= array=array('key'=>value) 

  4.  */  

  5. function post_contents($data = array()) {  

  6.     $post = $data ? http_build_query($data) : '';  

  7.     $header = "POST /test/ HTTP/1.1" . "\n";  

  8.     $header .= "User-Agent: Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1)" . "\n";  

  9.     $header .= "Host: localhost" . "\n";  

  10.     $header .= "Accept: */*" . "\n";  

  11.     $header .= "Referer: http://localhost/test/" . "\n";  

  12.     $header .= "Content-Length: ". strlen($post) . "\n";  

  13.     $header .= "Content-Type: application/x-www-form-urlencoded" . "\n";  

  14.     $header .= "\r\n";  

  15.     $ddd = $header . $post;  

  16.     $fp = stream_socket_client("tcp://localhost:80", $errno, $errstr, 30);  

  17.     $response = '';  

  18.     if (!$fp) {  

  19.         echo "$errstr ($errno)<br />\n";  

  20.     } else {  

  21.         fwrite($fp, $ddd);  

  22.         $i = 1;  

  23.         while ( !feof($fp) ) {  

  24.             $r = fgets($fp, 1024);  

  25.             $response .= $r;  

  26.             //处理这一行  

  27.         }  

  28.     }  

  29.     fclose($fp);  

  30.     return $response;  

  31. }  

注意,以上程序可能会进入死循环;

 

这个PHP的feof($fp) 须要注意的地方了,咱们来分析为何进入死循环。

 

[php] view plain copy print?

  1. while ( !feof($fp) ) {  

  2.     $r = fgets($fp, 1024);  

  3.     $response .= $r;  

  4. }  

 

实际上,feof是可靠的,可是结合fgets函数一块使用的时候,必需要当心了。一个常见的作法是:

 

[php] view plain copy print?

  1. $fp = fopen("myfile.txt", "r");  

  2. while (!feof($fp)) {  

  3.    $current_line = fgets($fp);  

  4.    //对结果作进一步处理,防止进入死循环  

  5. }  

当处理纯文本的时候,fgets获取最后一行字符后,foef函数返回的结果并非TRUE。实际的运算过程以下:

 1) while()继续循环。

 2) fgets 获取倒数第二行的字符串

 3) feof返回false,进入下一次循环

 4)fgets获取最后一行数据

 5)  一旦fegets函数被调用,feof函数仍然返回的是false。因此继续执行循环

 6) fget试图获取另一行,但实际结果是空的。实际代码没有意识到这一点,试图处理另外根本不存在的一行,但fgets被调用了,feof放回的结果仍然是false

 7)    .....

8) 进入死循环

相关文章
相关标签/搜索