博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
PHP Socket 编程进阶指南
阅读量:6717 次
发布时间:2019-06-25

本文共 9064 字,大约阅读时间需要 30 分钟。

学习准备

  • Linux 或者 Mac 环境;
  • 安装有 Sockets 扩展;
  • 了解 TCP/IP 协议。

socket函数只是PHP扩展的一部分,编译PHP时必须在配置中添加 --enable-sockets 配置项来启用。

如果自带的PHP没有编译scokets扩展,可以下载相同版本的源码,进入ext/sockets使用phpize编译安装。

socket系列函数

socket服务端/客户端流程:

socket服务端/客户端流程

图中所示流程在任何编程语言里都是通用的。

server端

接下来我们写一个简单的单进程TCP服务器:

socket_tcp_server.php

说明:例子里我们先创建了一个TCP server,然后循环等待客户端连接。收到客户端连接后,循环解析来自客户端的消息。

例子里使用\n作为消息结束符,如果一次没有接收到完整消息,就循环读取,直到遇到结束符;读取完一条完整消息后,向客户端发送收到的消息,然后清空消息,准备下一次接收。

我们在命令行里运行服务端:

$ php socket_tcp_server.php waiting client...

新开终端使用telnet连接:

$ telnet 127.0.0.1 9201Trying 127.0.0.1...Connected to 127.0.0.1.Escape character is '^]'.hello Server!

我们发送了一条消息,服务端这边会收到:

client connect succ.recv: hello Server!

接下来,我们使用socket写一个自己的tcp客户端。

client端

下面的例子很简单,创建客户端,连接服务端,发送消息,读取完后就结束了。

socket_tcp_client.php

我们先在原来的telnet终端页面输入quit退出连接,因为此时我们的服务端还只能接受一个客户端连接。然后运行自己写的客户端:

$ php socket_tcp_client.php from server: hello, I'm client!

socket_select

上面的例子里,我们的tcp服务端仅能接受一个客户端连接。怎么能做到支持多个客户端连接呢?常用的有:

  • 多进程
  • 多线程
  • I/O复用,使用select、poll、epoll等技术
  • 多进程+I/O复用

本节里我们使用第三种方法,即I/O复用。技术实现层面则是使用PHP提供的socket_select系统调用来实现。

I/O复用使得程序能同时监听多个文件描述符。实现I/O复用的系统调用主要的有select、poll、epoll。

接下来看实例:

socket_select.php

$client) { //新连接 if($client === $socket){ //阻塞等待客户端连接 $conn = socket_accept($socket); if(!$conn){ echo "accept server fail:".socket_strerror(socket_last_error())."\n"; break; } $clients[] = $conn; echo "client connect succ. fd: ".$conn."\n"; //获取客户端IP地址 socket_getpeername($conn, $addr, $port); echo "client addr: $addr:$port\n"; //获取服务端IP地址 socket_getsockname($conn, $addr, $port); echo "server addr: $addr:$port\n"; // print_r($clients); echo "total: ".(count($clients)-1)." client\n"; }else{ //注意:后续使用$client而不是$conn if (!isset($recvs[$k]) ) $recvs[$k] = ''; //兼容可能没有值的情况 $buffer = socket_read($client, 100); //每次读取100byte if($buffer === false || $buffer === ''){ echo "client closed\n"; unset($clients[array_search($client, $clients)]); //unset socket_close($client); //关闭本次连接 break; } //解析单次消息,协议:换行符 $pos = strpos($buffer, "\n"); if($pos === false){ //消息未读取完毕,继续读取 $recvs[$k] .= $buffer; }else{ //消息读取完毕 $recvs[$k] .= trim(substr($buffer, 0, $pos+1)); //去除换行符及空格 //客户端主动端口连接 if($recvs[$k] == 'quit'){ echo "client closed\n"; unset($clients[array_search($client, $clients)]); //unset socket_close($client); //关闭本次连接 break; } echo "recv:".$recvs[$k]."\n"; socket_write($client, $recvs[$k]."\n"); //发送消息 $recvs[$k] = ''; } } } }socket_close($socket);

我们先使用Crtl+C关闭上一次运行的TCP server,然后运行新写的server:

php socket_select.phpwaiting client...

新开终端telnet客户端:

telnet 127.0.0.1 9201Trying 127.0.0.1...Connected to localhost.Escape character is '^]'.hello worldhello world

再打开终端新开一个telnet客户端,我们来看服务端的输出:

client connect succ. fd: Resource id #5client addr: 127.0.0.1:60065server addr: 127.0.0.1:9201total: 1 clientrecv:hello server!client connect succ. fd: Resource id #6client addr: 127.0.0.1:60069server addr: 127.0.0.1:9201total: 2 clientrecv:hello world

此时我们的服务端就不受客户端连接数限制了。

注意点:

1、使用了socket_select后,解析消息的地方不能再是死循环,否则造成阻塞。

select 函数监视的文件描述符分为3类,分别是 writefds, readfds, exceptfds,调用之后select函数就会阻塞,直到有文件描述符就绪(有数据可读,可写或者except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回;当select函数返回之后,可以通过遍历 fdset来找到就绪的描述符。

2、socket系统调用最大支持1024个客户端连接,如果需要更大的客户端连连,则需要使用poll、epoll等技术。本文不做讲解。

socket_set_option

该函数用来设置socket选项,比如设置端口复用。函数原型:

bool socket_set_option ( resource $socket , int $level , int $optname , mixed $optval )

示例:

socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); //复用端口

该小节不是本文重点,该函数大家了解即可,需要设置的时候能知道怎么调用。顺便提一下,端口复用技术是用来解决"惊群"问题的,大家感兴趣可以看看博文:Linux网络编程“惊群”问题总结 -

https://www.cnblogs.com/Anker/p/7071849.html 。

函数参考

这些里都有,贴出来供大家快速查阅。

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() 删除一个已经分配的iovecsocket_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,如果没有指定则默认当前socketsocket_recvmsg() 从iovec里接受消息socket_select() 多路选择socket_send() 这个函数发送数据到已连接的socketsocket_sendmsg() 发送消息到socketsocket_sendto() 发送消息到指定地址的socketsocket_set_block() 在socket里设置为块模式socket_set_nonblock() socket里设置为非块模式socket_set_option() 设置socket选项socket_shutdown() 这个函数允许关闭读、写、或者指定的socketsocket_strerror() 返回指定错误号的详细错误socket_write() 写数据到socket缓存socket_writev() 写数据到分散/聚合数组

其中socket里的write readwritev readvrecv `sendrecvfrom sendtorecvmsg sendmsg五组 I/O 函数可以参考:https://blog.csdn.net/yangbingzhou/article/details/45221649

stream_socket系列函数

stream_socket系列函数相当于是socket函数的进一步封装。使用该系类函数能简化我们的编码。

stream_socket_serverstream_socket_accept返回的句柄可以由fgets() , fgetss() , fwrite() , fclose() 以及feof() 函数调用。

server端

我们先看一下函数原型。

stream_socket_server:

resource stream_socket_server ( string $local_socket [, int &$errno [, string &$errstr [, int $flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN [, resource $context ]]]] )

如果是udp服务,flags指定为STREAM_SERVER_BIND。 另外,$contextstream_context_create创建,例如:

$context_option['socket']['so_reuseport'] = 1;//端口复用$context = stream_context_create($context_option);

stream_socket_accept:

resource stream_socket_accept ( resource $server_socket [, float $timeout = ini_get("default_socket_timeout") [, string &$peername ]] )

接下来我们使用stream_socket_系列函数写一个tcp server。

tcp server

示例:

stream_socket_server.php

代码相比使用纯socket函数少了很多。

运行:

$ php stream_socket_server.php waiting client...new Client! fd:6recv: hello

客户端使用telnet:

$ telnet 127.0.0.1 9201Trying 127.0.0.1...Connected to 127.0.0.1.Escape character is '^]'.hellorecv: hello

udp server

udp服务端不需要listen操作。

运行:

$ php stream_socket_server_udp.php 127.0.0.1:43172recv: hello

客户端使用 netcat:

netcat -u 127.0.0.1 9201hellorecv: helloquit

如果没有netcat需要安装:

sudo apt-get install netcat

客户端

上面我们都是用的telnetnetcat来连接服务端,接下来我们使用stream_socket_系列函数编写tcp/udp客户端。

简单示例

stream_socket系列函数写client非常简单:

udp客户端仅需要修改tcp为udp。

stream_select

stream系列函数使用stream_select实现I/O复用,本质都是select系统调用。

接下来我们写两个示例,第一个示例和上面使用socket_select实现的类似,第二个则是监听了客户端读写事件,从而实现了类似telnet的功能,相信大家会感兴趣的。

同时监听socket和连接socket

使用stream_select可以实现IO复用,使得单进程程序也能支持同时处理多个客户端连接。示例:

运行服务端并随后运行telnet客户端:

$ php stream_select.php waiting client...new Client! fd:6recv: wwnew Client! fd:7recv: kkk

可以同时支持多个客户端。从例子可以看出来,stream_selectsocket_select用法相同。

同时处理网络连接和用户输入

下面的例子使用stream_select实现了客户端程序运行后,支持命令行界面手动实时输入与服务端进程交互:

例子里,我们把$socketSTDIN使用stream_select监听文件描述符的变化情况,当有文件描述符就绪,函数会返回,从而执行我们逻辑代码。

先运行tcp服务端程序stream_select.php,然后运行该客户端程序:

$ php tcp_client_select.php ENTER MSG:hello!ENTER MSG:Recv: recv: hello!ENTER MSG:

程序一直会等待我们的输入,除非输入quit退出。

函数参考

stream_socket_server() - 创建serverstream_socket_accept() - 接受由 stream_socket_server创建的socket连接stream_socket_get_name() - 获取本地或者远程的套接字名称stream_set_blocking() - 为资源流设置阻塞或者阻塞模式stream_set_timeout() - 为资源流设置超时stream_socket_client() - 创建clientstream_select() - select系统调用,实现IO多路选择stream_socket_shutdown() - 这个函数允许关闭读、写、或者指定的socketstream_socket_recvfrom() - stream_socket_sendto() -

总结

本文主要和大家讲解了 PHP Socket 编程相关知识。通过学习本文,大家学到了如下内容:

  • 熟悉 socket 系列函数使用
  • 熟悉 stream_socket 系列函数使用
  • 熟悉 I/O 复用
  • 如何使用 socket 系列函数实现 TCP 服务端和客户端
  • 如何使用 socket_select 实现 I/O 多路复用
  • 如何使用 stream_socket 系列函数实现TCP服务端和客户端
  • 如何使用 stream_select 实现 I/O 多路复用

也给大家留一个问题:

如何基于PHP多进程Master-Worker模型实现支持I/O复用的TCP server?

提示:我公众号(fhyblog)里有PHP多进程系列笔记相关文章,多进程不熟悉的同学可以学习一下。

(全文完)

参考

1、深入浅出讲解:php的socket通信 - 洒洒 - 博客园

http://www.cnblogs.com/thinksasa/archive/2013/02/26/2934206.html
2、write read;writev readv;recv send;recvfrom sendto;recvmsg sendmsg五组I/O函数汇总 - CSDN博客
https://blog.csdn.net/yangbingzhou/article/details/45221649
3、socket编程中的read、write与recv、send的区别 - CSDN博客
https://blog.csdn.net/xhu_eternalcc/article/details/18256561
4、php select socket - yuanlp_code - 博客园
https://www.cnblogs.com/yuanlipu/p/6431834.html
5、socket服务的模型以及实现(3)–单进程IO复用select | 你好,欢迎来到老张的博客,张素杰
http://www.xtgxiso.com/socket%e6%9c%8d%e5%8a%a1%e7%9a%84%e6%a8%a1%e5%9e%8b%e4%bb%a5%e5%8f%8a%e5%ae%9e%e7%8e%b03-%e5%8d%95%e8%bf%9b%e7%a8%8bio%e5%a4%8d%e7%94%a8select/
6、PHP Socket实现websocket(四)Select函数 - 海上小绵羊 - 博客园
https://www.cnblogs.com/yangxunwu1992/p/5564454.html .

转载于:https://www.cnblogs.com/52fhy/p/9293015.html

你可能感兴趣的文章
双星闪耀 上汽大众途岳、全新一代帕萨特联袂亮相
查看>>
监控系统云计算核心技术,主要包括十项技术
查看>>
下半年最值得关注的10个技术公众号
查看>>
MySQL 异步驱动浅析 (一):性能分析
查看>>
理解高性能网络模型
查看>>
提高 JavaScript 开发效率的高级 VSCode 扩展!
查看>>
30岁的你,还在写代码吗?
查看>>
阿里提出联合预估算法JUMP:点击率和停留时长预测效果最优
查看>>
世界杯要来了,AI预测冠军哪家强?
查看>>
代码这样写更优雅 (Python 版)
查看>>
SVG入门—如何手写SVG
查看>>
【面试必备】透过源码角度一步一步带你分析 ArrayList 扩容机制
查看>>
深入理解虚拟机之虚拟机性能监控和故障处理工具
查看>>
写个 vue-loading-template 组件
查看>>
北漂之毕业裁员后的又一波奇遇
查看>>
关于11月比特币现金将添加CTOR事件
查看>>
SIGIR2018大会最佳短论文:利用对抗学习的跨域正则化
查看>>
美国黄金公司Schiff Gold:BCH避险潜力远大于BCE
查看>>
Tomcat运行web程序过程及server.xml配置
查看>>
可读可写流简明实现指北【多图,附demo源码】
查看>>