ToB企服应用市场:ToB评测及商务社交产业平台

标题: websocket前后端实例 及 服务器基于tcp实现从http升级到websocket协议 [打印本页]

作者: 没腿的鸟    时间: 5 天前
标题: websocket前后端实例 及 服务器基于tcp实现从http升级到websocket协议
本文代码:本文代码:
1.HTTP的架构模式
  1.1. HTTP的特点
2. 双向通讯
  2.1 轮询
  2.2 长轮询
  2.3 iframe流
  2.4 EventSource流
  2.4.1 欣赏器端
  2.4.2 服务端
3.websocket
  3.1 websocket 优势
  3.2 websocket实战
    3.2.1 服务端
    3.2.2 客户端
  3.3 如何建立连接
    3.3.1 客户端:申请协议升级
    3.3.2 服务端:相应协议升级
    3.3.3 Sec-WebSocket-Accept的盘算
    3.3.4 Sec-WebSocket-Key/Accept的作用
  3.4 数据帧格式
    3.4.1 数据帧格式
    3.4.2 掩码算法
    3.4.3 服务器实战
  1. HTTP的架构模式
Http是客户端/服务器模式中哀求-相应所用的协议,在这种模式中,客户端(一样平常是web欣赏器)向服务器提交HTTP哀求,服务器相应哀求的资源。
1.1. HTTP的特点

2. 双向通讯
Comet是一种用于web的推送技能,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出哀求,目前有三种实现方式:轮询(polling) 长轮询(long-polling)和iframe流(streaming)。
2.1 轮询

  1. **index.html**
  2. <!DOCTYPE html>
  3. <html lang="en">
  4.   <head>
  5.     <meta charset="UTF-8" />
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7.     <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  8.     <title>Document</title>
  9.   </head>
  10.   <body>
  11.     <div id="clock"></div>
  12.     <script>
  13.       let clock = document.querySelector("#clock");
  14.       setInterval(function () {
  15.         let xhr = new XMLHttpRequest();
  16.         xhr.open("GET", "/clock", true);
  17.         xhr.onreadystatechange = function () {
  18.           if (xhr.readyState === 4 && xhr.status === 200) {
  19.             clock.innerHTML = xhr.responseText;
  20.           }
  21.         };
  22.         xhr.send();
  23.       });
  24.     </script>
  25.   </body>
  26. </html>
复制代码
  1. **app.js**
  2. let express = require("express");
  3. let app = express();
  4. // http://localhost:8000/
  5. app.use(express.static(__dirname));
  6. app.get("/clock", function (req, res) {
  7.   res.send(new Date().toLocaleString());
  8. });
  9. app.listen(8000);
复制代码
2.2 长轮询

  1. **index.html**
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5.     <meta charset="UTF-8">
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.     <meta http-equiv="X-UA-Compatible" content="ie=edge">
  8.     <title>Document</title>
  9. </head>
  10. <body>
  11.     <div id="clock"></div>
  12. <script>
  13. let clock = document.querySelector('#clock');
  14. function send(){
  15.     let xhr = new XMLHttpRequest;
  16.     xhr.open('GET','/clock',true);
  17.     xhr.onreadystatechange = function(){
  18.         if(xhr.readyState == 4 && xhr.status == 200){
  19.             clock.innerHTML = xhr.responseText;
  20.             send();
  21.         }
  22.     }
  23.     xhr.send();
  24. }
  25. send();
  26. </script>   
  27. </body>
  28. </html>
复制代码
  1. **app.js**
  2. let express = require("express");
  3. let app = express();
  4. // http://localhost:8000/
  5. app.use(express.static(__dirname));
  6. app.get("/clock", function (req, res) {
  7.   let $timer = setInterval(function () {
  8.     let date = new Date();
  9.     let seconds = date.getSeconds();
  10.     if (seconds % 5 === 0) {
  11.       res.send(date.toLocaleString());
  12.       clearInterval($timer);
  13.     }
  14.   }, 1000);
  15. });
  16. app.listen(8000);
复制代码
2.3 iframe流
通过在HTML页面里嵌入一个隐蔽的iframe,然后将这个iframe的src属性设为对一个长连接的哀求,服务器端就能源源不断地往客户推送数据。
  1. **index.html**
  2. <!DOCTYPE html>
  3. <html lang="en">
  4.   <head>
  5.     <meta charset="UTF-8" />
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7.     <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  8.     <title>Document</title>
  9.   </head>
  10.   <body>
  11.     <div id="clock" style="border: 1px solid red; height: 200px"></div>
  12.     <iframe src="/clock" style="border: none; height: 0"></iframe>
  13.     <script>
  14.       //window.setTime =
  15.       function setTime(ts) {
  16.         document.querySelector("#clock").innerHTML = ts;
  17.       }
  18.     </script>
  19.   </body>
  20. </html>
复制代码
  1. **app.js**
  2. let express = require("express");
  3. let app = express();
  4. // http://localhost:8000/
  5. app.use(express.static(__dirname));
  6. app.get("/clock", function (req, res) {
  7.   res.header("Content-Type", "text/html");
  8.   setInterval(function () {
  9.     res.write(`
  10.       <script>
  11.          parent.setTime("${new Date().toLocaleString()}")
  12.       </script>`);
  13.   }, 1000);
  14. });
  15. app.listen(9001);
复制代码
2.4 EventSource流

2.4.1 欣赏器端 #

  1. **index.html**
  2. <!DOCTYPE html>
  3. <html lang="en">
  4.   <head>
  5.     <meta charset="UTF-8" />
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7.     <title>Document</title>
  8.   </head>
  9.   <body>
  10.     <div id="clock"></div>
  11.     <script>
  12.       let eventSource = new EventSource("/clock");
  13.       let clock = document.querySelector("#clock");
  14.       eventSource.onmessage = function (event) {
  15.         let message = event.data;
  16.         clock.innerHTML = message;
  17.       };
  18.       eventSource.onopen = function (event) {
  19.         console.log("open");
  20.       };
  21.       eventSource.onerror = function (event) {
  22.         console.log("error");
  23.       };
  24.     </script>
  25.   </body>
  26. </html>
复制代码
2.4.2 服务端 #
事件流的对应MIME格式为text/event-stream,而且其基于HTTP长连接。针对HTTP1.1规范默认采用长连接,针对HTTP1.0的服务器必要特殊设置。
event-source必须编码成utf-8的格式,消息的每个字段利用"\n"来做分割,并且必要下面4个规范定义好的字段:
Event: 事件范例
Data: 发送的数据
ID: 每一条事件流的ID
Retry: 告知欣赏器在所有的连接丢失之后重新开启新的连接等待的时间,在主动重新连接的过程中,之前收到的末了一个事件流ID会被发送到服务端
  1. **app.js**
  2. let express = require("express");
  3. let app = express();
  4. app.use(express.static(__dirname));
  5. app.get("/clock", function (req, res) {
  6.   res.header("Content-Type", "text/event-stream");
  7.   let counter = 0;
  8.   let $timer = setInterval(function () {
  9.     res.write(
  10.       `id:${counter++}\nevent:abc\ndata:${new Date().toLocaleTimeString()}\n\n`
  11.     );
  12.   }, 1000);
  13.   res.on("close", function () {
  14.     clearInterval($timer);
  15.   });
  16. });
  17. app.listen(7777);
复制代码
  1. **app2.js** 使用ssestream库
  2. let express = require("express");
  3. let app = express();
  4. app.use(express.static(__dirname));
  5. //passThrough 通过流,它是一转换流
  6. const SseStream = require("ssestream");
  7. app.get("/clock", function (req, res) {
  8.   let counter = 0;
  9.   const sseStream = new SseStream(req);
  10.   sseStream.pipe(res);
  11.   const pusher = setInterval(function () {
  12.     sseStream.write({
  13.       id: counter++,
  14.       event: "message",
  15.       retry: 2000,
  16.       data: new Date().toString(),
  17.     });
  18.     // 他内部会帮你转换成:event:message\nid:0\nretry:2000\ndata:2019年3月23日17:45:51\n\n
  19.   }, 1000);
  20.   res.on("close", function () {
  21.     clearInterval(pusher);
  22.     sseStream.unpipe(res);
  23.   });
  24. });
  25. app.listen(7777);
复制代码
3. websocket

3.1 websocket优势

3.2 websocket实战
3.2.1 服务端
  1. **app.js**
  2. let express = require("express");
  3. let app = express();
  4. app.use(express.static(__dirname));
  5. app.listen(3000);
  6. let websocketServer = require("ws").Server;
  7. let server = new websocketServer({ port: 8888 });
  8. server.on("connection", (socket) => {
  9.   console.log("2.服务器监听到了客户端请求");
  10.   socket.on("message", (message) => {
  11.     console.log("4.客户端连接过来的消息", message);
  12.     socket.send("5.服务器说" + message);
  13.   });
  14. });
复制代码
3.2.2 客户端
  1. **index.html**
  2. <!DOCTYPE html>
  3. <html lang="en">
  4.   <head>
  5.     <meta charset="UTF-8" />
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7.     <title>Document</title>
  8.   </head>
  9.   <body>
  10.     <script>
  11.       let socket = new WebSocket("ws://localhost:8888");
  12.       socket.onopen = function () {
  13.         console.log("1. 客户端连接上了服务器");
  14.         socket.send("hello");
  15.       };
  16.       socket.onmessage = function (event) {
  17.         console.log(event.data);
  18.       };
  19.     </script>
  20.   </body>
  21. </html>
复制代码
3.3. 如何建立连接
WebSocket复用了HTTP的握手通道。详细指的是,客户端通过HTTP哀求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。
3.3.1 客户端:申请协议升级
首先,客户端发起协议升级哀求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法。
哀求头:
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: IHfMdf8a0aQXbwQO1pkGdA==

3.3.2 服务端:相应协议升级
服务端返回内容如下,状态代码101表现协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。
相应头:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aWAY+V/uyz5ILZEoWuWdxjnlb7E=
3.3.3 Sec-WebSocket-Accept的盘算
Sec-WebSocket-Accept根据客户端哀求首部的Sec-WebSocket-Key盘算出来。 盘算公式为:

  1. const crypto = require('crypto');
  2. const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  3. const webSocketKey = 'IHfMdf8a0aQXbwQO1pkGdA==';
  4. let websocketAccept = require('crypto').createHash('sha1').update(webSocketKey + CODE ).digest('base64');
  5. console.log(websocketAccept);//aWAY+V/uyz5ILZEoWuWdxjnlb7E=
复制代码
3.3.4 Sec-WebSocket-Key/Accept的作用

3.4 数据帧格式
WebSocket客户端、服务端通讯的最小单元是帧,由1个或多个帧组成一条完整的消息(message)。

3.4.1 数据帧格式
单元是比特。比如FIN、RSV1各占据1比特,opcode占据4比特
  1. 0                   1                   2                   3
  2.   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  3. +-+-+-+-+-------+-+-------------+-------------------------------+
  4. |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
  5. |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
  6. |N|V|V|V|       |S|             |   (if payload len==126/127)   |
  7. | |1|2|3|       |K|             |                               |
  8. +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
  9. |     Extended payload length continued, if payload len == 127  |
  10. + - - - - - - - - - - - - - - - +-------------------------------+
  11. | Extended payload length       | Masking-key, if MASK set to 1 |
  12. +-------------------------------+-------------------------------+
  13. | Masking-key (continued)       |          Payload Data         |
  14. +-------------------------------- - - - - - - - - - - - - - - - +
  15. :                     Payload Data continued ...                :
  16. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
  17. |                     Payload Data continued ...                |
  18. +---------------------------------------------------------------+
复制代码

3.4.2 掩码算法
掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操纵不会影响数据载荷的长度。掩码、反掩码操纵都采用如下算法:

  1. function maskOrUnmask(buffer, mask) {
  2.   const length = buffer.length;
  3.   for (let i = 0; i < length; i++) {
  4.     buffer[i] ^= mask[i % 4];
  5.   }
  6.   return buffer;
  7. }
  8. const mask = Buffer.from([0x12, 0x34, 0x56, 0x78]); // 随机写的字节数组
  9. const buffer = Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]); // 随机写的字节数组
  10. const masked = maskOrUnmask(buffer, mask); // 掩码
  11. console.log(masked); // <Buffer 7a 51 3a 14 7d>
  12. const unmasked = maskOrUnmask(buffer, mask); // 反掩码
  13. console.log(unmasked); // <Buffer 68 65 6c 6c 6f>,和buffer一致
复制代码
3.4.3 服务器实战
  1. **app2.js**
  2. let net = require("net"); // net模块用于创建tcp服务
  3. let CODE = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
  4. let crypto = require("crypto");
  5. // 实现http协议升级到ws协议。模拟服务端响应头返回
  6. /**
  7. *
  8. GET ws://localhost:8888/ HTTP/1.1/r/n
  9. Connection: Upgrade/r/n
  10. Upgrade: websocket/r/n
  11. Sec-WebSocket-Version: 13/r/n
  12. Sec-WebSocket-Key: O/SldTn2Th7GfsD07IxrwQ==/r/n
  13. /r/n
  14. */
  15. /**
  16. *
  17. HTTP/1.1 101 Switching Protocols
  18. Upgrade: websocket
  19. Connection: Upgrade
  20. Sec-WebSocket-Accept: H8BlFmSUnXVpM4+scTXjZIwFjzs=
  21. */
  22. let server = net.createServer((socket) => {
  23.   socket.once("data", (data) => {
  24.     // 建立连接,使用once
  25.     data = data.toString(); // buffer转字符串
  26.     if (data.match(/Connection: Upgrade/)) {
  27.       let rows = data.split("\r\n");
  28.       //   rows格式:
  29.       //   'GET / HTTP/1.1',
  30.       //   'Host: localhost:9999',
  31.       //   'Connection: Upgrade',
  32.       //   'Pragma: no-cache',
  33.       //   'Cache-Control: no-cache',
  34.       //   'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
  35.       //   'Upgrade: websocket',
  36.       //   'Origin: null',
  37.       //   'Sec-WebSocket-Version: 13',
  38.       //   'Accept-Encoding: gzip, deflate, br, zstd',
  39.       //   'Accept-Language: zh-CN,zh;q=0.9',
  40.       //   'Sec-WebSocket-Key: MwGApjw5wYjUrCrC2Rr1Cg==',
  41.       //   'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits',
  42.       //   '',
  43.       //   ''
  44.       rows = rows.slice(1, -2);
  45.       let headers = {};
  46.       rows.reduce((memo, item) => {
  47.         let [key, value] = item.split(": ");
  48.         memo[key] = value;
  49.         return memo;
  50.       }, headers);
  51.       if (headers["Sec-WebSocket-Version"] == "13") {
  52.         let secWebSocketKey = headers["Sec-WebSocket-Key"];
  53.         let secWebSocketAccept = crypto
  54.           .createHash("sha1")
  55.           .update(secWebSocketKey + CODE)
  56.           .digest("base64");
  57.         let response = [
  58.           "HTTP/1.1 101 Switching Protocols",
  59.           "Upgrade: websocket",
  60.           "Connection: Upgrade",
  61.           `Sec-WebSocket-Accept: ${secWebSocketAccept}`,
  62.           "\r\n",
  63.         ].join("\r\n");
  64.         socket.write(response);
  65.         //后面所有的格式都是基于websocket协议的
  66.         socket.on("data", (buffers) => {
  67.           // 通讯,使用on
  68.           // data默认是一个Buffer
  69.           let fin = buffers[0] & (0b10000000 === 0b10000000); // 结束位是true还是false,第0个字节第一位
  70.           let opcode = buffers[0] & 0b00001111; // 操作码,第0个字节后4位
  71.           let isMask = buffers[1] & (0b10000000 == 0b10000000); // 是否进行了掩码
  72.           let payloadLength = buffers[1] & 0b01111111; // 获得第1个字节后7位
  73.           let mask = buffers.slice(2, 6); // 掩码键,这里假设payloadLength是7位,下面根据这个来写代码
  74.           let payload = buffers.slice(6); // 携带的真实数据
  75.           payload = maskOrUnmask(payload, mask);
  76.           let response = Buffer.alloc(2 + payload.length);
  77.           response[0] = 0b10000000 | opcode;
  78.           response[1] = payloadLength;
  79.           payload.copy(response, 2); // 将 payload 的内容复制到 response 的第二个字节开始的位置,等于把客户端的消息又传了回去
  80.           socket.write(response);
  81.         });
  82.       }
  83.     }
  84.   });
  85. });
  86. server.listen(9999);
  87. function maskOrUnmask(buffer, mask) {
  88.   const length = buffer.length;
  89.   for (let i = 0; i < length; i++) {
  90.     buffer[i] ^= mask[i % 4];
  91.   }
  92.   return buffer;
  93. }
复制代码
  1. **index.html**
  2. <!DOCTYPE html>
  3. <html lang="en">
  4.   <head>
  5.     <meta charset="UTF-8" />
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7.     <title>Document</title>
  8.   </head>
  9.   <body>
  10.     <script>
  11.       let socket = new WebSocket("ws://localhost:9999");
  12.       socket.onopen = function () {
  13.         console.log("1. 客户端连接上了服务器");
  14.         socket.send(
  15.           "hello from the other side, I must've called a thousand times"
  16.         );
  17.       };
  18.       socket.onmessage = function (event) {
  19.         console.log(event.data);
  20.       };
  21.     </script>
  22.   </body>
  23. </html>
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4