WebSocket的通信原理和使用

WebSocket的通信原理和使用

一、什么是WebSocket?

1.1 简介

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。因此,在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。

1.2 WebSocket的优势

现在,很多网站为了实现推送技术,所用的技术都是Ajax轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。

这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求。然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。HTML5定义的WebSocket协议优势如下:

1、小Header,互相沟通的Header非常小,只有2Bytes左右。
2、服务器不再被动接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。
3、WebSocket协议能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

1.3 WebSocket的原理 

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)组成,很多时候会将帧和消息混用,因为大部分时候一条消息只使用一个帧

二、使用PHP实现WebSocket通信

1、server.php(服务端) 

<?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 "<pre/>";
        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的历史消息) 

<?php
//查看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(客户端)

<!DOCTYPE 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>&nbsp;在线情况:<span style="color:red" id="situation">离线</span>
  &nbsp;&nbsp;&nbsp;<button onclick="createWebsocket()">重新连接websocket</button>
  <!-- &nbsp;&nbsp;&nbsp; <button onclick="closeWebsocket()">关闭websocket</button> -->
  </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("连接成功..." + "<br/>");
      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 += "<font color='grey'>" + data.username + "</font><font color='green'>(本人)</font>";
        }else{
          chatStr += "<font color='grey'>" + data.username + "</font>";
        }
        chatStr += ":<font style='font-size:16px'>" + data.msg + "</font>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font color='grey'>" + data.time + "</font><br/><br/>";
        $(".chat").append(chatStr);
    }
    ws.onclose = function(event){
        $(".content").append("websocket 断开: " + event.code + " " + event.reason + " " + event.wasClean + "<br/>");
        $(".content").append("连接已关闭" + "<br/>");
      	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 += "<font color='grey'>" + v.username + "</font><font color='green'>(本人)</font>";
          }else{
            chatStr += "<font color='grey'>" + v.username + "</font>";
          }
          chatStr += ":<font style='font-size:16px'>" + v.msg + "</font>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font color='grey'>" + v.time + "</font><br/><br/>";
        });
        $(".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>
                       

点击阅读全文

上一篇 2023年 5月 25日 am10:50
下一篇 2023年 5月 25日 am10:54