PHP SOCKET编程基础
先说点屁话:SOCKET是死马?中文称为:套接字,这个翻译非常变态,老子以前就被这个翻译坑了,一直搞不清楚到底是什么,套接字是个死马意思?一点都不形象。在英语里面SOCKET:插座的意思。
先看看SOCKET是怎么定义的:socket是由IP和端口组成,通过某种协议实现不同计算机之间的通信。
既然SOCKET翻译过来是插座的意思,这就好理解了,服务器就是一个大的排插,链接就通过排插,那么现在如果链接需要什么东西?
1:服务端(被链接方)得有一个插孔。
2:客户端(链接方)得有一个插头。
有了这两个还是不行啊,服务器你得告诉我地址吧?我链接哪里的地址啊,这就是IP地址,IP地址就是服务器地址,但是服务器这么大,这么多服务,我要链接哪一个?好,我给你开一个插孔吧,这就是端口,但是插孔有三孔的,两孔的,圆孔的,你总得告诉我型号吧,这不就是协议吗?其实这里协议比喻还是不太形象,协议好比发快递的时候,你用的是顺丰,还是三通一达。
一:TCP/IP:
1:概述:TCP/IP:TCP/IP是互联网基础通信协议体系,它不是一个协议,是一个协议族,里面包括了TCP,UDP,HTTP,FTP,IMCP等等协议。
2:TCP/IP协议分层:ISO-OSI七层协议,TCP/IP四层。
为什么TCP/IP协议族只分4层,因为网络接口层下面属于硬件范畴了,和TCP/IP几乎没什么关系。
TCP/IP:由低到高:
(1)网络接口层:
为网络传输做准备,数据要在网络中传播,数据必须符合网络传输形态格式,第一步就是构成数据链路数据单元(帧),包括链路管理、差错控制、流量控制等(传输层也有流量控制)。
(2)网络层:
IP协议就在这里,主要功能为寻址与路由:寻址就是根据目标IP知道数据发到哪里,路由就是各个中间节点在原地址和目标IP地址之间找到合适的转发路径。
分段与重组:分段如果IP数据报大小超过了网络的最大传输单元(MTU),就需要分段。重组数据达到目标主机后,恢复原来的IP数据
(3)运输层:TCP,UDP,运输层虽然说也是协议,确切的说是一种运输方式可能更恰当。
(4)应用层:HTTP,FTP,SMTP等,为什么要用应用层协议?因为没有应用层协议,数据就无法识别,也就没什么意义,诸如数据的格式、大小、含义都不清楚。我们平常说的自定义协议就是指在应用层的自定义协议。
从上图协议分层看,似乎没看见SOCKET在哪里?我们把上面的TCP/IP协议分层图再稍微修改一下:
SOCKET编程:从图上我们就知道什么是SOCKET编程了,SOCKET不是什么协议,SOCKET编程就是对复杂的TCP/IP进行封装,比如如何链接、监听、关闭等等,和业务逻辑没毛关系。比如workerman用纯php写的socket框架,swoole用C编写的PHP socket框架……
二:PHP SOCKET编程方法:
PHP的SOCKET编程有两种种方法:
1:用C扩展php,比如swoole,门槛很高。
2:PHP自身语言编写,PHP提供了两种类型的socket:socket 和 stream_socket,两个互相不兼容。
(1):socket:更加底层,和C的SOCKET接口差不多,使用较麻烦,而且需要安装,编译php的时候需要加:--enable-sockets,方法以:socket_XXXX开头。
(2):stream_socket:PHP自己的接口,不需要安装,拿来即用,方便,缺点就是支持不够,一些参数无法自定义。方法以:stream_socket_XXXX开头,workerman就是基于这个来封装的。当 stream_socket无法设置某些参数的时候,通过socket_import_stream将stream_socket转换成底层sockets,然后通过socket_set_option设置stream_socket的socket选项。
三:PHP SOCKET实例
1:TCP流程步骤:
<?php /** TCP Server **/ /* 第1步:创建一个TCP socket AF_INET:IPv4网络协议; SOCK_STREAM:套接字类型TCP; SOL_TCP:TCP协议 正确时返回一个套接字,失败时返回 FALSE */ set_time_limit(0); //防止超时 $socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); if($socket == NULL){ //如果语法错误 exit(-1); } if (FALSE === $socket){ //创建失败 $errcode = socket_last_error(); die("socket create fail: " . socket_strerror($errcode)); } //第2步:绑定;把IP和端口绑定到创建的socket if(FALSE === socket_bind($socket, '0.0.0.0',23)){ $errcode = socket_last_error(); die("socket bind fail: " . socket_strerror($errcode)); } /* 第3步:监听,监听socket上的连接 这个10是 int $backlog 参数,表示每一个端口最大的监听队列的长度 服务端有一个队列,客户端的请求先放在队列里面,比如我这里设置为 10, 那么如果并发来20个,那么 其他10个就先拒绝,客户端可能会收到一个错误提示:econnnrejected, 如果是TCP,忽略该请求,服务端不会发送RST(连接重置)和FIN(关闭连接),那么客户端没有收到响应,就会重新发送SYN连接。 在linux上该值受限于somaxconn 如果超过linux 的somaxconn设置,那么该值就是:somaxconn值 */ if(FALSE === socket_listen($socket,10)){ //允许10个客户端排队 $errcode = socket_last_error(); die("socket listen fail: " . socket_strerror($errcode)); } echo "TCP Service Start:\n"; //服务成功开启 /* 第4步:等待连接 等待接受一个Socket连接,成功时返回新的socket资源,该新的socket资源用于通信 */ while(true){ //循环连接 $conn = socket_accept($socket); if($conn === FALSE){ die("accept() failed: reason: " . socket_strerror(socket_last_error()) . "\n"); } $msg=iconv('utf-8', 'gbk', "连接成功"); socket_write($conn, "$msg\n\r"); //返回给客户端数据:连接成功 echo "$msg\n"; //服务端提示 while (true) { //连接不断的情况下,重复发送 //第5步:读取客户端发送的信息 $input = @socket_read($conn, 1024); //1024为长度 $input = trim($input); //去掉字符串空格 if($input === FALSE || $input ==''){ break; //跳出 } //第6步:处理客户端数据 socket_write($conn, $input, strlen ($input)); //返回给客户端数据 echo "$input\n"; //server提示 }; }; /* 第7步:关闭socket 需要关闭两个socket资源 1:最开始创建的socket资源,用于绑定和监听 2:socket_accept连接后产生的新的socket资源,用于通信 */ socket_close($conn); //关闭连接 socket_close($socket); //关闭服务
<?php /** TCP Client **/ //第1步:创建一个TCP socket,和server一样 $socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); //第2步:连接 socket_connect($socket, '127.0.0.1', 23); while ($buff = socket_read($socket, 1024)) { //循环读取服务端返回的信息 echo("Response data:" . $buff . "\n"); // 服务端响应的数据 echo("Send data:\n"); $msg = fgets(STDIN); //输入待发送的数据 socket_write($socket, $msg,strlen($msg)); //发送信息到服务端 } socket_close($socket); //关闭连接
上面这个例子,功能实现了一个简单的TCP连接,客户端可以重复发送,但是有个问题,如果某个客户端在连接,其他的客户端要等到该连接断开才能操作,也就是一次只能处理一个连接和传输,这肯定不行,要解决这个问题,有三个办法:
a:多进程:PHP是支持多进程的,主进程负责监听连接,子进程负责数据传输,需要开启扩展,--enable-pcntl,WorkerMan就是多进程的。
b:IO多路复用机制:PHP的socket_select函数可以干这个,安装Event扩展 或者 libevent扩展,PHP7只能用 Event。
c:多线程:需要安装扩展,pthread 扩展, --enable-maintainer-zts
以下为IO多路复用,支持多个客户端同时连接,把上面的例子改一下:
用了socket_select函数,底层是select模型
<?php /** TCP Server **/ /* 第1步:创建一个TCP socket AF_INET:IPv4网络协议; SOCK_STREAM:套接字类型TCP; SOL_TCP:TCP协议 正确时返回一个套接字,失败时返回 FALSE */ set_time_limit(0); //防止超时 $socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); if($socket == NULL){ //如果语法错误 exit(-1); } if (FALSE === $socket){ //创建失败 $errcode = socket_last_error(); die("socket create fail: " . socket_strerror($errcode)); } //第2步:绑定;把IP和端口绑定到创建的socket if(FALSE === socket_bind($socket, '0.0.0.0',23)){ $errcode = socket_last_error(); die("socket bind fail: " . socket_strerror($errcode)); } /* 第3步:监听,监听socket上的连接 这个10是 int $backlog 参数,表示每一个端口最大的监听队列的长度 服务端有一个队列,客户端的请求先放在队列里面,比如我这里设置为 10, 那么如果并发来20个,那么 其他10个就先拒绝,客户端可能会收到一个错误提示:econnnrejected, 如果是TCP,忽略该请求,服务端不会发送RST(连接重置)和FIN(关闭连接),那么客户端没有收到响应,就会重新发送SYN连接。 在linux上该值受限于somaxconn 如果超过linux 的somaxconn设置,那么该值就是:somaxconn值 */ if(FALSE === socket_listen($socket,10)){ //允许10个客户端排队 $errcode = socket_last_error(); die("socket listen fail: " . socket_strerror($errcode)); } echo "TCP Service Start:\n"; //服务成功开启 //把所有的客户端连接放在这里 $clients = array($socket); /* 第4步:等待连接 等待接受一个Socket连接,成功时返回新的socket资源,该新的socket资源用于通信 */ while(true){ //循环连接 // 创建一个副本,保证$clients不会被socket_select修改 $read = $clients; //socket_select 构建多路IO复用,$read会保存多个客户端socket if (socket_select($read, $write = null, $except = null, 0) < 1) { continue; } //检查是否有客户端连接 if (in_array($socket, $read)) { // 接受客户端连接,把连接放到clients数组 $clients[] = $newsock = socket_accept($socket); // 向客户端发送:已连接 socket_write($newsock, iconv('utf-8', 'gbk', "已连接")."\n"); //server提示:已连接 socket_getpeername($newsock, $ip); //获取客户端IP地址 echo iconv('utf-8', 'gbk', "已连接").": {$ip}\n"; //输出已连接的客户端 // 删除已经连接的socket $key = array_search($socket, $read); unset($read[$key]); } //连接不断的情况下,重复发送 foreach ($read as $conn) { //第5步:读取客户端发送的信息 $input = @socket_read($conn, 1024); //1024为长度 $input = trim($input); //去掉字符串空格 if($input === FALSE || $input ==''){ //如果客户端断开 $key = array_search($conn, $clients); unset($clients[$key]); echo iconv('utf-8', 'gbk', "客户端已断开")."\n"; continue; } //第6步:处理客户端数据 socket_write($conn, $input, strlen ($input)); //返回给客户端数据 echo "$input\n"; //server提示 } }; /* 第7步:关闭socket 需要关闭两个socket资源 1:最开始创建的socket资源,用于绑定和监听 2:socket_accept连接后产生的新的socket资源,用于通信 */ socket_close($conn); //关闭连接 socket_close($socket); //关闭服务
2:UDP流程步骤:
TCP和UDP的步骤不一样,UDP少了一些步骤,主要是两者传输方式不一样,关于TCP和UDP的介绍和区别,网上有很多。