WebSocket

1、基础原理

WebSocketHTML5一种新的协议,基于TCP协议实现了客户端和服务端全双工通信的协议。

HTTP请求的生命周期通过Request界定,也就是一个Request、一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。

HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response

HTTP中永远是这样,也就是说一个Request只能有一个Response。而且这个Response也是被动的,不能主动发起。

2、Websocket握手

客户端:

GET /chat HTTP/1.1
Host: server.example.com
// 告诉服务器是一个WebSocket请求
Upgrade: websocket  
Connection: Upgrade
//一个Base64encode的值,这个是浏览器随机生成
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== 
//一个用户定义字符串,区分同URL下,不同的服务所需要的协议
Sec-WebSocket-Protocol: chat, superchat 
//告诉服务器所使用的Websocket Draft(协议版本)
Sec-WebSocket-Version: 13 
Origin: http://example.com

服务端:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
//这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= 
//表示最终使用的协议
Sec-WebSocket-Protocol: chat

3、其它方式

Ajax轮询:

类似定时任务,隔几秒向服务器发起请求,问有没有新的数据。

Long Poll

采用的是阻塞模式,就是向服务器发起一次请求,如果没有消息,就一直不返回Response给客户端,直到有消息才返回,返回完之后客户端再次建立连接。

以上两种方法也都是客户端主动发起(服务端不能主动发起),不断建立HTTP连接,然后等待服务器处理,体现HTTP被动性

不断的建立,关闭HTTP协议,由于HTTP无状态协议,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。

以上两种方式都是非常耗费服务器系统资源的:

Ajax: 需要服务器有很快的处理速度和资源。

Long Poll: 需要有很高的并发,也就是说同时处理多个请求的能力。

4、特点

  • 最初的握手阶段是http协议,握手完成后就切换到websocket协议,并完全与http协议脱离了。
  • 通讯一旦建立连接后,通讯就是“全双工”模式了。服务端和客户端都能在任何时间自由发送数据。
  • 交互模式不再是“请求-应答”模式,完全由开发者自行设计通讯协议。
  • 通信的数据是基于“帧(frame)”的,可以传输文本数据,也可以直接传输二进制数据,效率高。
  • 协议标识符是ws(如果加密,则为wss,类似http与https),服务器网址就是 URL。

5、常用方法

事件事件处理程序描述
openSocket onopen链接建立时触发
messageSocket onmessage客户端接收服务端数据时触发
errorSocket onerror通讯发生错误时触发
closeSocket onclose链接关闭时触发

6、JAVA代码

1、导入Maven依赖

 <!-- websocket -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2、创建配置类

@Configuration
public class WebSocketConfig {
    /**
     * 	注入ServerEndpointExporter,
     * 	这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3、WebSocket服务

@Slf4j
@ServerEndpoint(value = "/wsServer/{token}")
@Component
public class WebSocketServer {

    /**
     * 记录当前在线连接数量
     */
    private static AtomicInteger onlineCount = new AtomicInteger();

    /**
     * 存放客户端WebScoket对象 (线程安全)
     */
    private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 接收userId
     */
    private String userId = "";

    /**
     * @param session
     * @param token
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {
        this.session = session;
        try {
            //通过token获取UserId  获取到了证明token有效  获取不到则无效
            LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUserByToken(token);
            this.userId = loginUser.getUserId().toString();
            //刷新token
            SpringUtils.getBean(TokenService.class).verifyToken(loginUser);
            if (webSocketMap.containsKey(userId)) {
                webSocketMap.remove(userId);
                webSocketMap.put(userId, this);
            } else {
                webSocketMap.put(userId, this);
                addOnlineCount();
            }
            log.info("存储用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());
        } catch (Exception e) {
            log.error("token解析异常,连接失败!");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            subOnlineCount();
        }
        log.info("用户退出:" + userId + " 当前在线人数为:" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("接收消息:" + userId + " 报文:" + message);
        if (StringUtils.isNotBlank(message)) {
            try {
                if (message.equals("HeartBeat")) {
                    // 心跳
                }
                // 做事情
                // 这个项目不会涉及前端给后段发消息操作  发了也不用处理
                // 如果要用这个的话 最好每一次都做一下校验 因为尽管客户端不合法 如果地址正确也可以发送消息给我
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.info("用户错误:" + this.userId + " 原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 发送自定义消息
     */
    public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
        System.out.println("发送消息到:" + userId + ",报文:" + message);
        if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
            webSocketMap.get(userId).sendMessage(message);
        } else {
            System.out.println("用户" + userId + ",不在线!");
        }
    }

    /**
     * 获取连接数量
     *
     * @return 连接数量
     */
    public static synchronized AtomicInteger getOnlineCount() {
        return onlineCount;
    }

    /**
     * 连接数加一
     */
    public static synchronized void addOnlineCount() {
        onlineCount.incrementAndGet();
    }

    /**
     * 连接数减一
     */
    public static synchronized void subOnlineCount() {
        if (onlineCount.intValue() > 0) {
            onlineCount.decrementAndGet();
        }
    }
}

4、调用

@GetMapping("/test")
public void test(){
  try {
    WebSocketServer.sendInfo(roomId.toString(), userId);
  } catch (Exception e) {
    if(e instanceof IOException){
      //连接客户端异常
    }else if(e instanceof NullPointerException){
      //客户端不在线
    }else{
      //未知异常  联系管理员
    }
  }
}

7、VUE代码

<template>
  <div>
  </div>  
</template>
<script>
export default {
  data() {
    return {
    	// 计时器对象——重连
      socketReconnectTimer: null,
      // WebSocket重连的锁
      socketReconnectLock: false,
      // 离开标记(解决 退出登录再登录 时出现的 多次相同推送 问题,出现的本质是多次建立了WebSocket连接)
      socketLeaveFlag: false,
      // 长连接对象
      socketObj: {},
      // 当前用户token
      token: '',
      // 长连接地址
      socketUrl: 'ws://x.x.x.x:xxxx/wsServer/',
      // 心跳检测
      heartCheck: {
        vueThis: this, // vue实例
        timeout: 10000, // 超时时间
        timeoutObj: null, // 计时器对象——向后端发送心跳检测
        serverTimeoutObj: null, // 计时器对象——等待后端心跳检测的回复
        // 心跳检测重置
        reset: function() {
          clearTimeout(this.timeoutObj)
          clearTimeout(this.serverTimeoutObj)
          return this
        },
        // 心跳检测启动
        start: function() {
          this.timeoutObj && clearTimeout(this.timeoutObj)
          this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj)
          this.timeoutObj = setTimeout(() => {
            // 这里向后端发送一个心跳检测,后端收到后,会返回一个心跳回复
            this.vueThis.socketObj.send('HeartBeat')
            console.log('发送心跳检测')
            this.serverTimeoutObj = setTimeout(() => {
              // 如果超过一定时间还没重置计时器,说明websocket与后端断开了
              console.log('未收到心跳检测回复')
              // 关闭WebSocket
              this.vueThis.socketObj.close()
            }, this.timeout)
          }, this.timeout)
        }
      },
      
    }
  },
  created() {
    console.log('离开标记', this.socketLeaveFlag)
  },
  mounted() {
    // 初始化WebSocket
    this.initWebSocket()
  },
  destroyed() {
    // 离开标记
    this.socketLeaveFlag = true
    // 关闭WebSocket
    this.socketObj.close()
  },
  methods: {
    // 初始化webSocket
    initWebSocket() {
      try {
        this.socketObj = new WebSocket(this.socketUrl + this.token)
        // websocket事件绑定
        this.socketEventBind()
      } catch (e) {
        console.log('初始化WebSocket失败:' + e)
        this.reconnect()
      }
    },
    // websocket事件绑定
    socketEventBind() {
      this.socketObj.onopen = this.socketOpen // 连接建立触发
      this.socketObj.onmessage = this.onMessage // 客户端接收服务端数据触发
      this.socketObj.onsend = this.onSend
      this.socketObj.onclose = this.onClose // 连接关闭触发
      this.socketObj.onerror = this.onError // 通信发生异常触发
      // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
      window.onbeforeunload = () => {
        this.socketObj.close()
      }
    },
    // 连接建立触发
    socketOpen() {
      console.log('WebSocket:已连接')
      this.heartCheck.reset().start()
    },
    // 客户端接收服务端数据触发
    onMessage(msg) {
      console.log('WebSocket:已连接')
      // console.log(msg);
      console.log(msg.data);
      if (msg.data.indexOf("HeartBeat") > -1) {
        // 心跳回复——心跳检测重置
        // 收到心跳检测回复就说明连接正常
        console.log("收到心跳检测回复");
        // 心跳检测重置
        this.heartCheck.reset().start();
      } else {
        // 普通推送——正常处理
        console.log("收到推送消息");
        let data = JSON.parse(msg.data);
        // 相关处理
        console.log(data);
      }
    },
    // 向后端发送数据的回调
    onSend() {
      console.log('WebSocket:发送信息给后端')
    },
    // 连接关闭触发
    onClose() {
      console.log('WebSocket:已关闭')
      // 心跳检测重置
      this.heartCheck.reset()
      if (!this.socketLeaveFlag) {
        // 没有离开——重连
        this.reconnect()
      }
    },
    // 通信发生异常触发
    onError() {
      console.log('WebSocket:发生错误')
      this.reconnect()
    },
    // 重连
    reconnect() {
      if (this.socketReconnectLock) {
        return
      }
      this.socketReconnectLock = true
      this.socketReconnectTimer && clearTimeout(this.socketReconnectTimer)
      this.socketReconnectTimer = setTimeout(() => {
        console.log('WebSocket:重连中...')
        this.socketReconnectLock = false
        // websocket启动
        this.initWebSocket()
      }, 4000)
    }
  }
}
</script>

心跳机制:心跳就是客户端定时的给服务端发送消息,证明客户端是在线的, 如果超过一定的时间没有发送则就是离线了。

如何判断在线离线?

当客户端第一次发送请求至服务端时会携带唯一标识、以及时间戳,服务端到db或者缓存去查询该请求的唯一标识,如果不存在就存入db或者缓存中。

第二次客户端定时再次发送请求依旧携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果存在就把上次的时间戳拿取出来,使用当前时间戳减去上次的时间,

得出的毫秒秒数判断是否大于指定的时间,若小于的话就是在线,否则就是离线;

若服务端宕机了(服务器未回复心跳),客户端怎么做、服务端再次上线时怎么做?

客户端则需要断开连接,通过onclose关闭连接,服务端再次上线时则需要清除之间存的数据,若不清除则会造成只要请求到服务端的都会被视为离线。

WebSocket翻译的中文文档

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值