系列文章目录

【计算机网络基础——系列1】-matlab与python使用socket udp进行进程间通信
【计算机网络基础——系列2】-matlab与python使用socket tcp进行进程间通信
【计算机网络基础——系列3】输入url后页面会遇到的问题
【计算机网络基础——系列4】关于HTTP请求的相关内容
【计算机网络基础——系列5】前端遇到的三种网络攻击
【计算机网络基础——系列6】浏览器缓存之cookie、session、localstorage
【计算机网络基础——系列7】浏览器缓存之—http缓存
【计算机网络基础——系列8】前端优化总结
【计算机网络基础——系列9】restful规范;dns劫持
【计算机网络基础——系列10】osi网络结构;tcp协议保持传输的可靠性;SSL
【计算机网络基础——系列11】实现python作为服务端与qt进行udp通信
【计算机网络基础——系列12】flask作为服务器与vue实现websocket通信



一、为什么选择websocket

之前使用flask与前端进行通信一直是使用的http1.0,通过Ajax轮询的方式实现通信,通信的进行是不断的通过客户端发送请求-服务端响应来进行的,这样客户端需要不断的去进行请求,浪费资源和性能。
以前做项目时也因为使用AJAX轮询,而数据请求过于频繁出现过相关问题,所以这次的项目我打算使用websocket进行

1.1 websocket是什么?

HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。他在HTTP1.1实现长连接的基础上还能够保有超时重连和心跳机制。

1.2 websocket的优点

  1. 支持双向通信,实时性更强,有超时重连和心跳机制,数据传输更加稳定可靠。
  2. 更好的二进制支持。
  3. 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
  4. 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)

相比较传统的http与websocket可用一幅图表示:

从图中可以知道,websocket只需要发送一次请求就可以建立起长期有效的连接。

在这里插入图片描述


1.3 websocket报文

1.3.1 客户端请求报文:

可以看出,websocket还是基于http1.1的使用get请求的连接

GET / HTTP/1.1
Host: localhost:8080 
Origin: http://127.0.0.1:3000
Connection: Upgrade 
Upgrade: websocket 
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
  1. Connection: Upgrade:表示要升级协议
  2. Upgrade: websocket:表示要升级到websocket协议。
  3. Sec-WebSocket-Version:13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。(这个地方在前两天的尝试中就吃过亏,客户端和服务端的版本号不一致导致连接失败——后来全部升级到了最新版本)
  4. Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。

1.3.2 服务端响应报文:

HTTP/1.1 101 Switching Protocols
Connection:Upgrade 
Upgrade: websocket 
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
Sec-WebSocket-Protocol:chat
  1. 101 状态码表示服务器已经理解了客户端的请求,并将通过 Upgrade 消息头通知客户端采用不同的协议来完成这个请求;
  2. Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key,主要作用在于提供基础的防护,减少恶意连接、意外连接。
  3. Sec-WebSocket-Protocol 则是表示最终使用的协议,将 Sec-WebSocket-Key 跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接,通过 SHA1 计算出摘要,并转成 base64字符串。

1.3.3 数据帧预览格式:

  0                   1                   2                   3
  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
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+


二、客户端(vue)使用websocket

1.数据声明

data() {
      return {
        websock: null, //建立的连接
        lockReconnect: false, //是否真正建立连接
        timeout: 28 * 1000, //30秒一次心跳
        timeoutObj: null, //外层心跳倒计时
        serverTimeoutObj: null, //内层心跳检测
        timeoutnum: null //断开 重连倒计时
      };
    },

2.生命周期启动关闭连接

这里删除的定时器是后文进行心跳机制使用到的。

mounted:function(){
      this.initWebSocket();
    },
    beforeDestroy: function () {
      // 页面离开时断开连接,清除定时器
      this.disconnect();
      clearInterval(this.timer);
    },

3.methods中进行websocket各个函数设置

3.1 初始化websocket

 initWebSocket() {
        //初始化weosocket
        const wsuri = "ws://127.0.0.1:5000/test";
        this.websock = new WebSocket(wsuri);
        this.websock.onopen = this.websocketonopen;
        this.websock.onmessage = this.websocketonmessage;
        this.websock.onerror = this.websocketonerror;
        this.websock.onclose = this.websocketclose;
        this.websock.onsend=this.websocketsend;
      },
事件事件处理程序描述
openSocket.onopen连接建立时触发
messageSocket.onmessage客户端接收服务端数据时触发
errorSocket.onerror通信发生错误时触发
closeSocket.onclose连接关闭时触发
sendSocket.send()使用连接发送数据

3.2 websocket的超时重连机制

其原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的确认报文,那么就重新发送数据,直到发送成功为止。这里是以that.lockReconnect为判断依据

reconnect() {
        //重新连接
        var that = this;
        if (that.lockReconnect) {
          // 是否真正建立连接
          return;
        }
        that.lockReconnect = true;
        //没连接上会一直重连,设置延迟避免请求过多
        that.timeoutnum && clearTimeout(that.timeoutnum);
        // 如果到了这里断开重连的倒计时还有值的话就清除掉
        that.timeoutnum = setTimeout(function() {
          //然后新连接
          that.initWebSocket();
          that.lockReconnect = false;
        }, 5000);
      },

3.3 websocket的心跳机制

所谓心跳机制是指定时发送一个自定义的结构体 (心跳包,在本文中self.websock.send("heartCheck")),让对方知道自己还活着,以确保连接的有效性的机制。判断依据是self.websock.readyState

websock.readyState值代表状态
0正在链接中
1已经链接并且可以通讯
2连接正在关闭
3连接已关闭或者没有链接成功
 reset() {
        //重置心跳
        var that = this;
        //清除时间(清除内外两个心跳计时)
        clearTimeout(that.timeoutObj);
        clearTimeout(that.serverTimeoutObj);
        //重启心跳
        that.start();
      },
      start() {
        //开启心跳
        var self = this;
        self.timeoutObj && clearTimeout(self.timeoutObj);
        // 如果外层心跳倒计时存在的话,清除掉
        self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj);
        // 如果内层心跳检测倒计时存在的话,清除掉
        self.timeoutObj = setTimeout(function() {
          // 重新赋值重新发送 进行心跳检测
          //这里发送一个心跳,后端收到后,返回一个心跳消息,
          if (self.websock.readyState === 1) {
            //如果连接正常
            self.websock.send("heartCheck");
          }
          else {
            //否则重连
            self.reconnect();
          }
          self.serverTimeoutObj = setTimeout(function() {
            // 在三秒一次的心跳检测中如果某个值3秒没响应就关掉这次连接
            //超时关闭
            self.websock.close();
          }, self.timeout);
        }, self.timeout);
        // 3s一次
      },

3.4 执行函数

websocketonopen(e) {
        //连接建立之后执行send方法发送数据
        console.log("成功",e);
        let actions = 123;
        this.websocketsend(JSON.stringify(actions));
      },
      websocketonerror() {
        //连接建立失败重连
        console.log("失败");
        this.initWebSocket();
      },
      websocketonmessage(e) {
        //数据接收
        const redata = JSON.parse(e.data);
        console.log('接收',redata);
        // this.aa = [...this.aa, redata.type];
        this.reset();
      },
      websocketsend(Data) {
        //数据发送
        console.log('发送',Data);
        this.websock.send(Data);
      },
      websocketclose(e) {
        //关闭
        console.log("断开连接", e);
      }

3.5 源码


<template>
  <div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        websock: null, //建立的连接
        lockReconnect: false, //是否真正建立连接
        timeout: 28 * 1000, //30秒一次心跳
        timeoutObj: null, //外层心跳倒计时
        serverTimeoutObj: null, //内层心跳检测
        timeoutnum: null //断开 重连倒计时
      };
    },
    mounted:function(){
      this.initWebSocket();
    },
    beforeDestroy: function () {
      // 页面离开时断开连接,清除定时器
      this.disconnect();
      clearInterval(this.timer);
    },
    methods: {
      initWebSocket() {
        //初始化weosocket
        const wsuri = "ws://127.0.0.1:5000/test";
        this.websock = new WebSocket(wsuri);
        this.websock.onopen = this.websocketonopen;
        this.websock.onmessage = this.websocketonmessage;
        this.websock.onerror = this.websocketonerror;
        this.websock.onclose = this.websocketclose;
        this.websock.onsend=this.websocketsend;
      },
      reconnect() {
        //重新连接
        var that = this;
        if (that.lockReconnect) {
          // 是否真正建立连接
          return;
        }
        that.lockReconnect = true;
        //没连接上会一直重连,设置延迟避免请求过多
        that.timeoutnum && clearTimeout(that.timeoutnum);
        // 如果到了这里断开重连的倒计时还有值的话就清除掉
        that.timeoutnum = setTimeout(function() {
          //然后新连接
          that.initWebSocket();
          that.lockReconnect = false;
        }, 5000);
      },
      reset() {
        //重置心跳
        var that = this;
        //清除时间(清除内外两个心跳计时)
        clearTimeout(that.timeoutObj);
        clearTimeout(that.serverTimeoutObj);
        //重启心跳
        that.start();
      },
      start() {
        //开启心跳
        var self = this;
        self.timeoutObj && clearTimeout(self.timeoutObj);
        // 如果外层心跳倒计时存在的话,清除掉
        self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj);
        // 如果内层心跳检测倒计时存在的话,清除掉
        self.timeoutObj = setTimeout(function() {
          // 重新赋值重新发送 进行心跳检测
          //这里发送一个心跳,后端收到后,返回一个心跳消息,
          if (self.websock.readyState === 1) {
            //如果连接正常
            self.websock.send("heartCheck");
          }
          else {
            //否则重连
            self.reconnect();
          }
          self.serverTimeoutObj = setTimeout(function() {
            // 在三秒一次的心跳检测中如果某个值3秒没响应就关掉这次连接
            //超时关闭
            self.websock.close();
          }, self.timeout);
        }, self.timeout);
        // 3s一次
      },

      websocketonopen(e) {
        //连接建立之后执行send方法发送数据
        console.log("成功",e);
        let actions = 123;
        this.websocketsend(JSON.stringify(actions));
      },
      websocketonerror() {
        //连接建立失败重连
        console.log("失败");
        this.initWebSocket();
      },
      websocketonmessage(e) {
        //数据接收
        const redata = JSON.parse(e.data);
        console.log('接收',redata);
        // this.aa = [...this.aa, redata.type];
        this.reset();
      },
      websocketsend(Data) {
        //数据发送
        console.log('发送',Data);
        this.websock.send(Data);
      },
      websocketclose(e) {
        //关闭
        console.log("断开连接", e);
      }
    }
  };

</script>


三、服务端(python flask)使用websocket

本来是想用nodejs作为服务端的,但是算法部分使用python做的,如果用nodejs做又需要进程通信使用到socket udp,就还是使用flask了。

  • 端口设置了5000,ip地址就是127.0.0.1,确保心跳机制的执行,服务端必须接收到客户端发的消息才能保持该服务运行,如果ws.receive()没有接收到客户端发送的消息,那么它会关闭与客户端建立的链接。

  • 底层解释:Read and return a message from the stream. If None is
    returned, then
    the socket is considered closed/errored.

  • 所以客户端只建立连接,不与服务端交互通信,则无法实现自由通信状态,之后在客户端代码处会有详细内容。

from flask import Flask, jsonify, request  # 引入核心处理模块
import json
from flask_sockets import Sockets
import time
import sys
import os
from gevent import monkey
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler
print('已进入')
sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/' + '..'))
sys.path.append("..")
monkey.patch_all()
app = Flask(__name__)
sockets = Sockets(app)
now = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time()))
@sockets.route('/test')  # 指定路由
def echo_socket(ws):
    while not ws.closed:
            ws.send(str(111111111))  # 回传给clicent
            """ 服务端必须接收到客户端发的消息才能保持该服务运行,如果ws.receive()没有接收到客户端发送的
             消息,那么它会关闭与客户端建立的链接
             底层解释:Read and return a message from the stream. If `None` is returned, then
            the socket is considered closed/errored.
            所以客户端只建立连接,不与服务端交互通信,则无法实现自由通信状态,之后在客户端代码处会有详细内容。
             """
            message = ws.receive()  # 接收到消息
            if message is not None:
                print("%s receive msg==> " % now, str(json.dumps(message)))
                """ 如果客户端未发送消息给服务端,就调用接收消息方法,则会导致receive()接收消息为空,关闭此次连接 """
                ws.send(str(json.dumps(message)))  # 回传给clicent
            else:
                print(now, "no receive")
@app.route('/')
def hello():
    return 'Hello World! server start!'
if __name__ == "__main__":
    server = pywsgi.WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler)
    print('server start')
    server.serve_forever()




四、通信测试

这里推荐一个好网站,既可以测试服务端又可以测试客户端的网站(工具)

  • 这个网站自己内置了一个websocket客户端的js,在输入行内输入服务端的入口地址即可以连接上客户端,然后实现数据通信。也可以知晓服务端的问题所在
  • 同时,这个网站还提供了一个示例服务器,可以返回客户端发送的数据,这样就可以测试客户端的问题所在,前两天就是通过这个网站找到的服务端的问题的。

在这里插入图片描述

这是网站地址:测试websocket通信网站


五、报错集锦

5.1 404出错

在这里插入图片描述
这是链接指向的网页不存在,即原始网页的URL失效,无法在所请求的端口上访问Web站点,通过修改服务端的启动部分解决:

if __name__ == "__main__":
    server = pywsgi.WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler)
    print('server start')
    server.serve_forever()

5.2 python的flask_socket模块安装失败

ModuleNotFoundError: No module named 'flask_sockets'

我已经使用了pip install flask_socket,但是启动后仍然报错,于是我查到可能因为flask的版本问题,于是我就重新pip install flask,然后再下载flask_socket,于是成功

pip install flask
pip install flask_socket

5.3 object null is not iterable (cannot read property Symbol(Symbol.iterator))

这个错误因为强制转换了类型导致失败 如下:

 const redata = JSON.parse(e.data);
        console.log('接收',redata);
        // this.aa = [...this.aa, redata.type];
        this.reset();

于是我把这个隐去了this.aa = [...this.aa, redata.type];就成功了。


5.4 sockjs-node/info?t=报错

sockjs-node是一个低延时全双工的API,我在这里没有用到,于是找到它,隐去它,成功!

在node_moudules中找到它:

try {
  //  self.xhr.send(payload); 把这里注掉
  } catch (e) {
    self.emit('finish', 0, '');
    self._cleanup(false);
  }


5.5 ws与http的报错

在这个方法中,设置地址要用ws代替http;wss代替https。

const wsuri = "ws://127.0.0.1:5000/test";

六、相关环境配置

6.1 python flask服务端

pip install flask 
pip install json
pip install flask_sockets 
pip install time
pip install sys
pip install os
pip install gevent 
pip install geventwebsocket.handler

6.2 vue客户端

这个方法没有额外引入js文件,但如果使用socketio方法的话,需要在main.js中加入:

 import VueSocketIOExt from 'vue-socket.io-extended';
 import socketio from 'socket.io-client';
 Vue.use(VueSocketIOExt, socketio('http:127.0.0.1:8888'));
 Vue.prototype.$socketio = socketio;

或者是在当前vue文件中引入:

import SockJS from  'sockjs-client';
import  Stomp from 'stompjs';

码字不易~, 各位看官要是看爽了,可不可以三连走一波,点赞皆有好运!,不点赞也有哈哈哈~~~

Logo

前往低代码交流专区

更多推荐