• 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协议由RFC 6455定义,协议分为两个部分: 握手阶段和全双工通信阶段。

      客户端发送的header内容 

    1. GET /nickname11 HTTP/1.1
    2. Host: 127.0.0.1:9090
    3. Connection: Upgrade
    4. Upgrade: websocket
    5. Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    6. Sec-WebSocket-Key: wJdg8v4EJiDsIZg5+s0hY8RUQ2A=
    7. Sec-WebSocket-Version: 13
    8. Origin: http://127.0.0.1

      服务端响应的header内容,这里的Sec-WebSocket-Accept要根据发送的Sec-WebSocket-Key来处理算出来,计算方法:base64_encode(sha1(websocket_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)) 。

    1. HTTP/1.1 101 Switching Protocol
    2. Upgrade: WebSocket
    3. Sec-WebSocket-Version: 13
    4. Connection: Upgrade
    5. Sec-WebSocket-Accept: wJdg8v4EJiDsIZg5+s0hY8RUQ2A=

    ▪ Websocket协议的握手阶段是使用的HTTP协议。

    ▪ Websocket协议的“全双工”消息通信是基于 TCP/IP 的协议集之上的,客户端和服务端可随时发送数据。协议连接是“ws”或者加密的“wss”。

    ▪  通信的数据是基于“帧(frame)”的,可以传输文本数据,也可以直接传输二进制数据,效率高。

     一条消息(message)可由一个或多个帧(Frame)组成,很多时候会将帧和消息混用,因为大部分时候一条消息只使用一个帧

    二、使用PHP实现WebSocket通信

    1、server.php(服务端) 

    1. header('Content-Type:application/json; charset=utf-8');
    2. class server{
    3. protected $sockets;
    4. protected $users;
    5. protected $master;
    6. protected $ip = '0.0.0.0';
    7. protected $port = '9090';
    8. protected $backlog = 5; //排队等候的连接队列最大值
    9. protected $length = 1024*8; //可读取的最大字节数
    10. protected $redisIp = '127.0.0.1';
    11. protected $redisPort = 6379;
    12. protected $redisLength = 1024*600;
    13. public function __construct(){
    14. $this->master = $this->createWebSocket();
    15. //创建socket连接池
    16. $this->sockets=array($this->master);
    17. }
    18. public function start(){
    19. while (true) {
    20. $changes=$this->sockets;
    21. $write=NULL;
    22. $except=NULL;
    23. //设置非阻塞,让多个连接能同时正常往下执行
    24. @socket_select($changes, $write, $except, NULL);
    25. foreach($changes as $socket){
    26. //判断是否新的socket连接
    27. if($socket == $this->master){
    28. $client=socket_accept($socket);
    29. $key=uniqid();
    30. $this->sockets[]=$client;
    31. $this->users[$key]=array(
    32. 'client'=>$client,
    33. 'is_shake'=>0
    34. );
    35. }else{
    36. $len=0;
    37. $buffer='';
    38. do{
    39. $l=socket_recv($socket,$buf,1024,0);
    40. $len+=$l;
    41. $buffer.=$buf;
    42. }while($l==1024);
    43. $key = $this->search($socket);
    44. // 如果接收的信息长度小于7,则该client的socket为断开连接
    45. if($len<7){
    46. unset($this->users[$key]);
    47. socket_close($socket);
    48. continue;
    49. }
    50. //判断连接是否已握手
    51. if(!$this->users[$key]['is_shake']){
    52. $this->shake($key, $buffer);
    53. }else{
    54. //接收客户端发送消息
    55. $buffer = $this->getMsg($buffer);
    56. if($buffer === false){
    57. continue;
    58. }
    59. //发送消息
    60. $this->sendMsg($key,$buffer);
    61. }
    62. }
    63. }
    64. }
    65. }
    66. protected function intoRedis($data)
    67. {
    68. $redis = new Redis();
    69. $redis->pconnect($this->redisIp, $this->redisPort, $this->redisLength);
    70. $redis->lpush("ws_".$this->getMd5Key($data['username']), json_encode($data));
    71. return true;
    72. }
    73. protected function search($socket)
    74. {
    75. foreach ($this->users as $key=>$val){
    76. if($socket==$val['client'])
    77. return $key;
    78. }
    79. return false;
    80. }
    81. protected function shake($key, $buf)
    82. {
    83. preg_match("/Sec-WebSocket-Key: (.*)\r\n/i",$buf,$match);
    84. //用于服务端计算Sec_WebSocket_Accept的固定的字符串
    85. $keyStr = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
    86. $res= "HTTP/1.1 101 Switching Protocol".PHP_EOL
    87. ."Upgrade: WebSocket".PHP_EOL
    88. ."Sec-WebSocket-Version: 13".PHP_EOL
    89. ."Connection: Upgrade".PHP_EOL
    90. ."Sec-WebSocket-Accept: " . base64_encode(sha1($match[1].$keyStr ,true)) .PHP_EOL.PHP_EOL; // 注意需要两个换行
    91. // 向客户端应答 Sec-WebSocket-Accept
    92. socket_write($this->users[$key]['client'], $res, strlen($res));
    93. //对已经握手的client做标志
    94. $this->users[$key]['is_shake'] = 1;
    95. return true;
    96. }
    97. protected function sendMsg($key, $buffer)
    98. {
    99. $index = strpos($buffer, ":");
    100. $data = [
    101. 'username' => substr($buffer, 0, $index),
    102. 'msg' => substr($buffer, ($index+1)),
    103. 'time' => date("Y-m-d H:i:s", time()),
    104. ];
    105. foreach($this->users as $val){
    106. $msg = $this->buildMsg(json_encode($data));
    107. socket_write($val['client'], $msg, strlen($msg));
    108. }
    109. //通过redis记录消息
    110. $this->intoRedis($data);
    111. echo "
      ";
    112. print_r($data);
    113. }
    114. // 编码服务端向客户端发送的内容
    115. protected function buildMsg($msg) {
    116. $frame = [];
    117. $frame[0] = '81';
    118. $len = strlen($msg);
    119. if ($len < 126) {
    120. $frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
    121. } else if ($len < 65025) {
    122. $s = dechex($len);
    123. $frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s;
    124. } else {
    125. $s = dechex($len);
    126. $frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s;
    127. }
    128. $data = '';
    129. $l = strlen($msg);
    130. for ($i = 0; $i < $l; $i++) {
    131. $data .= dechex(ord($msg{$i}));
    132. }
    133. $frame[2] = $data;
    134. $data = implode('', $frame);
    135. return pack("H*", $data);
    136. }
    137. // 解析客户端向服务端发送的内容
    138. protected function getMsg($buffer) {
    139. $res = '';
    140. $len = ord($buffer[1]) & 127;
    141. if ($len === 126) {
    142. $masks = substr($buffer, 4, 4);
    143. $data = substr($buffer, 8);
    144. } else if ($len === 127) {
    145. $masks = substr($buffer, 10, 4);
    146. $data = substr($buffer, 14);
    147. } else {
    148. $masks = substr($buffer, 2, 4);
    149. $data = substr($buffer, 6);
    150. }
    151. for ($index = 0; $index < strlen($data); $index++) {
    152. $res .= $data[$index] ^ $masks[$index % 4];
    153. }
    154. return $res;
    155. }
    156. //建立WebSocket链接
    157. protected function createWebSocket(){
    158. $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    159. socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);//1代表接受所有的数据包
    160. socket_bind($server, $this->ip, $this->port);
    161. socket_listen($server);
    162. echo 'Socket连接创建成功,时间: '.date('Y-m-d H:i:s').PHP_EOL;
    163. return $server;
    164. }
    165. protected function getMd5Key($username)
    166. {
    167. return md5($username."WebSocket");
    168. }
    169. }
    170. $server = new server();
    171. $act = isset($_POST['act']) ? $_POST['act'] : 'start';
    172. if($act == 'start'){
    173. $server->start();
    174. }else if($act == 'getAllMsg'){
    175. $server->getRedis();
    176. }

    2、getredis.php(获取存在redis的历史消息) 

    1. //查看redis里全部的聊天信息
    2. $act = isset($_POST['act']) ? $_POST['act'] : '';
    3. if(!$act){
    4. echo json_encode(['code'=>500, 'msg'=>'act参数不能为空', 'data'=>[]]);
    5. exit;
    6. }
    7. if($act != 'getAllMsg'){
    8. echo json_encode(['code'=>500, 'msg'=>'act传参错误', 'data'=>[]]);
    9. exit;
    10. }
    11. $redisIp = '127.0.0.1';
    12. $redisPort = 6379;
    13. $redisLength = 1024*600;
    14. $redis = new Redis();
    15. $redis->pconnect($redisIp, $redisPort, $redisLength);
    16. $keys = $redis->keys("ws_*");
    17. $data = [];
    18. if($keys){
    19. foreach($keys as $key){
    20. $res = $redis->lGetRange($key, 0, -1);
    21. if($res){
    22. foreach($res as &$val){
    23. $val = json_decode($val, JSON_UNESCAPED_UNICODE);
    24. $val['time_stamp'] = strtotime($val['time']);
    25. }
    26. $data = array_merge($res, $data);
    27. }
    28. }
    29. }
    30. if($data){
    31. $sort = array_column($data, 'time_stamp');
    32. array_multisort($sort, SORT_ASC, $data);
    33. }
    34. echo json_encode(['code'=>200, 'msg'=>'获取成功', 'data'=>$data]);
    35. exit;

    3、chat.html(客户端)

    1. html>
    2. <html>
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>WebSocket聊天室title>
    6. head>
    7. <style type="text/css">
    8. body{
    9. font-size:14px;
    10. }
    11. h4{
    12. text-align: center;
    13. font-size:16px
    14. }
    15. .divBox{
    16. width:30%;
    17. float:left;
    18. border: 0.5px solid #bbb0b0;
    19. padding: 10px;
    20. margin-left: 10px;
    21. }
    22. .content{
    23. width: 100%;
    24. height: 500px;
    25. overflow-y: scroll;
    26. }
    27. .chat{
    28. width: 100%;
    29. height: 500px;
    30. overflow-y: scroll;
    31. }
    32. style>
    33. <body>
    34. <div class="divBox">
    35. <h4>状态栏h4>
    36. <p>当前用户:<span id="username">span> 在线情况:<span style="color:red" id="situation">离线span>
    37.    <button onclick="createWebsocket()">重新连接websocketbutton>
    38. p>
    39. <textarea id="textarea" style="width:260px;height: 100px">textarea>
    40. <br/>
    41. <input type="button" value="发送数据" id="send">
    42. div>
    43. <div class="divBox">
    44. <h4>聊天记录栏h4>
    45. <div class="chat">div>
    46. div>
    47. <div class="divBox">
    48. <h4>webSocket事件输出栏h4>
    49. <div class="content">div>
    50. div>
    51. body>
    52. <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js">script>
    53. <script type="text/javascript">
    54. var date = new Date();
    55. let username = prompt("请输入您的昵称", "nickname11");
    56. username = username.replace(":", "");
    57. $("#username").html(username);
    58. var is_online = 0;
    59. //创建webSocket连接
    60. createWebsocket();
    61. //获取存在redis的历史聊天记录
    62. setTimeout(getAllMsg(), 3*1000);
    63. function createWebsocket(){
    64. let ws = new WebSocket("ws://127.0.0.1:9090/"+username);
    65. ws.onopen = function(){
    66. $(".content").append("连接成功..." + "
      "
      );
    67. is_online = 1;
    68. // 点击发送数据
    69. $("#send").click(function(){
    70. var data = $("#textarea").val();
    71. if(data){
    72. ws.send(username+ ":"+ data);
    73. $("#textarea").blur();
    74. $("#textarea").val("");
    75. }
    76. })
    77. }
    78. ws.onmessage = function(event){
    79. var data = $.parseJSON(event.data);
    80. var chatStr = '';
    81. if(data.username == username){
    82. chatStr += "" + data.username + "(本人)";
    83. }else{
    84. chatStr += "" + data.username + "";
    85. }
    86. chatStr += ":" + data.msg + "     " + data.time + "

      ";
    87. $(".chat").append(chatStr);
    88. }
    89. ws.onclose = function(event){
    90. $(".content").append("websocket 断开: " + event.code + " " + event.reason + " " + event.wasClean + "
      "
      );
    91. $(".content").append("连接已关闭" + "
      "
      );
    92. is_online = 0;
    93. }
    94. ws.onerror = function(event){
    95. console.log(event.data);
    96. }
    97. }
    98. function send() {
    99. var data = document.getElementById('textarea').value;
    100. ws.send(username+ ":"+ data);
    101. }
    102. function getAllMsg(){
    103. //获取消息内容
    104. $.post("http://127.0.0.1/websocket/getredis.php", {act:"getAllMsg"}, function(res){
    105. var res = $.parseJSON(res);
    106. console.log(res);
    107. var chatStr = "";
    108. $.each(res.data, function(k, v){
    109. if(v.username == username){
    110. chatStr += "" + v.username + "(本人)";
    111. }else{
    112. chatStr += "" + v.username + "";
    113. }
    114. chatStr += ":" + v.msg + "     " + v.time + "

      ";
    115. });
    116. $(".chat").html(chatStr);
    117. });
    118. }
    119. function closeWebsocket(){
    120. }
    121. setInterval(function () {
    122. if(is_online == 1){
    123. $("#situation").html("在线");
    124. $("#situation").css({"color":"green"});
    125. }else{
    126. $("#situation").html("离线");
    127. $("#situation").css({"color": "red"});
    128. }
    129. }, 2*1000)
    130. script>
    131. html>

  • 相关阅读:
    Android studio连接MySQL并完成简单的登录注册功能
    JESD204B时钟网络
    不看后悔!第一本全面详解Transformer的综合性书籍!284页pdf下载
    人工智能——大白话熟悉目标检测基本流程
    《web课程设计》期末网页制作 基于HTML+CSS+JavaScript制作公司官网页面精美
    gcc中动态库和静态库的链接顺序
    IPV6(IPV6,RIPng的配置以及手工配置IPV4隧道)
    CTFHub | Cookie注入,UA注入,Refer注入,过滤空格(利用hackbar插件)
    2022-08-03 C++并发编程(六)
    如何看待时间序列与机器学习?
  • 原文地址:https://blog.csdn.net/m0_68949064/article/details/127729569