博主菜鸟一枚,总是听人说tcp是长连接,httpgt;那么菜鸟的博主就想了,既然tcp可以长连接,那么是否就可以用tcp实现持久的通信呢。永远不断开呢?于是博主不断的尝试php中各种实现tcp通信的函数。最后博主失望了,都不行什么socket_create什么 stream_socket_server啊!都尝试了,不断的失败让博主明白了一个道理
注(1)长连接和持久连接不是一回事,基于http协议下的tcp连接永远是会通信完毕就断的
这个理论的详细解说请参考①部分的解说:
要理解为什么会出现HTTP,WebSocket,可以做这样一个假设。
假设平行宇宙1984年(我懒的查数据,所以就说一个平行宇宙了,随便看看吧),人们使用TCP协议进行通讯,假设那时候还没有网页浏览器这一说法,大家都是通过各种软件直接通讯。
假设到了1986年,人们使用浏览器来浏览网页,假设当时电脑时100MHz,100M内存。对TCP协议熟悉一点,大概也能猜到一个TCP链接,会消耗一点点内存,假设是32k(具体我也不知道),那么如果一台几万块钱的服务器最大能支持100M/32k=3200个连接。显然,如果一个公司,面向全世界提供网页服务,如果使用TCP,最多也就3200个人同时看网页。
于是服务器要求“所有客户端,打开网页之后,必须关闭TCP连接”。这就是(猜测的)HTTP的初衷了。
按照这个协议,服务器接受TCP连接,几秒钟之内读取数据,检验之后,回复数据,断开连接。所谓的节省“资源”也没说明白到底节省了什么“资源”。
等到二十年后,平行宇宙的2004年,QQ桌面版好好的,QQ网页版用的越来越多。由于浏览器都是连接之后很快断开,QQ网页版,只能靠各种polling方式持续交互数据(HTTP keep-alive也有自己的缺点,其他答主讲的很好),浪费大量的带宽(这时候带宽的费用就大了),同时客户端收到消息也不及时,还有各种其它问题。
QQ网页版想直接用TCP协议长时间连接,但是QQ网页版能做的,都是浏览器允许做的。可以说,websocket的出现,就是因为浏览器不支持TCP直连,不给开后门。
于是“希望所有的浏览器都能够直接进行TCP连接”,于是浏览器出现了websocket协议。
所以,因为某些原因,人们在TCP上面弄了一个HTTP协议,把TCP支持的一些特性删除了,然后若干年之后想要那些被删除的特性,返回TCP,于是出现了WebSocket。
WebSocket实际上可以看作HTTP的升级!“不是WebSocket基于HTTP,而是可以看成HTTP基于WebSocket”。
好了,基于以上的理论知识我们知道了,长连接并不等于持久连接,长连接只是说我在发送的过程我可以不断的发,没事!也就是说如果你断的从客户端写消息到服务器,这个连接不会断的,但是不能说我停下来接收消息,这时候连接就断了。再也接不上了。(socket_write或者fputs、fwrite之类的写,但是不能socket_read、fgets、fread)
好了,那么怎么干,keep-alive不就好了,但是又有人说这个浪费资源,什么C10K问题、占用资源理论,总之这玩意不好,也不是我们想要的方法,那么接着看,好了,有了websocket是什么呢?是协议,是把当初HTTP上出TCP的部分给添加回来了,这就是websocket的来源,那么基于网络编程中,一切编程皆协议的理论来说,我们只需要完成握手协议即可,那么说干咱就干,搞起,上代码!
服务端:
<?php class SocketServer { private $_host; private $_port; private $_socket; private $_maxuser = 0; private $_socket_connect = [];//socket存储的数组 private $_isHand = []; public function __construct($host,$port){ $this -> _host = $host; $this -> _port = $port; } /** *socket连接 */ public function socketConnect(){ //创建一个socket对象 $this -> _socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); //允许使用本地地址 socket_set_option($this->_socket, SOL_SOCKET, SO_REUSEADDR, TRUE); socket_bind($this->_socket, $this->_host, $this->_port); //监听端口,并且达到一定的数量后,就会停止 socket_listen($this->_socket,$this->_maxuser); $this->_socket_connect[] = $this->_socket; echo "listen is success\n\n"; } private function socketAccept(){ //因为后续执行socket_select()将会只剩下活跃的socket,将不活跃的进程从数组中拿掉,所以不能拿连接数组去做,做一次copy赋值 $activity_socket_connect = $this->_socket_connect; //第一参数,所有连接中的socket, //$write表示监听写操作,如果传空代表不关心写操作, //$except代表监听从socket数组中去掉部分,null代表监听全部 //最后一个参数,0代表立马结束,>0代表多少秒之后结束,null代表监听到才结束,做阻塞进程 $write = null; $except = null; socket_select($activity_socket_connect,$write, $except, null); //下来了,那么说明这时候已经读到消息了,但是不知道哪些消息下来了,所以需要遍历所有活跃中的socket //这时候有一个说法:1、如果新连接到来的时候,那么将是监听的socket活跃;2、如果是新的数据或者客户端断开了,那么活跃的将是客户端的socket; var_dump(count($activity_socket_connect)); //如果监听socket活跃,那么必然有新的连接到来 if(in_array($this->_socket,$activity_socket_connect)){ echo "listen socket is activity,so your should create a new socket connect\n\n"; $accept = @socket_accept($this->_socket); if($accept === false){ //说明没有收到消息,那么就是一次没意义的操作 //to do something echo "new socket connect empty\n\n"; } if($accept < 0){ echo "new socket connect error: ".socket_strerror($connection)."\n\n"; } if($accept > 0){ if(!@socket_recv($accept,$data, 1024, 0) || !$data){ echo "because the data is empty so don't to connect\n\n"; }else{ //创建一个新的连接 $res = $this->add_accept($accept,$data); if($res){ echo "new socket connect success \n\n"; }else{ echo "new socket connect error \n\n"; } } } //将自己从数组中剔除 $key = array_search($this->_socket,$activity_socket_connect); unset($activity_socket_connect[$key]); } var_dump(count($activity_socket_connect)); //接下来就是一系列连接的循环 foreach($activity_socket_connect as $client_connect){ //判断是否异常的连接出现,以后多进程,更有可能会出现这种状况的,所以做一次防止操作 if(!in_array($client_connect,$this->_socket_connect)){ //这里就是一个新的,数据没有及时统计进来的一个连接,这时候无法处理,按道理是不可能出现的 //所以为了避免异常,将其关闭,另外就是continue执行即可 //1、关闭 $this->close($client_connect); continue; } //以下连接就属于正常的连接了 //1、接收数据 if(!@socket_recv($client_connect,$data, 1024, 0) || !$data){ $this->close($client_connect); echo "socket connect close\n\n"; continue; }else{ //输出得到的数据 echo "get data:\n\n"; var_dump($this->decode($data)); echo "----------success-----------\n\n"; $this->send($client_connect,'hello world'); } } } //增加一个初次连接的用户 private function add_accept($accept,$data) { //握手 if($this->upgrade($accept,$data)){ $this->_socket_connect[] = $accept; return true; } echo "client num is ".(count($this->_socket_connect)-1)."\n"; return false; } //响应升级协议 private function upgrade($accept, $data) { if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$data,$match)) { $key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: " . $key . "\r\n\r\n"; //必须以两个回车结尾 socket_write($accept, $upgrade, strlen($upgrade)); echo "upgrade is success\n\n"; return true; }else{ //握手失败 echo "upgrade is error\n\n"; return false; } } //关闭一个连接 private function close($accept) { $index = array_search($accept, $this->_socket_connect); socket_close($accept); unset($this->_socket_connect[$index]); echo "client num is ".(count($this->_socket_connect)-1)."\n"; } function decode($buffer) { $len = $masks = $data = $decoded = null; $len = ord($buffer[1]) & 127; if ($len === 126) { $masks = substr($buffer, 4, 4); $data = substr($buffer, 8); } else if ($len === 127) { $masks = substr($buffer, 10, 4); $data = substr($buffer, 14); } else { $masks = substr($buffer, 2, 4); $data = substr($buffer, 6); } // for ($index = 0; $index < strlen($data); $index++) { $decoded .= $data[$index] ^ $masks[$index % 4]; } return $decoded; } // 返回帧信息处理 function frame($s) { $a = str_split($s, 125); if (count($a) == 1) { return "\x81" . chr(strlen($a[0])) . $a[0]; } $ns = ""; foreach ($a as $o) { $ns .= "\x81" . chr(strlen($o)) . $o; } return $ns; } // 返回数据 function send($client, $msg) { $msg = $this->frame($msg); socket_write($client, $msg, strlen($msg)); } public function run(){ while(true){ $this->socketAccept(); usleep(500);//让CPU调整一下 } } } //调用SocketServer类 $socketServer = new SocketServer('127.0.0.1',2008); $socketServer->socketConnect(); $socketServer->run(); ?>
客户端:
<html> <head> <meta charset="utf-8"> <title>客户端测试</title> </head> <body> hello world! <button onclick="sendMessage()">发送消息</button> </body> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:2008"); ws.onopen = function(){ console.log("success"); }; ws.onerror = function(){ console.log("error"); }; ws.onclose = function(){ console.log('close'); }; ws.onmessage = function(event){ console.log(event.data); }; var i = 0; function sendMessage(){ ws.send("ssss"+(i++)); } </script> </html>
核心知识点:
socket_select
① 做阻塞用的,他的第一个参数read在执行前是要监听的所有连接,当他返回的时候只有活跃连接留下了,第二个参数关不关心写操作,我们写NULL表示我们不关心,第三个参数是说你要在read数组中排除掉哪些不监听,第四个参数0包括大于0表示多少秒后返回,也就0代表立马返回,1代表秒后返回,如果是NULL,就代表监听到返回期间一直阻塞!
② 知识点二就是说socket_select返回的活跃的read参数数组中,如果有当前服务端监听的连接的话,就代表有新建立的连接,因为只有新建立的连接才会使服务端的监听连接活跃起来,其他的比如一个连接断开了,是当前他存在的连接活跃,这时候是没有消息的代表他断开了,如果有消息就代表他给服务端发送了消息
只要深刻体会了这个函数的意义及新建连接时候是让listen的活跃的逻辑,就基本可以完成整个编程成了!
有什么不足,希望大家指教