说明
- 我们都知道通过IP,端口等可以实现两台机器之间的数据互通,但具体要怎么操作,系统给我们提供了socket接口,通过调用socket函数就可以实现互通。
- php的socket扩展和C本身的非常相似,如果找不到php相关的资料,可以对照着C的socket函数来学习,例如:C语言SOCKET编程指南
- php的socket文档,文档中有很多函数,我们只找主要通讯流程的函数理解其流程,其它函数后期用到再去查看即可
通信流程
图片引用自sockets百度百科词条,tcp通信的流程,其它方式的通信可参考
相关函数
-
服务端相关函数
-
socket_create
( int $domain , int $type , int $protocol ) : resource- 创建一个socket,例如
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
- $domain是选择
IP4
或者IP6
或者UNIX本地通讯
,配置过nginx的话应该会知道参数fastcgi_pass
用来连接php-fpm的,有两种方式,一种是tcp,一种是unix socket,就是对应这里的IP方式和UNIX本地方式 - $type是
SOCK_STREAM
(tcp)或者SOCK_DGRAM
(udp)或者其它 - $protocol 一般为0是IP协议。
- 上方语句创建了ip4地址的tpc套接字,更多参数自行查看文档。
- 创建一个socket,例如
-
socket_bind
( resource $socket , string $address [, int $port = 0 ] ) : bool- 将上方创建的socket绑定具体的IP和端口,到时候客户端连接需要填写此IP和接口
-
socket_listen
( resource $socket [, int $backlog = 0 ] ) : bool- 监听客户端的链接。(udp通信时,不需要用到此函数)
- 第二个参数backlog是accept之前握手队列的大小,如果我们配置过php-fpm的话应该有接触过,叫
listen.backlog
,这个参数就对应着socket_listen中的第二个参数,目前php-fpm默认值是511,而workerman中默认值是102400 - 相关参考:socket_listen里面第二个参数backlog的用处;TCP SOCKET中backlog参数的用途是什么?
-
socket_accept
( resource $socket ) : resource- 接收客户端连接(udp通信时,不需要用到此函数)
至此就可以和客户端联通,进行读写操作了,需要注意的是其返回值,是一个新的socket,而后续读写是在这个新的socket下进行的而不是一开始创建的socket。
- 接收客户端连接(udp通信时,不需要用到此函数)
-
-
客户端相关函数
-
socket_connect
( resource $socket , string $address [, int $port = 0 ] ) : bool- 客户端连接服务端,同服务端一样,首先要调用
socket_create
创建一个socket,然后再调用此函数连接到服务端
- 客户端连接服务端,同服务端一样,首先要调用
-
-
读写函数
-
socket_send
( resource $socket , string $buf , int $len , int $flags ) : int- 发送数据给socket
-
socket_write
( resource $socket , string $buffer [, int $length = 0 ] ) : int- 向socket中写数据
- 上两个函数基本一样,socket_send中最后一位$flags参数设置为0,就等于socket_write,网上关于这两个函数的比较非常少,基本搜索不到,而且费解的是查看php源码会发现这两个方法内部有些许差异,
socket_write
中引用#ifndef PHP_WIN32
来判定如果是非windows系统,则调用系统的write
方法来实现,windows系统则调用系统的send
方法来实现,但是socket_send
则没有判定,统一调用系统的send
来实现,不明白socket_write
为什么要增加判定? -
socket_sendto
( resource $socket , string $buf , int $len , int $flags , string $addr [, int $port = 0 ] ) : int- 主要是udp通信时,发送数据
-
socket_recv
( resource $socket , string &$buf , int $len , int $flags ) : int- 从socket中接收数据,需要注意的是返回值是int表示接收的字节数,而内容存在第二个引用类型的参数$buf中
-
socket_read
( resource $socket , int $length [, int $type = PHP_BINARY_READ ] ) : string- 从socket中读数据,返回结果就是读的数据
- 上面两个函数也很相近,除了返回值的区别,socket_recv最后$flags为0和socket_read最后$type为PHP_BINARY_READ时读取的数据是一样的,但是如果flags或者type变化,则不一定一样了。
-
socket_recvfrom
( resource $socket , string &$buf , int $len , int $flags , string &$name [, int &$port ] ) : int- 主要是udp通信是,接收数据
-
-
其它常用函数
-
socket_close
( resource $socket ) : void- 关闭socket
-
socket_set_nonblock
( resource $socket ) : bool- 设置为非阻塞
-
socket_set_block
( resource $socket ) : bool- 设置为阻塞
- 上面两个影响的函数为:
socket_connect
、socket_accept
以及上方的各种读写函数
,以socket_accept
为例,阻塞就是调用此方法后,如果没有接受到客户端,则此方法一直卡住,等待客户端加入后才会进行下一步动作,而非阻塞则是如果没有客户端加入,则直接返回false,具体的会在后面的IO模型中总结 -
socket_select
( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] ) : int- 调用系统select()相关IO模型
- 详细的在后面的IO模型中会总结
-
示例
- 服务端socket_service.php:
<?php //IP和端口 $address = ‘127.0.0.1‘; $port = 8888; //创建 $listenSocket = socket_create(AF_INET, SOCK_STREAM, 0); //绑定 socket_bind($listenSocket, $address, $port); //监听 socket_listen($listenSocket, 5); //循环接入客户端 while (true) { //接入客户端 $connectSocket = socket_accept($listenSocket); $msg = "hello\r\n"; //发送给客户端,注意此时用的是$connectSocket,而不是$listenSocket socket_write($connectSocket, $msg, strlen($msg)); //关闭 socket_close($connectSocket); } //关闭 socket_close($listenSocket);
- 客户端socket_client.php:
<?php //服务端IP和端口 $address = ‘127.0.0.1‘; $port = 8888; //创建 $socket = socket_create(AF_INET, SOCK_STREAM, 0); //连接 $result = socket_connect($socket, $address, $port); //读取 $out = socket_read($socket, 2048); echo $out; //关闭 socket_close($socket);
- 上方是简单的示例代码,在命令行cli模式下运行
php socket_service.php
,然后再新起一窗口运行php socket_client.php
就能看到客户端联通后收到了服务端发送的数据并显示出来。 - 我们在普通的编程中,通常是尽量避免使用while(true)的,害怕无限循环会导致卡死,但在网络编程中会经常用到,其实只要控制好就没问题,上方之所以可以用是因为默认socket_accept是阻塞的,也就是没有客户端接入时会卡在这里等待接入,所以不会造成性能影响。
- 也可以查看php文档中的示例,按照提示来测试。
- 通过示例我们可以看到是可以连通,但是有很大的局限性,只能一个服务端连接一个客户端,如果想同时连接多个客户端是不行的(上方简单的代码可能不明显,毕竟连接后接着就关闭断开了,但是运行php文档中的例子会比较明显,同时打开几个窗口用
telnet
来链接,会发现只有一个结束后,另一个才能接入),解决方法就是可以用fork子进程或者是IO多路复用,后续会总结。