WebSocket原理与实践
开题思考:如何实现客户端及时获取服务端数据?
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;
- }
- }
- @Controller
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();
- }
- }
- @Configuration
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;
- }
- }
- @Service
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>
- <!DOCTYPE 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>
- <!DOCTYPE html>
项目演示
访问meeting-create.html进入主持人界面,点击创建会议,生成会议邀请码,并显示会议连接成功,界面如下:

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

此时双方就可以互相发送消息,主持人离开会议,则所有人退出,观众离开,不影响会议进行。
- 具体代码地址:https://gitee.com/yehx/websocket-meeting
总结
WebSocket作为一个双通道的协议,颠覆了传统的Client请求Server这种单向通道的模式。由于WebSocket的兴起,Web领域的实时推送技术也被广泛使用,可以简单实现让用户不需要刷新浏览器就可以获得实时更新。它有着广泛的应用场景,比如在线聊天室、在线客服系统、评论系统、WebIM等。
WebSocket原理与实践的更多相关文章
- WebSocket原理与实践(四)--生成数据帧
WebSocket原理与实践(四)--生成数据帧 从服务器发往客户端的数据也是同样的数据帧,但是从服务器发送到客户端的数据帧不需要掩码的.我们自己需要去生成数据帧,解析数据帧的时候我们需要分片. 消息 ...
- WebSocket原理与实践(三)--解析数据帧
WebSocket原理与实践(三)--解析数据帧 1-1 理解数据帧的含义: 在WebSocket协议中,数据是通过帧序列来传输的.为了数据安全原因,客户端必须掩码(mask)它发送到服务器的所有 ...
- WebSocket原理与实践(二)---WebSocket协议
WebSocket原理与实践(二)---WebSocket协议 WebSocket协议是为了解决web即时应用中服务器与客户端浏览器全双工通信问题而设计的.协议定义ws和wss协议,分别为普通请求和基 ...
- WebSocket原理与实践(一)---基本原理
WebSocket原理与实践(一)---基本原理 一:为什么要使用WebSocket?1. 了解现有的HTTP的架构模式:Http是客户端/服务器模式中请求-响应所用的协议,在这种模式中,客户端(一般 ...
- Atitit 管理原理与实践attilax总结
Atitit 管理原理与实践attilax总结 1. 管理学分类1 2. 我要学的管理学科2 3. 管理学原理2 4. 管理心理学2 5. 现代管理理论与方法2 6. <领导科学与艺术4 7. ...
- Atitit.ide技术原理与实践attilax总结
Atitit.ide技术原理与实践attilax总结 1.1. 语法着色1 1.2. 智能提示1 1.3. 类成员outline..func list1 1.4. 类型推导(type inferenc ...
- Atitit.异步编程技术原理与实践attilax总结
Atitit.异步编程技术原理与实践attilax总结 1. 俩种实现模式 类库方式,以及语言方式,java futuretask ,c# await1 2. 事件(中断)机制1 3. Await 模 ...
- Atitit.软件兼容性原理与实践 v5 qa2.docx
Atitit.软件兼容性原理与实践 v5 qa2.docx 1. Keyword2 2. 提升兼容性的原则2 2.1. What 与how 分离2 2.2. 老人老办法,新人新办法,只新增,少修改 ...
- Atitit 表达式原理 语法分析 原理与实践 解析java的dsl 递归下降是现阶段主流的语法分析方法
Atitit 表达式原理 语法分析 原理与实践 解析java的dsl 递归下降是现阶段主流的语法分析方法 于是我们可以把上面的语法改写成如下形式:1 合并前缀1 语法分析有自上而下和自下而上两种分析 ...
随机推荐
- ahk保存
python ;把大写禁用了,因为确实基本不用.`表示删除,caplock+ijkl可以控制光标 SetCapsLockState , AlwaysOff ;用;p来替换书写经常不好使,因为输入多个字 ...
- 别人的Linux私房菜(6)文件权限与目录配置
账号与一般身份用户存放在/etc/passwd文件中 个人密码存放在/etc/shadow文件中 Linux所有组名存放在/etc/group中 ls -al查看所有信息并显示权限等 文件权限的10字 ...
- 源码管理工具Git-客户端GitBash常用命令
1.配置用户名和邮箱地址(第一次启动程序时配置,以后使用不用配置)git config --global user.name "dolen"git config --global ...
- centos7安装python3.6后导致防火墙功能无法正常工作的解决办法
问题:因为默认python版本被设置成了python3.6,而进行防火墙的指令操作频频报错. Jul 19 16:30:51 localhost.localdomain systemd[1]: Sta ...
- Papers | 超分辨 + 深度学习(未完待续)
目录 1. SRCNN 1.1. Contribution 1.2. Inspiration 1.3. Network 1.3.1. Pre-processing 1.3.2. Patch extra ...
- Latex一次添加两个图(并列),半栏
\begin{figure}[t] \centering \includegraphics[width=0.9\columnwidth, clip=true, trim=0 0 0 32]{figur ...
- WITH RECOMPILE和OPTION(RECOMPILE)区别仅仅是存储过程级重编译和SQL语句级重编译吗
在考虑重编译T-SQL(或者存储过程)的时候,有两种方式可以实现强制重编译(前提是忽略导致重编译的其他因素的情况下,比如重建索引,更新统计信息等等), 一是基于WITH RECOMPILE的存储过程级 ...
- JS中多维数组的深拷贝的多种实现方式
因为javascript分原始类型与引用类型(与java.c#类似).Array是引用类型,所以直接用=号赋值的话,只是把源数组的地址(或叫指针)赋值给目的数组,并没有实现数组的数据的拷贝.另外对一维 ...
- Mac 安装微软雅黑字体
https://www.jianshu.com/p/d8c34fff3483 1.找一台Windows电脑,打开字体文件夹C:\Windows\Fonts. 2.搜索"Calibri.微软雅 ...
- MongoDB 字符串值长度条件查询
在实际项目中常常会有根据字段值长度大小进行限制查询,例如查询商品名称过长或过短的商品信息,具体的实现方式可能有多种,在此记录常见的两种实现 使用 $where 查询(性能稍逊一些) 1 2 3 4 5 ...