转载请注明文章出处: tlanyan.me/php-review-…php
web开发一直是PHP的主战场,也是PHP最为被世人所熟知的一面。其实只要你愿意去发掘,PHP除了作网页在许多其余方面也是小能手。web
本文简要介绍PHP的Socket编程。redis
在开始以前,但愿你已经知道网络编程中的一些基本概念。好比OSI七层模型、TCP/IP四层模型;TCP中的三次握手、四次挥手等。这些概念是网络编程的理论基础,实践中不必定用获得,但能让你把握总体脉络,更快的定位编程中出现的问题。数据库
再说一下Socket。咱们常说的网络编程就是指Socket编程,它既指代实现了TCP/IP协议簇的一套网络编程API,也指代一个客户端与服务器的链接。socket是插座/接口的意思,计算机中常翻译成“套接字”。实际中能够简单的认为网络编程与Socket编程等价,一个tcp链接的说法等价于一个socket。编程
PHP中有以socket
开头的一套函数API用于Socket编程,PHP5引入“流”的抽象概念后,以stream
开头的一套API也能够用于网络编程。二者的主要区别是:json
socket
系列函数相对底层,而stream
系列函数是高层的抽象。若是你想体验原味Socket编程,用socket
开头的API比较适合;不然建议使用流函数。有关流的知识,请参考本人以前的博文:PHP回顾之流。服务器
接下来咱们用流函数实现一个简单的TCP客户端和服务端。cookie
客户端网络编程能够归结为简单的三步:网络
下面是客户端的代码,发送10条消息到服务端:session
// 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);
复制代码
客户端已经搞定,接下来看服务端。
服务端编程也很简单,四步搞定:
因为服务端通常是长时间运行,除非重启或进程被杀死,极少会主动关闭服务。另外服务端通常须要长时间运行,因此应当运行在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_blocking
和stream_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;
}
}
// 父进程先退出,不会出现僵尸进程,忽略孤儿进程的处理
复制代码
启动客户端后,能够看到服务端正确的同时处理多个客户端,这正是咱们期待的。
上述代码实现了客户端和可并发的服务端,做为演示基本够用。若是要投入到实践中使用,至少有如下方面的不足:
每一个方面展开来讲至少都是一篇长文。本文目的是简要介绍PHP中的Socket编程,行文到此已经达到目的。因为网络协议十分繁杂,想深刻网络编程请参阅更多权威文档。
本文基于PHP5引入的流简要介绍了PHP中的Socket编程,并给出了一个简单并发服务器的实现。文中代码仅作演示用,在生产环境中,请使用成熟的网络框架/库。