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

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. java 线程池简单例子

    package com.hra.riskprice; import com.hra.riskprice.SysEnum.Factor_Type; import com.hra.riskprice.po ...

  2. 设计模式之桥接模式 c++11

    参考http://blog.csdn.net/calmreason/article/details/50909321 桥接模式 实现与抽象之间由指针关联 调用sample类 实际是调用sample类保 ...

  3. python爬虫笔记

    1.抓取网页并保存到txt中.解决控制台乱码问题 #_*_coding:utf-8_*_ import urllib2 response = urllib2.urlopen('http://hws.m ...

  4. flex布局之flex-basis采坑

    场景: 容器设置为display: flex,容器里的子项目部分设置 flex: auto,子项目的宽高自适应正常,但如果再往子项目里嵌套一个(如:div),并设置高度(如:height: 100%) ...

  5. MFC在对话框中嵌入对话框

    在对话框中嵌入子对话框 代码 m_childDlg = new CChildDlg(); m_childDlg->Create(IDD_CHILD_DIALOG,AfxGetApp()-> ...

  6. w7 全网架构-rsync-备份

    准备 1.从安装系统开始准备 安装过程中添加网卡 eth0 ip 10.0.0.210 netmask 24 gateway 10.0.0.254 eth1 ip 172.16.1.210 netma ...

  7. Nginx+Django搭建

    本机环境介绍 虚拟机操作系统版本如下 [root@node1 ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) IP地址 ...

  8. Tomcat 多项目部署方法整理

    Tomcat 多项目部署方法整理 说明:tomcat-deploy-aaa和tomcat-deploy-bbb是两个不同的web项目,为了方便以下简称aaa和bbb,请先自行创建并跑通 导航: NO1 ...

  9. 2.DI依赖注入

    一:DI Dependency Injection ,依赖注入 is a :是一个,继承. has a:有一个,成员变量,依赖. class B { private A a;   //B类依赖A类 } ...

  10. Centos7中docker开启远程访问

    在作为docker远程服务的centos7机器中配置: 1.在/usr/lib/systemd/system/docker.service,配置远程访问.主要是在[Service]这个部分,加上下面两 ...