开题思考:如何实现客户端及时获取服务端数据?

Polling

指客户端每隔一段时间(周期性)请求服务端获取数据,可能有更新数据返回,也可能什么都没有,它并不在乎服务端数据有无更新。(Web端一般采用ajax polling实现)

Long Polling

阻塞型Polling,和Polling不同的是假如服务端数据没有准备好,那么可能会hold住请求,直到服务端有相关数据,或者等待一定时间超时才会返回。

WebSocket

HTML5 WebSocket规范定义了一种API,使Web页面能够使用WebSocket协议与远程主机进行双向通信。与轮询和长轮询相比,巨大减少了不必要的网络流量和等待时间。

Websocket体系结构

Websocket协议

WebSocket协议被设计成与现有的Web基础结构很好地工作。该协议规范定义了HTTP连接作为WebSocket连接生命的开始,从Http协议转换成WebSocket,被称为WebSocket握手。
  • 浏览器向服务器发送请求,表示它希望将协议从HTTP切换到WebSocket。客户端通过升级报头表达其愿望:

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    Origin: http://example.com
  • 从上面的报文可以看到,和HTTP协议的请求中,多了几样东西,核心就是Upgrade和Connection两个参数,用来告诉服务器,我需要升级为Websocket:

    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
  • 如果服务端能够理解WebSocket协议,它同意以Upgrade头字段来升级协议,会响应以下信息:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept:HSmrc0sMlYUkAGmm5OPpG2HaGWk=
    Sec-WebSocket-Protocol: chat
  • 此时,HTTP连接中断,并由同一底层TCP/IP连接上的WebSocket连接替换。 默认情况下,WebSocket连接使用与HTTP(80)和HTTPS(443)相同的端口。

Spring-WebSocket实战

Spring框架提供了WebSocket支持,很容易实现相关功能,此处分享一下使用Spring集成WebSocket实现简单的多人会议系统。

服务端相关代码

  • MeetingController (很简单的一个入口,创建会议,并生成会议id和对应随机串)

    @Controller
    public class MeetingController { private static AtomicInteger id = new AtomicInteger(0); @RequestMapping(value = "/meeting", method = RequestMethod.POST)
    @ResponseBody
    public Map<String, Object> createMeeting() {
    int meetingId = id.incrementAndGet();
    String randStr = RandomStringUtils.random(6, true, true);
    SystemCache.idRandStrMap.put(meetingId, randStr);
    Map<String, Object> meetingVO = new HashMap<>();
    meetingVO.put("id", meetingId);
    meetingVO.put("randStr", randStr);
    return meetingVO;
    }
    }
  • WebSocketConfig (通过WebSocketConfigurer来配置定义自己的Websocket处理器和拦截器)

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    /**
    * 注册websocket处理器以及拦截器
    */
    registry.addHandler(meetingWebSocketHandler(), "/websocket/spring/meeting").addInterceptors(myInterceptor());
    } @Bean
    public MeetingWebSocketHandler meetingWebSocketHandler() {
    return new MeetingWebSocketHandler();
    } @Bean
    public WebSocketHandshakeInterceptor myInterceptor() {
    return new WebSocketHandshakeInterceptor();
    }
    }
  • WebSocketHandshakeInterceptor (握手拦截器,用于处理请求携带参数)

    public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
    
        @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
    Map<String, Object> attributes) throws Exception {
    if (request instanceof ServletServerHttpRequest) {
    ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
    String randStr = serverHttpRequest.getServletRequest().getParameter("randStr");
    String role = serverHttpRequest.getServletRequest().getParameter("role");
    if (StringUtils.isNotBlank(randStr)) {
    attributes.put("randStr", randStr);
    }
    if (StringUtils.isNotBlank(role)) {
    attributes.put("role", role);
    }
    }
    return true;
    } @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
    Exception exception) {
    }
    }
  • MeetingWebSocketHandler(websocket处理器,用于接受客户端发送各种类型数据,主要分为数据帧和控制帧)

    @Service
    public class MeetingWebSocketHandler extends TextWebSocketHandler { private static final Log LOG = LogFactory.getLog(MeetingWebSocketHandler.class);
    // 会议id和wsSession列表
    private static final ConcurrentHashMap<Integer, CopyOnWriteArraySet<WebSocketSession>> meetingWsSeesionMap = new ConcurrentHashMap<>(); @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    LOG.info("spring websocket成功建立连接...");
    int meetingId = getMeetingId(session);
    if (meetingId <= 0) {
    singleMessage(session, new TextMessage("会议不存在!"));
    session.close();
    }
    // 如果该会议已存在,则直接加入
    if (meetingWsSeesionMap.containsKey(meetingId)) {
    CopyOnWriteArraySet<WebSocketSession> webSocketSessions = meetingWsSeesionMap.get(meetingId);
    webSocketSessions.add(session);
    }
    // 如果不存在,则新建
    else {
    CopyOnWriteArraySet<WebSocketSession> webSocketSessions = new CopyOnWriteArraySet<>();
    webSocketSessions.add(session);
    meetingWsSeesionMap.put(meetingId, webSocketSessions);
    }
    } @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
    if (!session.isOpen())
    return;
    LOG.info(message.getPayload());
    int meetingId = getMeetingId(session);
    TextMessage wsMessage = new TextMessage(message.getPayload());
    broadcastMessage(meetingId, wsMessage);
    } /**
    * 发送信息给指定用户
    * @param clientId
    * @param message
    * @return
    */
    public void singleMessage(WebSocketSession session, TextMessage message) {
    if (!session.isOpen())
    return;
    try {
    session.sendMessage(message);
    } catch (IOException e) {
    e.printStackTrace();
    }
    } /**
    * 广播信息
    * @param message
    * @return
    */
    public void broadcastMessage(int meetingId, TextMessage message) {
    // 获取会议所有的wsSession
    CopyOnWriteArraySet<WebSocketSession> webSocketSessions = meetingWsSeesionMap.get(meetingId);
    for (WebSocketSession session : webSocketSessions) {
    try {
    if (session.isOpen()) {
    session.sendMessage(message);
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    } @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    if (session.isOpen()) {
    session.close();
    }
    LOG.info("连接出错");
    } @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    LOG.info("连接已关闭:" + status);
    int meetingId = getMeetingId(session);
    // role 1为主持人
    String role = String.valueOf(session.getAttributes().get("role"));
    // 如果是主持人,则关闭所有该会议连接
    CopyOnWriteArraySet<WebSocketSession> webSocketSessions = meetingWsSeesionMap.get(meetingId);
    if (StringUtils.equals("1", role)) {
    SystemCache.idRandStrMap.remove(meetingId);
    for (WebSocketSession webSocketSession : webSocketSessions) {
    webSocketSession.close();
    }
    webSocketSessions.remove(meetingId);
    } else {
    webSocketSessions.remove(session);
    }
    } @Override
    public boolean supportsPartialMessages() {
    return false;
    } private int getMeetingId(WebSocketSession session) {
    String randStr = String.valueOf(session.getAttributes().get("randStr"));
    int meetingId = SystemCache.getMeetingIdByRandStr(randStr);
    return meetingId;
    }
    }
  • SystemCache(系统缓存,集群部署的情况下,可改为redis实现分布式缓存,单机则不需要)

    public class SystemCache {
    
        // 会议id和随机字符串的映射关系
    public static ConcurrentHashMap<Integer, String> idRandStrMap = new ConcurrentHashMap<>(); public static int getMeetingIdByRandStr(String randStr) {
    int meetingId = 0;
    for (Map.Entry<Integer, String> entry : idRandStrMap.entrySet()) {
    if (randStr.equals(entry.getValue())) {
    meetingId = entry.getKey();
    }
    }
    return meetingId;
    }
    }

前端相关代码

  • meeting-create.html(主持人页面,用于创建会议并且可以发送消息)

    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    <title>在线会议系统</title>
    </head>
    <body>
    <h2>欢迎使用会议系统</h2>
    <button id="create" onclick="createMeeting()">创建会议</button>
    <hr />
    <div id="meeting"></div>
    消息内容:
    <input id="text" type="text" />
    <button id="send" disabled="disabled" onclick="send()">发送消息</button>
    <hr />
    <button id="close" onclick="closeWebSocket()">结束会议</button>
    <hr />
    <div id="message"></div>
    </body> <script type="text/javascript" src="js/jquery-1.12.0.js"></script>
    <script type="text/javascript">
    var websocket = null;
    var randStr;
    var remote = window.location.host;
    function openWebsocket() {
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
    websocket = new WebSocket("ws://" + window.location.host
    + "/websocket/spring/meeting?role=1&randStr=" + randStr); //连接发生错误的回调方法
    websocket.onerror = function() {
    setMessageInnerHTML("会议连接发生错误!");
    }; //连接成功建立的回调方法
    websocket.onopen = function() {
    setMessageInnerHTML("会议连接成功...");
    document.getElementById("send").disabled = false;
    } //接收到消息的回调方法
    websocket.onmessage = function(event) {
    setMessageInnerHTML(event.data);
    } //连接关闭的回调方法
    websocket.onclose = function() {
    setMessageInnerHTML("会议结束,连接关闭!");
    document.getElementById("create").disabled = false;
    document.getElementById("send").disabled = true;
    } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常
    window.onbeforeunload = function() {
    closeWebSocket();
    }
    } else {
    alert('当前浏览器 Not support websocket');
    }
    } //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
    document.getElementById('message').innerHTML += innerHTML + '<br/>';
    } //关闭WebSocket连接
    function closeWebSocket() {
    websocket.close();
    } //发送消息
    function send() {
    var content = document.getElementById('text').value;
    websocket.send(content);
    } function createMeeting() {
    $.post("/meeting", function(data, status) {
    randStr = data.randStr;
    $("#create").after("<p>会议邀请码:" + randStr + "</p>");
    $("#create").attr("disabled", true);
    openWebsocket();
    });
    }
    </script>
    </html>
  • meeting-join.html(观众页面,用于加入会议并且也可以发送消息)

    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    <title>在线会议系统</title>
    </head>
    <body>
    <h2>欢迎使用会议系统</h2>
    会议邀请码:
    <input id="randStr" type="text" />
    <button id="open" onclick="openWebsocket()">加入会议</button>
    <hr />
    消息内容:
    <input id="text" type="text" />
    <button id="send" disabled="disabled" onclick="send()">发送消息</button>
    <hr />
    <button id="close" disabled="disabled" onclick="closeWebSocket()">离开会议</button>
    <hr />
    <div id="message"></div>
    </body> <script type="text/javascript">
    var websocket = null;
    var remote = window.location.host;
    function openWebsocket() {
    var randStr = document.getElementById('randStr').value;
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
    websocket = new WebSocket("ws://" + window.location.host
    + "/websocket/spring/meeting?randStr=" + randStr); //连接发生错误的回调方法
    websocket.onerror = function() {
    setMessageInnerHTML("会议连接发生错误!");
    }; //连接成功建立的回调方法
    websocket.onopen = function() {
    setMessageInnerHTML("会议连接成功...");
    document.getElementById("open").disabled = true;
    document.getElementById("randStr").disabled = true;
    document.getElementById("send").disabled = false;
    document.getElementById("close").disabled = false;
    } //接收到消息的回调方法
    websocket.onmessage = function(event) {
    setMessageInnerHTML(event.data);
    } //连接关闭的回调方法
    websocket.onclose = function() {
    setMessageInnerHTML("会议结束,连接关闭!");
    document.getElementById("randStr").disabled = false;
    document.getElementById("open").disabled = false;
    document.getElementById("send").disabled = true;
    document.getElementById("close").disabled = true;
    } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常
    window.onbeforeunload = function() {
    closeWebSocket();
    }
    } else {
    alert('当前浏览器 Not support websocket');
    }
    } //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
    document.getElementById('message').innerHTML += innerHTML + '<br/>';
    } //关闭WebSocket连接
    function closeWebSocket() {
    websocket.close();
    } //发送消息
    function send() {
    var content = document.getElementById('text').value;
    websocket.send(content);
    }
    </script>
    </html>

项目演示

  • 访问meeting-create.html进入主持人界面,点击创建会议,生成会议邀请码,并显示会议连接成功,界面如下:

  • 访问meeting-join.html进入观众界面,并通过上面的邀请码加入会议,界面如下:

  • 此时双方就可以互相发送消息,主持人离开会议,则所有人退出,观众离开,不影响会议进行。

  • 具体代码地址:https://gitee.com/yehx/websocket-meeting

总结

WebSocket作为一个双通道的协议,颠覆了传统的Client请求Server这种单向通道的模式。由于WebSocket的兴起,Web领域的实时推送技术也被广泛使用,可以简单实现让用户不需要刷新浏览器就可以获得实时更新。它有着广泛的应用场景,比如在线聊天室、在线客服系统、评论系统、WebIM等。

 

WebSocket原理与实践的更多相关文章

  1. WebSocket原理与实践(四)--生成数据帧

    WebSocket原理与实践(四)--生成数据帧 从服务器发往客户端的数据也是同样的数据帧,但是从服务器发送到客户端的数据帧不需要掩码的.我们自己需要去生成数据帧,解析数据帧的时候我们需要分片. 消息 ...

  2. WebSocket原理与实践(三)--解析数据帧

    WebSocket原理与实践(三)--解析数据帧 1-1 理解数据帧的含义:   在WebSocket协议中,数据是通过帧序列来传输的.为了数据安全原因,客户端必须掩码(mask)它发送到服务器的所有 ...

  3. WebSocket原理与实践(二)---WebSocket协议

    WebSocket原理与实践(二)---WebSocket协议 WebSocket协议是为了解决web即时应用中服务器与客户端浏览器全双工通信问题而设计的.协议定义ws和wss协议,分别为普通请求和基 ...

  4. WebSocket原理与实践(一)---基本原理

    WebSocket原理与实践(一)---基本原理 一:为什么要使用WebSocket?1. 了解现有的HTTP的架构模式:Http是客户端/服务器模式中请求-响应所用的协议,在这种模式中,客户端(一般 ...

  5. Atitit 管理原理与实践attilax总结

    Atitit 管理原理与实践attilax总结 1. 管理学分类1 2. 我要学的管理学科2 3. 管理学原理2 4. 管理心理学2 5. 现代管理理论与方法2 6. <领导科学与艺术4 7. ...

  6. Atitit.ide技术原理与实践attilax总结

    Atitit.ide技术原理与实践attilax总结 1.1. 语法着色1 1.2. 智能提示1 1.3. 类成员outline..func list1 1.4. 类型推导(type inferenc ...

  7. Atitit.异步编程技术原理与实践attilax总结

    Atitit.异步编程技术原理与实践attilax总结 1. 俩种实现模式 类库方式,以及语言方式,java futuretask ,c# await1 2. 事件(中断)机制1 3. Await 模 ...

  8. Atitit.软件兼容性原理与实践 v5 qa2.docx

    Atitit.软件兼容性原理与实践   v5 qa2.docx 1. Keyword2 2. 提升兼容性的原则2 2.1. What 与how 分离2 2.2. 老人老办法,新人新办法,只新增,少修改 ...

  9. Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法

    Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法 于是我们可以把上面的语法改写成如下形式:1 合并前缀1 语法分析有自上而下和自下而上两种分析 ...

随机推荐

  1. 20172306 2018-2019《Java程序设计与数据结构课堂测试补充报告》

    学号 2017-2018-2 <程序设计与数据结构>课堂测试补充报告 课程:<程序设计与数据结构> 班级: 1723 姓名: 刘辰 学号:20172306 实验教师:王志强 必 ...

  2. python 数据可视化(matplotlib)

    matpotlib 官网 :https://matplotlib.org/index.html matplotlib 可视化示例:https://matplotlib.org/gallery/inde ...

  3. python 常用知识点

    1,字典get用法 如果key没有值,返回一个None >>> dic = {'k1':'v1','k2':'v2','k3':'v3'} >>> dic.get( ...

  4. Unity3D中声音播放

    Unity3D 播放声音需要使用 Audio Source 组件,并且需要 Audio Listener 组件配合,不然无法听到声音.Main Camera 会默认有 Audio Lisetener. ...

  5. Linux 查看IP

    UBuntu 系统下 按Ctrl+Alt+T 唤出终端 在终端输入: ifconfig 命令 点击回车 就可以看到自己电脑在局域网的IP地址了 图中第二行 inet 地址:192.168.1.101 ...

  6. JS应用实例6:二级联动

    本案例很常用,应用场景:注册页面填写籍贯,省市二级联动 总体思想:创建一个二维数组存入省市,获取选中的省份并比较,创建标签遍历添加 代码: <!DOCTYPE html> <html ...

  7. MySQL:explain 和 慢查询日志

    1. 执行SQL时显示执行情况 explain + SQL语句 2. 强制使用索引 select  *  from  t  force  index (a)  where  a  between  1 ...

  8. ARM中几个典型的汇编指令解析

    启动嵌入式设备时,遇到了一些汇编,做个笔记,免得以后忘记了. 一句汇编语句如下所指示: __asm ( ".syntax unified\n" ".thumb\n&quo ...

  9. 被高通收购的CSR是一家怎样的公司

    CSR plc(LSE:CSR),原名剑桥硅晶无线电或英商剑桥无线电(英语:Cambridge Silicon Radio),是一间创立于 1998年的跨国无厂半导体公司,总部位于英国剑桥.早期技术主 ...

  10. LeetCode--No.002 Add Two Numbers

    Add Two Numbers Total Accepted: 160702 Total Submissions: 664770 Difficulty: Medium You are given tw ...