WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。因此,在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。
现在,很多网站为了实现推送技术,所用的技术都是Ajax轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。
这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求。然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。HTML5定义的WebSocket协议优势如下:
1、小Header,互相沟通的Header非常小,只有2Bytes左右。
2、服务器不再被动接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。
3、WebSocket协议能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

▪ Websocket协议由RFC 6455定义,协议分为两个部分: 握手阶段和全双工通信阶段。
客户端发送的header内容
- GET /nickname11 HTTP/1.1
- Host: 127.0.0.1:9090
- Connection: Upgrade
- Upgrade: websocket
- Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
- Sec-WebSocket-Key: wJdg8v4EJiDsIZg5+s0hY8RUQ2A=
- Sec-WebSocket-Version: 13
- Origin: 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 Protocol
- Upgrade: WebSocket
- Sec-WebSocket-Version: 13
- Connection: Upgrade
- Sec-WebSocket-Accept: wJdg8v4EJiDsIZg5+s0hY8RUQ2A=
▪ Websocket协议的握手阶段是使用的HTTP协议。
▪ Websocket协议的“全双工”消息通信是基于 TCP/IP 的协议集之上的,客户端和服务端可随时发送数据。协议连接是“ws”或者加密的“wss”。
▪ 通信的数据是基于“帧(frame)”的,可以传输文本数据,也可以直接传输二进制数据,效率高。
一条消息(message)可由一个或多个帧(Frame)组成,很多时候会将帧和消息混用,因为大部分时候一条消息只使用一个帧
1、server.php(服务端)
-
- header('Content-Type:application/json; charset=utf-8');
-
- class server{
-
- protected $sockets;
- protected $users;
- protected $master;
-
- protected $ip = '0.0.0.0';
- protected $port = '9090';
- protected $backlog = 5; //排队等候的连接队列最大值
- protected $length = 1024*8; //可读取的最大字节数
- protected $redisIp = '127.0.0.1';
- protected $redisPort = 6379;
- protected $redisLength = 1024*600;
-
- public function __construct(){
-
- $this->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();
- }
-
2、getredis.php(获取存在redis的历史消息)
-
- //查看redis里全部的聊天信息
- $act = isset($_POST['act']) ? $_POST['act'] : '';
- if(!$act){
- echo json_encode(['code'=>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;
3、chat.html(客户端)
- html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>WebSocket聊天室title>
- head>
- <style type="text/css">
-
- body{
- font-size:14px;
- }
- h4{
- text-align: center;
- font-size:16px
- }
- .divBox{
- width:30%;
- float:left;
- border: 0.5px solid #bbb0b0;
- padding: 10px;
- margin-left: 10px;
- }
- .content{
- width: 100%;
- height: 500px;
- overflow-y: scroll;
- }
-
- .chat{
- width: 100%;
- height: 500px;
- overflow-y: scroll;
- }
-
- style>
- <body>
- <div class="divBox">
- <h4>状态栏h4>
- <p>当前用户:<span id="username">span> 在线情况:<span style="color:red" id="situation">离线span>
- <button onclick="createWebsocket()">重新连接websocketbutton>
-
- p>
- <textarea id="textarea" style="width:260px;height: 100px">textarea>
- <br/>
- <input type="button" value="发送数据" id="send">
- div>
-
- <div class="divBox">
- <h4>聊天记录栏h4>
- <div class="chat">div>
- div>
-
- <div class="divBox">
- <h4>webSocket事件输出栏h4>
- <div class="content">div>
- div>
-
- body>
- <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js">script>
- <script type="text/javascript">
-
- var date = new Date();
- let username = prompt("请输入您的昵称", "nickname11");
- username = username.replace(":", "");
- $("#username").html(username);
- var is_online = 0;
-
- //创建webSocket连接
- createWebsocket();
-
- //获取存在redis的历史聊天记录
- setTimeout(getAllMsg(), 3*1000);
-
- function createWebsocket(){
- let ws = new WebSocket("ws://127.0.0.1:9090/"+username);
-
- ws.onopen = function(){
- $(".content").append("连接成功..." + "
"); - is_online = 1;
-
- // 点击发送数据
- $("#send").click(function(){
-
- var data = $("#textarea").val();
- if(data){
- ws.send(username+ ":"+ data);
- $("#textarea").blur();
- $("#textarea").val("");
- }
- })
- }
-
- ws.onmessage = function(event){
- var data = $.parseJSON(event.data);
- var chatStr = '';
- if(data.username == username){
- chatStr += "" + data.username + "(本人)";
- }else{
- chatStr += "" + data.username + "";
- }
-
- chatStr += ":" + data.msg + " " + data.time + "
"; - $(".chat").append(chatStr);
- }
-
- ws.onclose = function(event){
- $(".content").append("websocket 断开: " + event.code + " " + event.reason + " " + event.wasClean + "
"); - $(".content").append("连接已关闭" + "
"); - is_online = 0;
- }
-
- ws.onerror = function(event){
- console.log(event.data);
- }
-
- }
-
- function send() {
- var data = document.getElementById('textarea').value;
- ws.send(username+ ":"+ data);
- }
-
- function getAllMsg(){
- //获取消息内容
- $.post("http://127.0.0.1/websocket/getredis.php", {act:"getAllMsg"}, function(res){
-
- var res = $.parseJSON(res);
- console.log(res);
- var chatStr = "";
- $.each(res.data, function(k, v){
-
- if(v.username == username){
- chatStr += "" + v.username + "(本人)";
- }else{
- chatStr += "" + v.username + "";
- }
-
- chatStr += ":" + v.msg + " " + v.time + "
"; - });
- $(".chat").html(chatStr);
- });
- }
-
- function closeWebsocket(){
-
-
- }
-
- setInterval(function () {
- if(is_online == 1){
- $("#situation").html("在线");
- $("#situation").css({"color":"green"});
- }else{
- $("#situation").html("离线");
- $("#situation").css({"color": "red"});
- }
-
- }, 2*1000)
-
-
-
-
-
-
- script>
- html>