在作PHP开发的过程当中,大部分咱们都在和http协议打交道,在ISO模型里面,http属于应用层协议,它底层会用到TCP协议。http协议很是简单,它是一个文本协议,一个请求对应一个响应,客户端发起一个请求,服务端响应这个请求。http是一个一问一答的对话,每次请求都得从新创建对话(这里暂不讨论Keep-Alive),若是你想经过一个请求进行屡次对话,那就是长链接通讯,必须使用TCP或者UDP协议。php
互联网运行的基石是创建在一些协议上的,目前而言主要是TCP/IP协议族,大部分协议都是公开开放的,计算机遵循这些协议咱们才能通讯,固然也有一些私有协议,私有协议只有本身知道如何去解析,至关来讲更安全,好比QQ所用的协议就是本身定义的。在ISO模型里面,我们经常使用的有http、ftp、ssh、dns等,可是不经常使用的数不胜数,发明一个协议不难,难的是如何设计的更好用,并且你们都喜欢用。html
Socket并非一个协议,本质上说Socket是对 TCP/IP 协议的封装,它是一组接口,在设计模式中,Socket 其实就是一个门面(facade)模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来讲,一组简单的接口就是所有,让 Socket 去组织数据,以符合指定的协议。编程
下图展现了Socket在ISO模型里面大概位置:json
虽然PHP的强项是处理文本,通常用来写网页和http接口,可是官方依然提供了Socket扩展,编译PHP时在配置中添加--enable-sockets 配置项来启用,若是使用apt或yum安装,默认状况下是已启用。设计模式
官方文档里面列出了大概40个函数,可是经常使用的也就那几个,跟着文档,我们一块儿来学学如何使用,首先声明一下,本人对Socket编程并不熟悉,若有错误的地方,但愿你们指出来。数组
我们先看一幅图,关于TCP客户端和服务端之间的通讯过程,我们平时写http接口的时候并未作这么多工做,那是客户端给封装好了:浏览器
<?php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 8888;
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sock, $ip, $port);
socket_listen($sock, 4);
echo "Server Started, Listen On $ip:$port\n";
$accept = socket_accept($sock);
socket_write($accept, "Hello World!\n", 8192);
$buf = socket_read($accept, 8192);
echo "Receive Msg: " . $buf . "\n";
socket_close($sock);
复制代码
简单说一下,为便于演示,因此省略了全部的错误处理代码,能够看到分为create、bind、listen、accept、write\read、close这几步,看上去很是简单!具体参数你们能够看一下文档!在服务端启动以后,当收到一个请求以后,咱们首先返回了一个Hello World\n
,而后又读取了8192个字节的数据,打印出来!最后关闭链接。安全
因为这里,咱尚未写客户端,因此暂时使用curl访问一下,运行效果以下:swoole
===>服务端:网络
===>客户端:
从这个例子里面咱们能够看出来,curl发出是一个标准的http请求,实际上它的每一行后面是有\n的,在http协议里面,这几行文本实际上是头(header),可是在这个例子里面,对于咱们来讲,它就是一段文本而已,服务端只是把它的内容打印出来了,并无去按照http协议去解析。虽然咱们返回了Hello World!\n
,可是这也并无按照http协议的格式去作,缺乏响应头。我只能说curl比较强大,若是使用浏览器访问的话会失败,提示127.0.0.1 sent an invalid response
。
可是稍加改造,咱们就能够返回一个标准的http响应:
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Server: Socket-Http\r\n";
$response .= "Content-Type: text/html\r\n";
$response .= "Content-Length: 13\r\n\r\n";
$response .= "Hello World!\n";
socket_write($accept, $response, 8192);
复制代码
这时候若是再用浏览器访问,就能够看到 Hello World!了,可是这个服务端目前是一次性的,就是说它只能处理一次请求,而后就结束了,正常的服务端是能够处理屡次请求的,很简单,加一个死循环就好了!
只贴一下改动的部分,代码以下:
while (true) {
$accept = socket_accept($sock);
$buf = socket_read($accept, 8192);
echo "Receive Msg: " . $buf . "\n";
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Server: Socket-Http\r\n";
$response .= "Content-Type: text/html\r\n";
$response .= "Content-Length: 13\r\n\r\n";
$response .= "Hello World!\n";
socket_write($accept, $response, 8192);
socket_close($accept);
}
复制代码
摇身一变,就是一个http服务了,使用ab测了一下,并发上万,是否是有点小激动?
然而,之因此这么快是由于逻辑简单,假如你在while里面任何位置加一个 sleep(1) 你就会发现,原来这特么是串行的,一个个执行的,并非并行,这段脚本一次只能处理一个请求!
解决这个问题方法有不少种,具体能够参考 PHP并发IO编程之路, 看看前半段就好了,后半段是广告!该文章总结了3种方法:最先是采用多进程多线程方式,因为进程线程开销大,这种方式效率最低。后来演进出master-worker模型,也就是相似如今fpm采用的方式。目前最早进的方式就是异步io多路复用,基于epoll实现的。理论上讲C能实现的,PHP都能经过扩展去实现,并且PHP确实提供了相关扩展,其思想和C写的都差很少,然而今天咱不是说高并发编程的,仍是接着说Socket吧!
以前的例子里面咱们使用的是curl访问的,也可使用浏览器或者telnet,这些工具均可以算做是客户端,客户端也能够本身实现。
set_time_limit(0);
$port = 8888;
$ip = '127.0.0.1';
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
echo "Connecting $ip:$port\n";
socket_connect($sock, $ip, $port);
$input = "Hello World Socket";
socket_write($sock, $input, strlen($input));
$out = socket_read($sock, 8192);
echo "Receive Msg: $out\n";
socket_close($sock);
复制代码
这段代码一样省略了错误处理代码,能够看到第一步都是create,可是第二步变成connect,而后是read\write、最后close。
具体运行效果这里再也不展现,和curl访问没多大区别,可是这个客户端也是一次性的,执行完了就结束!
接下来,咱们来写一个基于TCP通讯的应用,这个应用很是简单,就是加减乘除!
(1)服务端代码:
<?php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 8888;
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sock, $ip, $port);
socket_listen($sock, 4);
echo "Server Started, Listen On $ip:$port\n";
while (true) {
$accept = socket_accept($sock);
$buf = socket_read($accept, 8192);
echo "Receive Msg: " . $buf . "\n";
$params = json_decode($buf, true);
$m = $params['m'];
$a = $params['a'];
$b = $params['b'];
switch ($m) {
case '+';
$response = $a + $b;
break;
case '-';
$response = $a - $b;
break;
case '*';
$response = $a * $b;
break;
case '/';
$response = $a / $b;
break;
default:
$response = $a + $b;
}
socket_write($accept, $response."\n", 8192);
socket_close($accept);
}
复制代码
(2)客户端代码:
<?php
set_time_limit(0);
$port = 8888;
$ip = '127.0.0.1';
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
echo "Connecting $ip:$port\n";
socket_connect($sock, $ip, $port);
$input = json_encode([
'a' => 15,
'b' => 10,
'm' => '+'
]);
socket_write($sock, $input, strlen($input));
$out = socket_read($sock, 8192);
echo "Receive Msg: $out\n";
socket_close($sock);
复制代码
在这些代码里面,我按照本身的需求定义了一个“协议”,我把须要运算的数和方式经过一个json数组传输,约定了一个格式,这个协议只有我本身清楚,因此只有我才知道怎么调用。服务端在接受到参数以后,经过运算得出结果,而后把结果返回给客户端。
可是这个例子还有问题,客户端依然是一次性的,参数都被硬编码在代码里面,不够灵活,最关键是没有用到TCP长链接的特性,咱们每次计算都得从新发起请求、从新创建链接,实际上,我须要的是一次链接,屡次对话,也就是进行屡次计算!
目前为止,这些演示代码都没有复用链接,由于在服务端最后我close了这个链接,这意味着每次都是一个新的请求,若是是http服务的话尚且能够用一下,如何去实现一个TCP长链接呢?
select系统调用的目的是在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件,虽然这个方式也比较低效,可是不妨了解一下,经过这种方式咱们能够复用链接,完整的代码以下:
<?php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 8888;
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sock, $ip, $port);
socket_listen($sock, 4);
echo "Server Started, Listen On $ip:$port\n";
socket_set_nonblock($sock);
$clients = [];
while (true) {
$rs = array_merge([$sock], $clients);
$ws = [];
$es = [];
//监听文件描述符变更
$ready = socket_select($rs, $ws, $es, 3);
if (!$ready) {
continue;
}
if (in_array($sock, $rs)) {
$clients[] = socket_accept($sock);
$key = array_search($sock, $rs);
unset($rs[$key]);
}
foreach ($rs as $client) {
$input = socket_read($client, 8096);
if ($input == null) {
$key = array_search($client, $clients);
unset($clients[$key]);
continue;
}
echo "input: " . $input;
//解析参数,计算结果
preg_match("/(\d+)(\W)(\d+)/", $input, $params);
if (count($params) === 4) {
$a = intval($params[1]);
$b = intval($params[3]);
$m = $params[2];
} else {
continue;
}
switch ($m) {
case '+';
$result = $a + $b;
break;
case '-';
$result = $a - $b;
break;
case '*';
$result = $a * $b;
break;
case '/';
$result = $a / $b;
break;
default:
$result = $a + $b;
}
$output = "output: $result\n";
echo $output;
socket_write($client, $output, strlen($output));
}
}
复制代码
而后我使用了telnet链接服务端进行操做,运行效果以下,一个基于TCP长链接的网络版简易计算器:
在这个例子,传参的“协议”稍微有点变化,只是为了更方便在telnet里面交互,可是很容易理解。这里面最关键是定义了一个全局变量用来存储链接资源描述符,而后经过select去监听变化,最后遍历整个数组,读取\写入数据!
经过上面的简单介绍,但愿你们都对PHP Socket编程有一些了解和认识,其实做为Web开发来讲,不多会用到裸TCP去链接,大部分时候都是使用基于TCP的http协议,只有涉及到一些对响应速度要求很是高的应用,好比说游戏、实时通讯、物联网才会用到,若是真的用到,不妨尝试一下Workman、Swoole这些成熟的框架!