webSocket


一. WebSocket

1.1 WebSocket介绍

  • WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

  • 在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

  • HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

  • 浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。当获取 Web Socket 连接后,可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

1.2 WebSocket的原理

(1)ajax轮询

ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

(2)long poll(长轮询)

long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输 identity info (鉴别信息),来告诉服务端你是谁。然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性。

被动性:其实就是,服务端不能主动联系客户端,只能有客户端发起。从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。

ajax轮询 需要服务器有很快的处理速度和资源。(速度)

long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)

(3)WebSocket

Websocket解决了HTTP的这几个难题。首先,被动性,当服务器完成协议升级后(HTTP-->Websocket),服务端就可以主动推送信息给客户端。解决了上面同步有延迟的问题。

解决服务器上消耗资源的问题:其实所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler来处理。

简单地说,有一个非常快速的 接线员(Nginx) ,他负责把问题转交给相应的 客服(Handler) 。Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。

由于Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。

此外,WebSocket不兼容低版本的IE.

1.3 WebSocket的优缺点

优点:

  • 1、采用全双工通信,摆脱传统HTTP轮询的窘境。

  • 2、采用W3C国际标准,完美支持HTML5。

  • 3、实时性,推送消息及时高效;

    考虑到服务器压力,使用轮询方式不可能很短的时间间隔,否则服务器压力太多,所以轮询时间间隔都比较长,好几秒,设置十几秒。 而WebSocket是由服务器主动推送过来,实时性是最高的。

  • 4、支持服务器端向客户端推送功能。服务器可以直接发送数据而不用等待客户端的请求。

  • 5、减少通信量

    只要建立起websocket连接,就一直保持连接,在此期间可以源源不断的传送消息,直到关闭请求。

  • 6、无浪费,减少服务器上资源消耗

    轮询方式有可能轮询10次,才碰到服务端数据更新,那么前9次都白轮询了,因为没有拿到变化的数据。 而WebSocket是由服务器主动回发,来的都是新数据。

  • 7、节约带宽

    不停地轮询服务端数据这种方式,使用的是http协议,head信息很大,有效数据占比低, 而使用WebSocket方式,头信息很小,有效数据占比高。

缺点:

  • 1、websocket 是长连接,受网络限制比较大,需要处理好重连,比如用户进电梯或用户打个电话网断了,这时候就需要重连,如果 websocket 一直重连不上,有些较复杂的业务将会被迫中断。
  • 2、websocket 长连接的用户收到消息是个 push 操作,http 轮训用户收消息则是 pull 操作。而push 都存在单生产推多消费,为广播模型,所以使用websocket 还需要处理好连接,保障每个消费推且只推一次。而使用http 轮训的pull 操作,消费方想要生产方拉一下或拉几次,消息就准确的送达几次,不存在多消费和连接处理的问题。
  • 3、使用websocket时对前后端均有要求
    • 对前端开发者,往往要具备数据驱动使用javascript的能力,且需要维持住websocket连接(否则消息无法推送);
    • 对后端开发者而言,则是长连接需要后端处理业务的代码更稳定,且在高并发下不能漏推信息

1.4 WebSocket和http的区别

http:

http链接分为短链接,长链接。

短链接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response。

长链接是在一定的期限内保持链接。保持TCP连接不断开。客户端与服务器通信,必须要有客户端发起然后服务器返回结果。客户端是主动的,服务器是被动的。

WebSocket:

WebSocket是为了解决客户端发起多个http请求到服务器资源浏览器必须要经过长时间的轮训问题而生的,它实现了多路复用,是全双工通信。在webSocket协议下客服端和浏览器可以同时发送信息。

建立了WebSocket之后服务器不必在浏览器发送request请求之后才能发送信息到浏览器。这时的服务器已有主动权想什么时候发就可以发送信息到客户端。而且信息当中不必在带有head的部分信息了与http的长链接通信来说,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。

1.5 WebSocket的使用场景

  • 社交聊天

  • 弹幕

  • 多玩家游戏

  • 协同编辑,在线文档

  • 视频会议

  • 电商的实时报价

  • 社交订阅信息实时更新与推送

    ..............

二. WebSocket实现实时聊天

代码demo: 下载

2.1 打造 WebSocket 聊天客户端

得益于W3C国际标准的实现,在浏览器JS就能直接创建WebSocket对象,再通过简单的回调函数就能完成WebSocket客户端的编写。

使用:

1、获取WebSocket客户端对象。

var webSocket = new WebSocket(url);

2、获取WebSocket回调函数。

webSocket.onmessage = function (event) {console.log('WebSocket收到消息:' + event.data);
事件类型WebSocket回调函数事件描述
openwebSocket.onopen当打开连接后触发
messagewebSocket.onmessage当客户端接收服务端数据时触发
errorwebSocket.onerror当通信异常时触发
closewebSocket.onclose当连接关闭时触发

3、发送消息给服务端

webSokcet.send(jsonStr) 

具体实现(js部分)

<script>

    /**
     * WebSocket客户端
     *
     * 使用说明:
     * 1、WebSocket客户端通过回调函数来接收服务端消息。例如:webSocket.onmessage
     * 2、WebSocket客户端通过send方法来发送消息给服务端。例如:webSocket.send();
     */
    function getWebSocket() {
        /**
         * WebSocket客户端 PS:URL开头ws表示WebSocket协议 中间是域名端口 结尾是服务端映射地址
         */
        var webSocket = new WebSocket('ws://localhost:8080/chat');
        /**
         * 当服务端打开连接
         */
        webSocket.onopen = function (event) {
            console.log('WebSocket打开连接');
        };

        /**
         * 当服务端发来消息:1.广播消息 2.更新在线人数
         */
        webSocket.onmessage = function (event) {
            console.log('WebSocket收到消息:%c' + event.data, 'color:green');
            //获取服务端消息
            var message = JSON.parse(event.data) || {};
            var $messageContainer = $('.message-container');
           
            if (message.type === 'SPEAK') {
                $messageContainer.append(
                    '<div class="mdui-card" style="margin: 10px 0;">' +
                    '<div class="mdui-card-primary">' +
                    '<div class="mdui-card-content message-content">' + message.username + ":" + message.msg + '</div>' +
                    '</div></div>');
            }
            $('.chat-num').text(message.onlineCount);
            //防止刷屏
            var $cards = $messageContainer.children('.mdui-card:visible').toArray();
            if ($cards.length > 5) {
                $cards.forEach(function (item, index) {
                    index < $cards.length - 5 && $(item).slideUp('fast');
                });
            }
        };

        /**
         * 关闭连接
         */
        webSocket.onclose = function (event) {
            console.log('WebSocket关闭连接');
        };

        /**
         * 通信失败
         */
        webSocket.onerror = function (event) {
            console.log('WebSocket发生异常');

        };
        return webSocket;
    }

    var webSocket = getWebSocket();


    /**
     * 通过WebSocket对象发送消息给服务端
     */
    function sendMsgToServer() {
        var $message = $('#msg');
        if ($message.val()) {
            webSocket.send(JSON.stringify({username: $('#username').text(), msg: $message.val()}));
            $message.val(null);
        }

    }
    /**
     * 清屏
     */
    function clearMsg(){
      $(".message-container").empty();
    }

    /**
     * 使用ENTER发送消息
     */
    document.onkeydown = function (event) {
        var e = event || window.event || arguments.callee.caller.arguments[0];
        e.keyCode === 13 && sendMsgToServer();
    };
</script>

2.2 打造 WebSocket 聊天服务端

得益于SpringBoot提供的自动配置,只需要通过简单注解@ServerEndpoint就能创建WebSocket服务端,再通过简单的回调函数就能完成WebSocket服务端的编写。

1、首先在pom.xml文件引入spring-boot-starter-websocket 、thymeleaf 、FastJson等依赖。


<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
    <relativePath/>
</parent>

<dependencies>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency><!--Webjars版本定位工具-->
    <groupId>org.webjars</groupId>
    <artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
    <groupId>org.webjars.npm</groupId>
    <artifactId>mdui</artifactId>
    <version>0.4.0</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.49</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
</dependencies>

2、开启WebSocket服务端的自动注册。

这里需要特别提醒:ServerEndpointExporter 是由Spring官方提供的标准实现,用于扫描ServerEndpointConfig配置类和@ServerEndpoint注解实例。

使用规则也很简单:1.如果使用默认的嵌入式容器 比如Tomcat 则必须手工在上下文提供ServerEndpointExporter。2. 如果使用外部容器部署war包,则不要提供提供ServerEndpointExporter,因为此时SpringBoot默认将扫描服务端的行为交给外部容器处理。

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {

        return new ServerEndpointExporter();
    }
}

3、创建WebSocket服务端

  • ① 通过注解@ServerEndpoint来声明实例化WebSocket服务端。
  • ② 通过注解@OnOpen、@OnMessage、@OnClose、@OnError 来声明回调函数。
事件类型WebSocket服务端注解事件描述
open@OnOpen当打开连接后触发
message@OnMessage当接收客户端信息时触发
error@OnError当通信异常时触发
close@OnClose当连接关闭时触发
  • ③ 通过ConcurrentHashMap保存全部在线会话对象。
@Component
@ServerEndpoint("/chat")//标记此类为服务端
public class WebSocketChatServer {

    /**
     * 全部在线会话  
     * 基于场景考虑 这里使用线程安全的ConcurrentHashMap存储会话对象。
     */
    private static Map<String, Session> onlineSessions = new ConcurrentHashMap<>();


    /**
     * 当客户端打开连接:1.添加会话对象 2.更新在线人数
     */
    @OnOpen
    public void onOpen(Session session) {
        onlineSessions.put(session.getId(), session);
        sendMessageToAll(Message.jsonStr(Message.ENTER, "", "", onlineSessions.size()));
    }

    /**
     * 当客户端发送消息:1.获取它的用户名和消息 2.发送消息给所有人
     * <p>
     * 这里约定传递的消息为JSON字符串 方便传递更多参数!
     */
    @OnMessage
    public void onMessage(Session session, String jsonStr) {
        Message message = JSON.parseObject(jsonStr, Message.class);
        sendMessageToAll(Message.jsonStr(Message.SPEAK, message.getUsername(), message.getMsg(), onlineSessions.size()));
    }

    /**
     * 当关闭连接:1.移除会话对象 2.更新在线人数
     */
    @OnClose
    public void onClose(Session session) {
        onlineSessions.remove(session.getId());
        sendMessageToAll(Message.jsonStr(Message.QUIT, "", "下线了!", onlineSessions.size()));
    }

    /**
     * 当通信发生异常:打印错误日志
     */
    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    /**
     * 公共方法:发送信息给所有人
     */
    private static void sendMessageToAll(String msg) {
        onlineSessions.forEach((id, session) -> {
            try {
                session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

}
  • ④ 通过会话对象 javax.websocket.Session 来发消息给客户端。
/**
 * WebSocket 聊天消息类
 */
package com.hehe.chat;

import com.alibaba.fastjson.JSON;

/**
 * WebSocket 聊天消息类
 */
public class Message {

    public static final String ENTER = "ENTER";
    public static final String SPEAK = "SPEAK";
    public static final String QUIT = "QUIT";

    private String type;//消息类型

    private String username; //发送人

    private String msg; //发送消息

    private int onlineCount; //在线用户数

    public static String jsonStr(String type, String username, String msg, int onlineTotal) {
        return JSON.toJSONString(new Message(type, username, msg, onlineTotal));
    }

    public Message(String type, String username, String msg, int onlineCount) {
        this.type = type;
        this.username = username;
        this.msg = msg;
        this.onlineCount = onlineCount;
    }

    //这里省略了get/set方法 
}

启动类:

@SpringBootApplication
@RestController
public class WebSocketChatApplication {

    /**
     * 登陆界面
     */
    @GetMapping("/")
    public ModelAndView login() {
        return new ModelAndView("/login");
    }

    /**
     * 聊天界面
     */
    @GetMapping("/index")
    public ModelAndView index(String username, String password, HttpServletRequest request) throws UnknownHostException {
        if (StringUtils.isEmpty(username)) {
            username = "匿名用户";
        }
        ModelAndView mav = new ModelAndView("/chat");
        mav.addObject("username", username);
        mav.addObject("webSocketUrl", "ws://"+InetAddress.getLocalHost().getHostAddress()+":"+request.getServerPort()+request.getContextPath()+"/chat");
        return mav;
    }

    public static void main(String[] args) {
        SpringApplication.run(WebSocketChatApplication.class, args);
    }
}

三. 运行

运行启动类,浏览器访问http://localhost:8080/

使用 用户1登录,当前在线人数显示为1

新开一个浏览器窗口访问http://localhost:8080/,并使用 用户2登录,当前在线人数显示为2

分别在用户1和用户2窗口发送消息,消息实时推送

Thymeleaf
webSocket
  • 作者:管理员(联系作者)
  • 发表时间:2020-06-14 22:36
  • 版权声明:自由转载-非商用-非衍生-保持署名(null)
  • undefined
  • 评论