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

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. 19. pt-query-digest

    慢查询参数 slow_query_log=1slow_query_log_file=/mysql3306/log/slow.log 记录的是查询语句,而非管理语句.除非启用 los_slow_admi ...

  2. ubuntu中运行java程序

    查找jdk rivsidn@rivsidn:~/demo/java$ sudo apt-cache search jdk default-jdk - Standard Java or Java com ...

  3. Vc 检测内存泄漏

    启用内存泄漏检测 检测内存泄漏是 C/c + + 调试器和 C 运行时库 (CRT) 的主要工具调试堆函数. 若要启用调试堆的所有函数,在 c + + 程序中,按以下顺序包含以下语句: C++复制 # ...

  4. Eclipse GBK批量转UTF-8插件(转)

    最近需要把Android项目转Android Studio,由于之前是eclipse开发,而且坑爹的是编码还是GBK的,转到Android Studio中文都是乱码,如果一个文件一个文件ctrl+c的 ...

  5. Unity 游戏运行越久加载越慢

    原因是某个GameObject 被调用多次DontDestroyOnLoad,表面上是调用多次没问题,实际上调用次数越多,加载速度越慢.

  6. GBDT(MART) 迭代决策树详解

    在网上看到一篇对从代码层面理解gbdt比较好的文章,转载记录一下: GBDT(Gradient Boosting Decision Tree) 又叫 MART(Multiple Additive Re ...

  7. dom4j 通过 org.dom4j.XPath 设置命名空间来支持 带namespace 的 xpath

    测试文件 test.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http:/ ...

  8. 20155326刘美岑 《网络对抗》Exp2 后门原理与实践

    20155326刘美岑 <网络对抗>Exp2 后门原理与实践 实验内容 (1)使用netcat获取主机操作Shell,cron启动 (2)使用socat获取主机操作Shell, 任务计划启 ...

  9. APK防护——Anti_Virtual App的思路和实现

    作者:HAI_i 原文来自:https://bbs.ichunqiu.com/thread-42982-1-1.html 0×00 前言 Virtual App是一个很强大的存在,破坏了Android ...

  10. 三种实现Android主界面Tab的方式

    在平时的Android开发中,我们经常会使用Tab来进行主界面的布局.由于手机屏幕尺寸的限制,合理使用Tab可以极大的利用屏幕资源,给用户带来良好的体验.学会Tab的使用方法已经成为学习Android ...