本篇文章更新于:2019年5月17日,禁止抄袭,否则必究。
最近做了一个活动,需要用到websocket实现实时通讯功能,所以总结一下自己的经验,以便小伙伴们少走弯路。
接下来我将讲述的是H5和php实现相互通讯的实例,冗余的话不多说,直接讲解代码。
首先要了解一下几个术语:
HTTP协议的特性:属于“请求-响应”模型,只有客户端发起了请求消息,服务器才能给出响应消息,没有请求,就没有响应;一个请求消息,服务器只能返回一个响应消息。有些特殊应用场景中,如“在线股票”、“聊天室”、”互动游戏”、“摇一摇活动”等,需要模拟呈现出“客户端不发请求,服务器也在不停的给出响应效果,若使用HTTP协议,只能使用“AJAX+定时器”来近似的实现——心跳请求,问题:心跳过慢则信息的实效性差,心跳过快则服务器压力太大!
WebSocket协议的特性:属于“广播-收听”模型,只要客户端连接到服务器上,就不再断开(永久连接),一方可以不停的给对方发消息,对方可以不给出响应。使用WS协议代替心跳请求,可以非常好的解决HTTP协议在某些应用中的不足。WS协议的服务器也有不足:永久连接限制了客户端的数量。
WebSocket应用程序必需两个程序:一个是前端,一个是后端。
WS服务器后端程序:
可以使用Java、PHP、C#、Node.js编写,本例的服务端采用第apche的php编写,如果想使用其他的语言,请百度,或者google等等。
1.第一步:搭建web PHP环境这里使用的apache的服务器进行运行,如果不懂的怎么搭建
环境的请点击 apache web环境搭建
2.在配置web站点域名以及相关的配置:在yum安装的apache环境里面配置文件在 /etc/httpd/conf.d/virtualhost.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | ##注意这里的192.168.254.225是局域网,如果是生成环境的就要使用公网IP进行设置 <VirtualHost 192.168.254.225:80> <Directory /var/www/html/websocket> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory> ServerAdmin webmaster@sky8g.com DocumentRoot /var/www/html/websocket #例如www.sky8g.com ServerName your_domain_name #短域名例如sky8g.com ServerAlias xxx.com #错误的文件日志记录 ErrorLog logs/sky8g_error_log #访问的日志记录 CustomLog logs/sky8g_access_log combined env=!image-request </VirtualHost> |
注意:这个只是配置http访问的。如果想使用https访问则要配置443端口,这里配置ssl请点击配置HTTP和配置SSL
3.第三步:在/var/www/html/目录下创建websocket目录,如果使用命令
1 | sudo mkdir websocket |
4.在websocket目录下创建文件前端显示页面文件index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | <html> <head> <title>WebSocket</title> <style> html,body{font:normal 0.9em arial,helvetica;} #log {width:440px; height:200px; border:1px solid #7F9DB9; overflow:auto;} #msg {width:330px;} </style> <script type="text/javascript" src="/lib/jquery/jquery-1.10.2.min.js"></script> <script> var socket; function init(){ var host = "ws://www.sky8g.com:8090/phpwebsocket.php"; try{ socket = new WebSocket(host); log('WebSocket - status '+socket.readyState); socket.onopen = function(msg){ log("Welcome - status "+this.readyState); }; socket.onmessage = function(msg){ log("Received: "+msg.data); }; socket.onclose = function(msg){ log("Disconnected - status "+this.readyState); }; } catch(ex){ log(ex); } $("msg").focus(); } function send(){ var txt,msg; txt = $("msg"); msg = txt.value; if(!msg){ alert("Message can not be empty"); return; } txt.value=""; txt.focus(); try{ socket.send(msg); log('Sent: '+msg); } catch(ex){ log(ex); } } function quit(){ log("Goodbye!"); socket.close(); socket=null; } // Utilities function $(id){ return document.getElementById(id); } function log(msg){ $("log").innerHTML+="<br>"+msg; } function onkey(event){ if(event.keyCode==13){ send(); } } </script> </head> <body onload="init()"> <h3>WebSocket v2.00</h3> <div id="log"></div> <input id="msg" type="textbox" onkeypress="onkey(event)"/> <button onclick="send()">Send</button> <button onclick="quit()">Quit</button> <div>Commands: hello, hi, name, age, date, time, thanks, bye</div> </body> </html> |
5.在websocket目录下创建文件服务器后端php文件phpwebsocket.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #!/php -q <?php // Run from command prompt > php -q chatbot.demo.php include "websocket.class.php"; // Extended basic WebSocket as ChatBot class ChatBot extends WebSocket{ function process($user,$msg){ $this->say("< ".$msg); switch($msg){ case "hello" : $this->send($user->socket,"hello human"); break; case "hi" : $this->send($user->socket,"zup human"); break; case "name" : $this->send($user->socket,"my name is Multivac, silly I know"); break; case "age" : $this->send($user->socket,"I am older than time itself"); break; case "date" : $this->send($user->socket,"today is ".date("Y.m.d")); break; case "time" : $this->send($user->socket,"server time is ".date("H:i:s")); break; case "thanks": $this->send($user->socket,"you're welcome"); break; case "bye" : $this->send($user->socket,"bye"); break; default : $this->send($user->socket,$msg." not understood"); break; } } } $master = new ChatBot("localhost",8090); |
第6步:在websocket目录下再次创建依赖的类文件websocket.class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | <?php // Usage: $master=new WebSocket("localhost",12345); class WebSocket{ var $master; var $sockets = array(); var $users = array(); var $debug = true; function __construct($address,$port){ error_reporting(E_ALL); set_time_limit(0); ob_implicit_flush(); $this->master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() failed"); socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) or die("socket_option() failed"); socket_bind($this->master, $address, $port) or die("socket_bind() failed"); socket_listen($this->master,20) or die("socket_listen() failed"); $this->sockets[] = $this->master; $this->say("Server Started : ".date('Y-m-d H:i:s')); $this->say("Listening on : ".$address." port ".$port); $this->say("Master socket : ".$this->master."\n"); if( $this->debug ) { $this->say("Debugging on\n"); } while(true){ $changed = $this->sockets; //添加这两个变量 //$write = array(); //$except = array(); socket_select($changed,$write=NULL,$except=NULL,NULL); foreach($changed as $socket){ if($socket==$this->master){ $client=socket_accept($this->master); if($client<0){ $this->log("socket_accept() failed"); continue; } else{ $this->connect($client); } } else{ $bytes = @socket_recv($socket,$buffer,2048,0); if($bytes==0){ $this->disconnect($socket); } else{ $user = $this->getuserbysocket($socket); if(!$user->handshake){ $this->dohandshake($user,$buffer); } else{ $this->process($user,$this->unwrap($buffer)); } } } } } } function process($user,$msg){ /* Extend and modify this method to suit your needs */ /* Basic usage is to echo incoming messages back to client */ $this->send($user->socket,$msg); } // unit test for message lengths of 124, 65535, 2^64 // FIXME hook this up function test_send($client) { $base = "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; for( $i =0; $i < 10; $i++ ) { $this->send($client,$base); $this->send($client,"".strlen($base)); $base = $base."a"; } } // FIXME throw error if message length is longer than 0x7FFFFFFFFFFFFFFF chracters function send($client,$data){ $this->say("> ".$data); $header = " "; $header[0] = chr(0x81); $header_length = 1; //Payload length: 7 bits, 7+16 bits, or 7+64 bits $dataLength = strlen($data); //The length of the payload data, in bytes: if 0-125, that is the payload length. if($dataLength <= 125) { $header[1] = chr($dataLength); $header_length = 2; } elseif ($dataLength <= 65535) { // If 126, the following 2 bytes interpreted as a 16 // bit unsigned integer are the payload length. $header[1] = chr(126); $header[2] = chr($dataLength >> 8); $header[3] = chr($dataLength & 0xFF); $header_length = 4; } else { // If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the // most significant bit MUST be 0) are the payload length. $header[1] = chr(127); $header[2] = chr(($dataLength & 0xFF00000000000000) >> 56); $header[3] = chr(($dataLength & 0xFF000000000000) >> 48); $header[4] = chr(($dataLength & 0xFF0000000000) >> 40); $header[5] = chr(($dataLength & 0xFF00000000) >> 32); $header[6] = chr(($dataLength & 0xFF000000) >> 24); $header[7] = chr(($dataLength & 0xFF0000) >> 16); $header[8] = chr(($dataLength & 0xFF00 ) >> 8); $header[9] = chr( $dataLength & 0xFF ); $header_length = 10; } $result = socket_write($client, $header . $data, strlen($data) + $header_length); //$result = socket_write($client, chr(0x81) . chr(strlen($data)) . $data, strlen($data) + 2); if ( !$result ) { $this->disconnect($client); $client = false; } $this->say("len(".strlen($data).")"); } function connect($socket){ $user = new User(); $user->id = uniqid(); $user->socket = $socket; array_push($this->users,$user); array_push($this->sockets,$socket); $this->log($socket." CONNECTED!"); $this->log(date("d/n/Y ")."at ".date("H:i:s T")); } function disconnect($socket){ $found=null; $n=count($this->users); for($i=0;$i<$n;$i++){ if($this->users[$i]->socket==$socket){ $found=$i; break; } } if(!is_null($found)){ array_splice($this->users,$found,1); } $index=array_search($socket,$this->sockets); socket_close($socket); $this->log($socket." DISCONNECTED!"); if($index>=0){ array_splice($this->sockets,$index,1); } } function dohandshake($user,$buffer){ $this->log("\nRequesting handshake..."); $this->log($buffer); list($resource,$host,$origin,$key1,$key2,$l8b,$key0) = $this->getheaders($buffer); $this->log("Handshaking..."); //$port = explode(":",$host); //$port = $port[1]; //$this->log($origin."\r\n".$host); $upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" . "Upgrade: WebSocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Origin: " . $origin . "\r\n" . "Sec-WebSocket-Accept: " . $this->calcKeyHybi10($key0) . "\r\n" . "\r\n" ; socket_write($user->socket,$upgrade,strlen($upgrade)); $user->handshake=true; $this->log($upgrade); $this->log("Done handshaking..."); return true; } function calcKeyHybi10($key){ $CRAZY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; $sha = sha1($key.$CRAZY,true); return base64_encode($sha); } function getheaders($req){ $r=$h=$o=null; if(preg_match("/GET (.*) HTTP/" ,$req,$match)){ $r=$match[1]; } if(preg_match("/Host: (.*)\r\n/" ,$req,$match)){ $h=$match[1]; } if(preg_match("/Origin: (.*)\r\n/" ,$req,$match)){ $o=$match[1]; } if(preg_match("/Sec-WebSocket-Key1: (.*)\r\n/",$req,$match)){ $this->log("Sec Key1: ".$sk1=$match[1]); } if(preg_match("/Sec-WebSocket-Key2: (.*)\r\n/",$req,$match)){ $this->log("Sec Key2: ".$sk2=$match[1]); } if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/" ,$req,$match)){ $this->log("new Sec Key2: ".$sk0=$match[1]); } if($match=substr($req,-8)) { $this->log("Last 8 bytes: ".$l8b=$match); } return array($r,$h,$o,$sk1,$sk2,$l8b,$sk0); } function getuserbysocket($socket){ $found=null; foreach($this->users as $user){ if($user->socket==$socket){ $found=$user; break; } } return $found; } function say($msg=""){ echo $msg."\n"; } function log($msg=""){ if($this->debug){ echo $msg."\n"; } } function wrap($msg=""){ return chr(0).$msg.chr(255); } // copied from http://lemmingzshadow.net/386/php-websocket-serverclient-nach-draft-hybi-10/ function unwrap($data="") { $bytes = $data; $dataLength = ''; $mask = ''; $coded_data = ''; $decodedData = ''; $secondByte = sprintf('%08b', ord($bytes[1])); $masked = ($secondByte[0] == '1') ? true : false; $dataLength = ($masked === true) ? ord($bytes[1]) & 127 : ord($bytes[1]); if($masked === true) { if($dataLength === 126) { $mask = substr($bytes, 4, 4); $coded_data = substr($bytes, 8); } elseif($dataLength === 127) { $mask = substr($bytes, 10, 4); $coded_data = substr($bytes, 14); } else { $mask = substr($bytes, 2, 4); $coded_data = substr($bytes, 6); } for($i = 0; $i < strlen($coded_data); $i++) { $decodedData .= $coded_data[$i] ^ $mask[$i % 4]; } } else { if($dataLength === 126) { $decodedData = substr($bytes, 4); } elseif($dataLength === 127) { $decodedData = substr($bytes, 10); } else { $decodedData = substr($bytes, 2); } } return $decodedData; } } //class WebSocket class User{ var $id; var $socket; var $handshake; } ?> |
第7步:使用命令php 在安装的目录执行文件phpwebsocket.php
1 | php -q phpwebsocket.php |
如果出现了如下图所示错误则:
解决办法是:在websocket.class.php文件里面第27行修改函数 socket_select 传值的参数
为如下
1 2 3 4 | //下面这两个值必须为数组类型的。 $write = array(); $except = array(); socket_select($changed,$write,$except,NULL); |
第8步:再次执行php -q phpwebsocket.php命令,执行ok无报错
第8步:访问你的配置的域名例如:www.sky8g.com则显示下面图所示
打开调试模式如果出现了错误
(index):16 WebSocket connection to ‘ws://www.sky8g.com:8090/phpwebsocket.php‘ failed: Error during WebSocket handshake: Unexpected response code: 200
解决办法:俩个地方需要检查第一个地方还是前端index.html里面的的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var socket; function init(){ //是否是这样的 var host = "ws://www.sky8g.com:8090/phpwebsocket.php"; try{ socket = new WebSocket(host); log('WebSocket - status '+socket.readyState); socket.onopen = function(msg){ log("Welcome - status "+this.readyState); }; socket.onmessage = function(msg){ log("Received: "+msg.data); }; socket.onclose = function(msg){ log("Disconnected - status "+this.readyState); }; } catch(ex){ log(ex); } $("msg").focus(); } |
第二个地方是最常见的地方就是文件phpwebsocket.php最后一行的实例的localhost 应该是是你绑定的ip地址
1 2 3 4 5 | $master = new ChatBot("localhost",8090); //localhost 应该改为192.168.254.225 或者 0.0.0.0万能的 即使: $master = new ChatBot("192.168.254.225",8090); |
再次访问刷新页面无报错。
发送hello会返回信息OK
如果下载完整版的代码请点击这里。