一、什么是WebSocket?
1.1 简介
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。因此,在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。
1.2 WebSocket的优势
现在,很多网站为了实现推送技术,所用的技术都是Ajax轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。
这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求。然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。HTML5定义的WebSocket协议优势如下:
小Header,互相沟通的Header非常小,只有2Bytes左右。
2、服务器不再被动接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。
3、WebSocket协议能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
1.3 WebSocket的原理
▪ Websocket协议由RFC 6455定义,协议分为两个部分: 握手阶段和全双工通信阶段。
客户端发送的header内容
GET /nickname11 HTTP/1.1 Host: 127.0.0.1:9090Connection: UpgradeUpgrade: websocketSec-WebSocket-Extensions: permessage-deflate; client_max_window_bitsSec-WebSocket-Key: wJdg8v4EJiDsIZg5+s0hY8RUQ2A=Sec-WebSocket-Version: 13Origin: http://127.0.0.1
服务端响应的header内容,这里的Sec-WebSocket-Accept要根据发送的Sec-WebSocket-Key来处理算出来,计算方法:base64_encode(sha1(websocket_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)) 。
HTTP/1.1 101 Switching ProtocolUpgrade: WebSocketSec-WebSocket-Version: 13Connection: UpgradeSec-WebSocket-Accept: wJdg8v4EJiDsIZg5+s0hY8RUQ2A=
▪ Websocket协议的握手阶段是使用的HTTP协议。
▪ Websocket协议的“全双工”消息通信是基于 TCP/IP 的协议集之上的,客户端和服务端可随时发送数据。协议连接是“ws”或者加密的“wss”。
▪ 通信的数据是基于“帧(frame)”的,可以传输文本数据,也可以直接传输二进制数据,效率高。
一条消息(message)可由一个或多个帧(Frame)组成,很多时候会将帧和消息混用,因为大部分时候一条消息只使用一个帧
二、使用PHP实现WebSocket通信
server.php(服务端)
master = $this->createWebSocket(); //创建socket连接池 $this->sockets=array($this->master); }public function start(){while (true) {$changes=$this->sockets; $write=NULL; $except=NULL; //设置非阻塞,让多个连接能同时正常往下执行@socket_select($changes, $write, $except, NULL);foreach($changes as $socket){//判断是否新的socket连接if($socket == $this->master){$client=socket_accept($socket);$key=uniqid();$this->sockets[]=$client;$this->users[$key]=array( 'client'=>$client, 'is_shake'=>0 );}else{$len=0; $buffer='';do{ $l=socket_recv($socket,$buf,1024,0); $len+=$l; $buffer.=$buf; }while($l==1024);$key = $this->search($socket);// 如果接收的信息长度小于7,则该client的socket为断开连接if($len<7){ unset($this->users[$key]); socket_close($socket); continue; } //判断连接是否已握手 if(!$this->users[$key]['is_shake']){ $this->shake($key, $buffer); }else{ //接收客户端发送消息 $buffer = $this->getMsg($buffer); if($buffer === false){continue; } //发送消息 $this->sendMsg($key,$buffer); }}}}}protected function intoRedis($data){$redis = new Redis();$redis->pconnect($this->redisIp, $this->redisPort, $this->redisLength);$redis->lpush("ws_".$this->getMd5Key($data['username']), json_encode($data));return true;}protected function search($socket){ foreach ($this->users as $key=>$val){ if($socket==$val['client']) return $key; } return false; } protected function shake($key, $buf) { preg_match("/Sec-WebSocket-Key: (.*)\r\n/i",$buf,$match); //用于服务端计算Sec_WebSocket_Accept的固定的字符串 $keyStr = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; $res= "HTTP/1.1 101 Switching Protocol".PHP_EOL ."Upgrade: WebSocket".PHP_EOL ."Sec-WebSocket-Version: 13".PHP_EOL ."Connection: Upgrade".PHP_EOL ."Sec-WebSocket-Accept: " . base64_encode(sha1($match[1].$keyStr ,true)) .PHP_EOL.PHP_EOL; // 注意需要两个换行 // 向客户端应答 Sec-WebSocket-Accept socket_write($this->users[$key]['client'], $res, strlen($res)); //对已经握手的client做标志 $this->users[$key]['is_shake'] = 1; return true; }protected function sendMsg($key, $buffer){$index = strpos($buffer, ":"); $data = [ 'username' => substr($buffer, 0, $index), 'msg' => substr($buffer, ($index+1)), 'time' => date("Y-m-d H:i:s", time()), ]; foreach($this->users as $val){ $msg = $this->buildMsg(json_encode($data)); socket_write($val['client'], $msg, strlen($msg)); } //通过redis记录消息 $this->intoRedis($data); echo ""; print_r($data);}// 编码服务端向客户端发送的内容protected function buildMsg($msg) { $frame = []; $frame[0] = '81'; $len = strlen($msg); if ($len < 126) { $frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len); } else if ($len < 65025) { $s = dechex($len); $frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s; } else { $s = dechex($len); $frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s; } $data = ''; $l = strlen($msg); for ($i = 0; $i < $l; $i++) { $data .= dechex(ord($msg{$i})); } $frame[2] = $data; $data = implode('', $frame); return pack("H*", $data);}// 解析客户端向服务端发送的内容protected function getMsg($buffer) { $res = ''; $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++) { $res .= $data[$index] ^ $masks[$index % 4]; } return $res;}//建立WebSocket链接protected function createWebSocket(){ $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);//1代表接受所有的数据包 socket_bind($server, $this->ip, $this->port); socket_listen($server); echo 'Socket连接创建成功,时间: '.date('Y-m-d H:i:s').PHP_EOL; return $server;}protected function getMd5Key($username){return md5($username."WebSocket");}}$server = new server();$act = isset($_POST['act']) ? $_POST['act'] : 'start';if($act == 'start'){$server->start();}else if($act == 'getAllMsg'){$server->getRedis();}
getredis.php(获取存在redis的历史消息)
500, 'msg'=>'act参数不能为空', 'data'=>[]]);exit;}if($act != 'getAllMsg'){echo json_encode(['code'=>500, 'msg'=>'act传参错误', 'data'=>[]]);exit;}$redisIp = '127.0.0.1';$redisPort = 6379;$redisLength = 1024*600;$redis = new Redis();$redis->pconnect($redisIp, $redisPort, $redisLength);$keys = $redis->keys("ws_*");$data = [];if($keys){foreach($keys as $key){$res = $redis->lGetRange($key, 0, -1);if($res){foreach($res as &$val){$val = json_decode($val, JSON_UNESCAPED_UNICODE);$val['time_stamp'] = strtotime($val['time']);}$data = array_merge($res, $data);}}}if($data){$sort = array_column($data, 'time_stamp');array_multisort($sort, SORT_ASC, $data);}echo json_encode(['code'=>200, 'msg'=>'获取成功', 'data'=>$data]);exit;
chat.html(客户端)
WebSocket聊天室 状态栏
当前用户: 在线情况:离线
聊天记录栏
webSocket事件输出栏
来源地址:https://blog.csdn.net/m0_68949064/article/details/127729569