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

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. Linux 自动挂载硬盘的方法

    每次重启后,都需要手动挂载硬盘( sudo mount ),非常不方便,使用一下步骤可以实现硬盘的自动挂载 第一步  获取硬盘的基本信息(UUID TYPE) sudo blkid 第二步  修改 / ...

  2. delphi三层结构常出现的问题和解决方案

    以下问题出现原因有可能多个,暂时将我遇见的记录下来,以后有新的在陆续更新上去,有网友愿意的话也可以共同测试一下. 一,无法更新定位行.一些值可能已在最后一次读取已更改. 错误出现前提: 1, 录数据时 ...

  3. oracle服务端与客户端字符集不同导致中文乱码解决方案

    1.问题描述 用pl/sql登录时,会提示“数据库字符集(ZHS16GBK)和客户端字符集(2%)是不同的,字符集转化可能会造成不可预期的后果”,具体问题是中文乱码,如下图 2.问题分析 不管错误信息 ...

  4. 【APP测试(Android)】--客户端数据库

  5. linux python 安装到用户目录

    在公司服务器中,python可能存在多个版本,而且python中的包也有多个不同版本,由于不同猿的需求不同,经常会引起程序冲突,影响工作效率.因此,给大家分享一个在没有root权限时,将python安 ...

  6. windows下多tomcat部署

    两种方式: 第一种是修改tomcat下很多配置文件,且配置环境变量: 第二种不配tomcat环境变量,通过修改server.xml文件即可: 本篇讲解第二种方式: 首先说下如何修改tomcat控制台标 ...

  7. (PMP)第9章-----项目资源管理

    9.1 规划资源管理 数据表现: 1.层级型(高层次的角色):工作分解结构,组织分解结构,资源分解结构 2.责任分配矩阵:RAM,RACI,执行,负责,咨询,知情(只有一个A) 3.文本型(记录详细职 ...

  8. s2 Docker环境的快速搭建方法

    常规linux下安装 centos7 下配置docker源并安装 cat >/etc/yum.repos.d/docker.repo< [dockerrepo] name=Docker R ...

  9. 从开启GTID功能的库同步数据到未开启GTID功能库时,注意事项!

    从开启GTID的库中导出数据到未开启GTID的库中,需要注意,在导出的文件中去掉相应的gtid内容,否则导入时会报错如下: ERROR 1839 (HY000) at line 24 in file: ...

  10. 深圳scala-meetup-20180902(1)- Monadic 编程风格

    刚完成了9月份深圳scala-meetup,趁刮台风有空,把我在meetup里的分享在这里发表一下.我这次的分享主要分三个主题:“Monadic编程风格“.”Future vs Task and Re ...