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

Polling

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

Long Polling

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

WebSocket

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

Websocket体系结构

Websocket协议

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

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

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

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

Spring-WebSocket实战

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

服务端相关代码

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

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

    1. @Configuration
    2. @EnableWebSocket
    3. public class WebSocketConfig implements WebSocketConfigurer {
    4. @Override
    5. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    6. /**
    7. * 注册websocket处理器以及拦截器
    8. */
    9. registry.addHandler(meetingWebSocketHandler(), "/websocket/spring/meeting").addInterceptors(myInterceptor());
    10. }
    11.  
    12. @Bean
    13. public MeetingWebSocketHandler meetingWebSocketHandler() {
    14. return new MeetingWebSocketHandler();
    15. }
    16.  
    17. @Bean
    18. public WebSocketHandshakeInterceptor myInterceptor() {
    19. return new WebSocketHandshakeInterceptor();
    20. }
    21. }
  • WebSocketHandshakeInterceptor (握手拦截器,用于处理请求携带参数)

    1. public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
    2.  
    3. @Override
    4. public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
    5. Map<String, Object> attributes) throws Exception {
    6. if (request instanceof ServletServerHttpRequest) {
    7. ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
    8. String randStr = serverHttpRequest.getServletRequest().getParameter("randStr");
    9. String role = serverHttpRequest.getServletRequest().getParameter("role");
    10. if (StringUtils.isNotBlank(randStr)) {
    11. attributes.put("randStr", randStr);
    12. }
    13. if (StringUtils.isNotBlank(role)) {
    14. attributes.put("role", role);
    15. }
    16. }
    17. return true;
    18. }
    19.  
    20. @Override
    21. public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
    22. Exception exception) {
    23. }
    24. }
  • MeetingWebSocketHandler(websocket处理器,用于接受客户端发送各种类型数据,主要分为数据帧和控制帧)

    1. @Service
    2. public class MeetingWebSocketHandler extends TextWebSocketHandler {
    3.  
    4. private static final Log LOG = LogFactory.getLog(MeetingWebSocketHandler.class);
    5. // 会议id和wsSession列表
    6. private static final ConcurrentHashMap<Integer, CopyOnWriteArraySet<WebSocketSession>> meetingWsSeesionMap = new ConcurrentHashMap<>();
    7.  
    8. @Override
    9. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    10. LOG.info("spring websocket成功建立连接...");
    11. int meetingId = getMeetingId(session);
    12. if (meetingId <= 0) {
    13. singleMessage(session, new TextMessage("会议不存在!"));
    14. session.close();
    15. }
    16. // 如果该会议已存在,则直接加入
    17. if (meetingWsSeesionMap.containsKey(meetingId)) {
    18. CopyOnWriteArraySet<WebSocketSession> webSocketSessions = meetingWsSeesionMap.get(meetingId);
    19. webSocketSessions.add(session);
    20. }
    21. // 如果不存在,则新建
    22. else {
    23. CopyOnWriteArraySet<WebSocketSession> webSocketSessions = new CopyOnWriteArraySet<>();
    24. webSocketSessions.add(session);
    25. meetingWsSeesionMap.put(meetingId, webSocketSessions);
    26. }
    27. }
    28.  
    29. @Override
    30. public void handleTextMessage(WebSocketSession session, TextMessage message) {
    31. if (!session.isOpen())
    32. return;
    33. LOG.info(message.getPayload());
    34. int meetingId = getMeetingId(session);
    35. TextMessage wsMessage = new TextMessage(message.getPayload());
    36. broadcastMessage(meetingId, wsMessage);
    37. }
    38.  
    39. /**
    40. * 发送信息给指定用户
    41. * @param clientId
    42. * @param message
    43. * @return
    44. */
    45. public void singleMessage(WebSocketSession session, TextMessage message) {
    46. if (!session.isOpen())
    47. return;
    48. try {
    49. session.sendMessage(message);
    50. } catch (IOException e) {
    51. e.printStackTrace();
    52. }
    53. }
    54.  
    55. /**
    56. * 广播信息
    57. * @param message
    58. * @return
    59. */
    60. public void broadcastMessage(int meetingId, TextMessage message) {
    61. // 获取会议所有的wsSession
    62. CopyOnWriteArraySet<WebSocketSession> webSocketSessions = meetingWsSeesionMap.get(meetingId);
    63. for (WebSocketSession session : webSocketSessions) {
    64. try {
    65. if (session.isOpen()) {
    66. session.sendMessage(message);
    67. }
    68. } catch (IOException e) {
    69. e.printStackTrace();
    70. }
    71. }
    72. }
    73.  
    74. @Override
    75. public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    76. if (session.isOpen()) {
    77. session.close();
    78. }
    79. LOG.info("连接出错");
    80. }
    81.  
    82. @Override
    83. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    84. LOG.info("连接已关闭:" + status);
    85. int meetingId = getMeetingId(session);
    86. // role 1为主持人
    87. String role = String.valueOf(session.getAttributes().get("role"));
    88. // 如果是主持人,则关闭所有该会议连接
    89. CopyOnWriteArraySet<WebSocketSession> webSocketSessions = meetingWsSeesionMap.get(meetingId);
    90. if (StringUtils.equals("1", role)) {
    91. SystemCache.idRandStrMap.remove(meetingId);
    92. for (WebSocketSession webSocketSession : webSocketSessions) {
    93. webSocketSession.close();
    94. }
    95. webSocketSessions.remove(meetingId);
    96. } else {
    97. webSocketSessions.remove(session);
    98. }
    99. }
    100.  
    101. @Override
    102. public boolean supportsPartialMessages() {
    103. return false;
    104. }
    105.  
    106. private int getMeetingId(WebSocketSession session) {
    107. String randStr = String.valueOf(session.getAttributes().get("randStr"));
    108. int meetingId = SystemCache.getMeetingIdByRandStr(randStr);
    109. return meetingId;
    110. }
    111. }
  • SystemCache(系统缓存,集群部署的情况下,可改为redis实现分布式缓存,单机则不需要)

    1. public class SystemCache {
    2.  
    3. // 会议id和随机字符串的映射关系
    4. public static ConcurrentHashMap<Integer, String> idRandStrMap = new ConcurrentHashMap<>();
    5.  
    6. public static int getMeetingIdByRandStr(String randStr) {
    7. int meetingId = 0;
    8. for (Map.Entry<Integer, String> entry : idRandStrMap.entrySet()) {
    9. if (randStr.equals(entry.getValue())) {
    10. meetingId = entry.getKey();
    11. }
    12. }
    13. return meetingId;
    14. }
    15. }

前端相关代码

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

    1. <!DOCTYPE html>
    2. <html>
    3. <head>
    4. <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    5. <title>在线会议系统</title>
    6. </head>
    7. <body>
    8. <h2>欢迎使用会议系统</h2>
    9. <button id="create" onclick="createMeeting()">创建会议</button>
    10. <hr />
    11. <div id="meeting"></div>
    12. 消息内容:
    13. <input id="text" type="text" />
    14. <button id="send" disabled="disabled" onclick="send()">发送消息</button>
    15. <hr />
    16. <button id="close" onclick="closeWebSocket()">结束会议</button>
    17. <hr />
    18. <div id="message"></div>
    19. </body>
    20.  
    21. <script type="text/javascript" src="js/jquery-1.12.0.js"></script>
    22. <script type="text/javascript">
    23. var websocket = null;
    24. var randStr;
    25. var remote = window.location.host;
    26. function openWebsocket() {
    27. //判断当前浏览器是否支持WebSocket
    28. if ('WebSocket' in window) {
    29. websocket = new WebSocket("ws://" + window.location.host
    30. + "/websocket/spring/meeting?role=1&randStr=" + randStr);
    31.  
    32. //连接发生错误的回调方法
    33. websocket.onerror = function() {
    34. setMessageInnerHTML("会议连接发生错误!");
    35. };
    36.  
    37. //连接成功建立的回调方法
    38. websocket.onopen = function() {
    39. setMessageInnerHTML("会议连接成功...");
    40. document.getElementById("send").disabled = false;
    41. }
    42.  
    43. //接收到消息的回调方法
    44. websocket.onmessage = function(event) {
    45. setMessageInnerHTML(event.data);
    46. }
    47.  
    48. //连接关闭的回调方法
    49. websocket.onclose = function() {
    50. setMessageInnerHTML("会议结束,连接关闭!");
    51. document.getElementById("create").disabled = false;
    52. document.getElementById("send").disabled = true;
    53. }
    54.  
    55. //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常
    56. window.onbeforeunload = function() {
    57. closeWebSocket();
    58. }
    59. } else {
    60. alert('当前浏览器 Not support websocket');
    61. }
    62. }
    63.  
    64. //将消息显示在网页上
    65. function setMessageInnerHTML(innerHTML) {
    66. document.getElementById('message').innerHTML += innerHTML + '<br/>';
    67. }
    68.  
    69. //关闭WebSocket连接
    70. function closeWebSocket() {
    71. websocket.close();
    72. }
    73.  
    74. //发送消息
    75. function send() {
    76. var content = document.getElementById('text').value;
    77. websocket.send(content);
    78. }
    79.  
    80. function createMeeting() {
    81. $.post("/meeting", function(data, status) {
    82. randStr = data.randStr;
    83. $("#create").after("<p>会议邀请码:" + randStr + "</p>");
    84. $("#create").attr("disabled", true);
    85. openWebsocket();
    86. });
    87. }
    88. </script>
    89. </html>
  • meeting-join.html(观众页面,用于加入会议并且也可以发送消息)

    1. <!DOCTYPE html>
    2. <html>
    3. <head>
    4. <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    5. <title>在线会议系统</title>
    6. </head>
    7. <body>
    8. <h2>欢迎使用会议系统</h2>
    9. 会议邀请码:
    10. <input id="randStr" type="text" />
    11. <button id="open" onclick="openWebsocket()">加入会议</button>
    12. <hr />
    13. 消息内容:
    14. <input id="text" type="text" />
    15. <button id="send" disabled="disabled" onclick="send()">发送消息</button>
    16. <hr />
    17. <button id="close" disabled="disabled" onclick="closeWebSocket()">离开会议</button>
    18. <hr />
    19. <div id="message"></div>
    20. </body>
    21.  
    22. <script type="text/javascript">
    23. var websocket = null;
    24. var remote = window.location.host;
    25. function openWebsocket() {
    26. var randStr = document.getElementById('randStr').value;
    27. //判断当前浏览器是否支持WebSocket
    28. if ('WebSocket' in window) {
    29. websocket = new WebSocket("ws://" + window.location.host
    30. + "/websocket/spring/meeting?randStr=" + randStr);
    31.  
    32. //连接发生错误的回调方法
    33. websocket.onerror = function() {
    34. setMessageInnerHTML("会议连接发生错误!");
    35. };
    36.  
    37. //连接成功建立的回调方法
    38. websocket.onopen = function() {
    39. setMessageInnerHTML("会议连接成功...");
    40. document.getElementById("open").disabled = true;
    41. document.getElementById("randStr").disabled = true;
    42. document.getElementById("send").disabled = false;
    43. document.getElementById("close").disabled = false;
    44. }
    45.  
    46. //接收到消息的回调方法
    47. websocket.onmessage = function(event) {
    48. setMessageInnerHTML(event.data);
    49. }
    50.  
    51. //连接关闭的回调方法
    52. websocket.onclose = function() {
    53. setMessageInnerHTML("会议结束,连接关闭!");
    54. document.getElementById("randStr").disabled = false;
    55. document.getElementById("open").disabled = false;
    56. document.getElementById("send").disabled = true;
    57. document.getElementById("close").disabled = true;
    58. }
    59.  
    60. //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常
    61. window.onbeforeunload = function() {
    62. closeWebSocket();
    63. }
    64. } else {
    65. alert('当前浏览器 Not support websocket');
    66. }
    67. }
    68.  
    69. //将消息显示在网页上
    70. function setMessageInnerHTML(innerHTML) {
    71. document.getElementById('message').innerHTML += innerHTML + '<br/>';
    72. }
    73.  
    74. //关闭WebSocket连接
    75. function closeWebSocket() {
    76. websocket.close();
    77. }
    78.  
    79. //发送消息
    80. function send() {
    81. var content = document.getElementById('text').value;
    82. websocket.send(content);
    83. }
    84. </script>
    85. </html>

项目演示

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

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

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

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

总结

  1. 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. ahk保存

    python ;把大写禁用了,因为确实基本不用.`表示删除,caplock+ijkl可以控制光标 SetCapsLockState , AlwaysOff ;用;p来替换书写经常不好使,因为输入多个字 ...

  2. 别人的Linux私房菜(6)文件权限与目录配置

    账号与一般身份用户存放在/etc/passwd文件中 个人密码存放在/etc/shadow文件中 Linux所有组名存放在/etc/group中 ls -al查看所有信息并显示权限等 文件权限的10字 ...

  3. 源码管理工具Git-客户端GitBash常用命令

    1.配置用户名和邮箱地址(第一次启动程序时配置,以后使用不用配置)git config --global user.name "dolen"git config --global ...

  4. centos7安装python3.6后导致防火墙功能无法正常工作的解决办法

    问题:因为默认python版本被设置成了python3.6,而进行防火墙的指令操作频频报错. Jul 19 16:30:51 localhost.localdomain systemd[1]: Sta ...

  5. Papers | 超分辨 + 深度学习(未完待续)

    目录 1. SRCNN 1.1. Contribution 1.2. Inspiration 1.3. Network 1.3.1. Pre-processing 1.3.2. Patch extra ...

  6. Latex一次添加两个图(并列),半栏

    \begin{figure}[t] \centering \includegraphics[width=0.9\columnwidth, clip=true, trim=0 0 0 32]{figur ...

  7. WITH RECOMPILE和OPTION(RECOMPILE)区别仅仅是存储过程级重编译和SQL语句级重编译吗

    在考虑重编译T-SQL(或者存储过程)的时候,有两种方式可以实现强制重编译(前提是忽略导致重编译的其他因素的情况下,比如重建索引,更新统计信息等等), 一是基于WITH RECOMPILE的存储过程级 ...

  8. JS中多维数组的深拷贝的多种实现方式

    因为javascript分原始类型与引用类型(与java.c#类似).Array是引用类型,所以直接用=号赋值的话,只是把源数组的地址(或叫指针)赋值给目的数组,并没有实现数组的数据的拷贝.另外对一维 ...

  9. Mac 安装微软雅黑字体

    https://www.jianshu.com/p/d8c34fff3483 1.找一台Windows电脑,打开字体文件夹C:\Windows\Fonts. 2.搜索"Calibri.微软雅 ...

  10. MongoDB 字符串值长度条件查询

    在实际项目中常常会有根据字段值长度大小进行限制查询,例如查询商品名称过长或过短的商品信息,具体的实现方式可能有多种,在此记录常见的两种实现 使用 $where 查询(性能稍逊一些) 1 2 3 4 5 ...