WebSocket实现群发和单聊--Springboot实现

一:WebSocket原理

1、要谈WebSocket就不得不提起HTTP连接

    WebSocket是HTML5出的东西(协议,就是大家一起约定好的东西),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充。有交集,但是并不是全部。当然他们都属于网络的7层协议中的应用层。

    另外Html5是指的一系列新的API,或者说新规范,新技术。Http协议本身只有1.0和1.1,而且跟Html本身没有直接关系。
    通俗来说,你可以用HTTP协议传输非Html数据,就是这样。再简单来说,层级不一样。

    此外,ws协议是全双工协议,意味着不光服务器向客户端推送,客户端也可以像服务器发送请求。

2、Websocket是什么样的协议,具体有什么优点

    首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
    1) HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。
    在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
    但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。
    2)首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。在握手阶段是一样的。

    在三次握手进行通信的过程中,Websocket协议要比HTTP协议的握手请求中,多了几个东西。

    

这个就是Websocket的核心了,告诉Apache、Nginx等后端服务器:发起的是Websocket协议,即协议是ws:// 而不是http://。

之后,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:我要验证你是不是真的是Websocket。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。不过现在还好,已经定下来啦~大家都使用的一个东西。
3、Websocket的作用

在讲Websocket之前,我就顺带着讲下 long poll 和 ajax轮询 的原理。
首先是 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。相当于java代码的while循环,不段的发起请求,如果客户端连接数量过多的话,对服务器压力无疑是个重大的考验。

其次是 long poll,其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(就是请求到服务器后,一直不返回相应,也不超时),直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

通过上面这个例子,我们可以看出,这两种方式都不是最好的方式,需要很多资源。
一种需要更快的速度,一种需要更多的'电话'。这两种都会导致'电话'的需求越来越高。
HTTP还是一个无状态协议。通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。
所以在这种情况下出现了,Websocket出现了。他解决了HTTP的这几个难题。

首先,被动性,当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。所以我们其实一直建立者连接,一旦有消息了,服务器就会推送给客户端。

只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你)
这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。

那么为什么他会解决服务器上消耗资源的问题呢?
其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(JAVA等)来处理。
简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)。
本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。
Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
这样就可以解决客服处理速度过慢的问题了。
同时,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。
但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)了。

至于怎么在不支持Websocket的客户端上使用Websocket。。答案是:不能。
但是可以通过上面说的 long poll 和 ajax 轮询来模拟出类似的效果。

二:代码实现

    首先是jar包引入,springboot可以很容易的实现websocket协议的配置(springboot版本2.0以上)

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

    接下来就是配置启动文件了

package cn.chinotan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @program: test
 * @description: WebSocketConfig启动配置
 * @author: xingcheng
 * @create: 2019-05-30 19:33
 **/
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

    紧接着就是业务逻辑处理

package cn.chinotan.websocket;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: test
 * @description: WebSocketServer 服务器
 * @author: xingcheng
 * @create: 2019-05-30 19:34
 **/
@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {

    /**
     * 静态变量,用来记录当前在线连接数。线程安全。
     */
    private static AtomicInteger onlineCount = new AtomicInteger(0);
    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     */
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

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

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

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        this.session = session;
        // 加入set中
        webSocketSet.add(this);
        // 在线数加1
        addOnlineCount();          
        log.info("有新窗口开始监听:" + sid + ",当前在线人数为" + getOnlineCount());
        this.sid = sid;
        sendMessage("连接成功: " + sid);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        //从set中删除
        webSocketSet.remove(this);
        //在线数减1
        subOnlineCount();
        log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到来自窗口" + sid + "的信息:" + message);
        //群发消息
        for (WebSocketServer item : webSocketSet) {
            item.sendMessage(message);
        }
    }

    /**
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("消息推送失败");
            e.printStackTrace();
        }
    }


    /**
     * 群发自定义消息
     */
    public static void sendInfo(String message, String sid) {
        log.info("推送消息到窗口" + sid + ",推送内容:" + message);
        for (WebSocketServer item : webSocketSet) {
            //这里可以设定只推送给这个sid的,为null则全部推送
            if (sid == null) {
                item.sendMessage(message);
            } else if (item.sid.equals(sid)) {
                item.sendMessage(message);
            }
        }
    }

    public static int getOnlineCount() {
        return onlineCount.get();
    }

    public static void addOnlineCount() {
        WebSocketServer.onlineCount.addAndGet(1);
    }

    public static void subOnlineCount() {
        WebSocketServer.onlineCount.decrementAndGet();
    }
}

可以看到,通过springboot的注解,可以很容易的实现ws协议的编写,类似于controller,并且还有对应的生命周期监听注解

最后服务器端,我们配置一个推送请求,通过平常的http协议编写,模拟服务器想客户端推送过程

package cn.chinotan.controller;

import cn.chinotan.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.List;

/**
 * @program: test
 * @description: websocket控制器
 * @author: xingcheng
 * @create: 2019-06-01 15:13
 **/
@RestController
@RequestMapping("/webSocket")
public class WebSocketController {
    
    @Autowired
    WebSocketServer webSocketServer;

    @GetMapping("/server/push/{sid}")
    public void push(@PathVariable("sid") Integer sid) {
        if (0 == sid) {
            WebSocketServer.sendInfo(sid + ":全员注意啦!", null);
        } else {
            WebSocketServer.sendInfo(sid + ":注意啦! -> " + (sid + 1), String.valueOf(sid + 1));
        }
    }
    
}

最后,我们编写客户端的代码,采用html

<html>

<head>
<title>WebSocket测试页面</title>
</head>

<body>
    <script> 
    var socket;  
    if(typeof(WebSocket) == "undefined") {  
        console.log("您的浏览器不支持WebSocket");  
    }else{  
        console.log("您的浏览器支持WebSocket");
        let sid = getUrlParam("sid");
        //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接  
        if (!sid) {
            alert("sid不存在")
        };
        socket = new WebSocket("ws://localhost:9000/websocket/" + sid);  
        //打开事件  
        socket.onopen = function() {  
            console.log("Socket 已打开");  
            socket.send("这是来自客户端的消息" + location.href + new Date());  
        };  
        //获得消息事件  
        socket.onmessage = function(msg) {  
            console.log(msg.data);  
            //发现消息进入    开始处理前端触发逻辑
        };  
        //关闭事件  
        socket.onclose = function() {  
            console.log("Socket已关闭");  
        };  
        //发生了错误事件  
        socket.onerror = function() {  
            alert("Socket发生了错误");  
            //此时可以尝试刷新页面
        }  
        //离开页面时,关闭socket
        //jquery1.8中已经被废弃,3.0中已经移除
        // $(window).unload(function(){  
        //     socket.close();  
        //});  
    }

    function getUrlParam(paraName) {
    var url = document.location.toString();
    var arrObj = url.split("?");

    if (arrObj.length > 1) {
      var arrPara = arrObj[1].split("&");
      var arr;

      for (var i = 0; i < arrPara.length; i++) {
        arr = arrPara[i].split("=");

        if (arr != null && arr[0] == paraName) {
          return arr[1];
        }
      }
      return "";
    }
    else {
      return "";
    }
  }
    </script> 
</body>
</html>

接下来就可以进行测试了:

首先,我们启动在谷歌浏览器上启动三个窗口,连接地址分别是file:///Users/xingcheng/Downloads/websocket.html?sid=21,file:///Users/xingcheng/Downloads/websocket.html?sid=22,file:///Users/xingcheng/Downloads/websocket.html?sid=23,代表3个不同的客户端

来看服务器和客户端的交互过程

服务器:

客户端:

可以看到,连接的过程全部监听到了

接下来模拟服务器的推送过程:群聊,访问连接:http://localhost:9000/webSocket/server/push/0

服务器:

客户端:

最后,验证单聊:22客户端通过服务器给23发送消息,推送地址:http://localhost:9000/webSocket/server/push/22

服务器:

客户端:

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章