Spring之WebSocket网页聊天以及服务器推送
Spring之WebSocket网页聊天以及服务器推送
转自:http://www.xdemo.org/spring-websocket-comet/
1. WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。
2. 轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的有用数据可能只是一个很小的值,这样会占用很多的带宽。
3. 比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求
4. 在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送
5. 在此WebSocket 协议中,为我们实现即时服务带来了两大好处:
5.1. Header
互相沟通的Header是很小的-大概只有 2 Bytes
5.2. Server Push
浏览器支持情况
| Chrome | 4+ |
| Firefox | 4+ |
| Internet Explorer | 10+ |
| Opera | 10+ |
| Safari | 5+ |
服务器支持
| jetty | 7.0.1+ |
| tomcat | 7.0.27+ |
| Nginx | 1.3.13+ |
| resin | 4+ |
API
var ws = new WebSocket(“ws://echo.websocket.org”);ws.onopen = function(){ws.send(“Test!”); };//当有消息时,会自动调用此方法ws.onmessage = function(evt){console.log(evt.data);ws.close();};ws.onclose = function(evt){console.log(“WebSocketClosed!”);};ws.onerror = function(evt){console.log(“WebSocketError!”);}; |
Demo简介
模拟了两个用户的对话,张三和李四,然后还有发送一个广播,即张三和李四都是可以接收到的,登录的时候分别选择张三和李四即可
Demo效果

Maven依赖
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>2.3.1</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.3.3</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-messaging</artifactId><version>4.0.5.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-websocket</artifactId><version>4.0.5.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>4.0.5.RELEASE</version></dependency><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.3.1</version></dependency><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency> |
Web.xml,spring-mvc.xml,User.java请查看附件
WebSocket相关的类
WebSocketConfig,配置WebSocket的处理器(MyWebSocketHandler)和拦截器(HandShake)
package org.xdemo.example.websocket.websocket;import javax.annotation.Resource;import org.springframework.stereotype.Component;import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;/** * WebScoket配置处理器 * @author Goofy * @Date 2015年6月11日 下午1:15:09 */@Component@EnableWebSocketpublic class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {@ResourceMyWebSocketHandler handler;public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(handler, "/ws").addInterceptors(new HandShake());registry.addHandler(handler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();}} |
MyWebSocketHandler
package org.xdemo.example.websocket.websocket;import java.io.IOException;import java.text.SimpleDateFormat;import java.util.Date;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Map.Entry;import org.springframework.stereotype.Component;import org.springframework.web.socket.CloseStatus;import org.springframework.web.socket.TextMessage;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.WebSocketMessage;import org.springframework.web.socket.WebSocketSession;import org.xdemo.example.websocket.entity.Message;import com.google.gson.Gson;import com.google.gson.GsonBuilder;/** * Socket处理器 * * @author Goofy * @Date 2015年6月11日 下午1:19:50 */@Componentpublic class MyWebSocketHandler implements WebSocketHandler {public static final Map<Long, WebSocketSession> userSocketSessionMap;static {userSocketSessionMap = new HashMap<Long, WebSocketSession>();}/** * 建立连接后 */public void afterConnectionEstablished(WebSocketSession session)throws Exception {Long uid = (Long) session.getAttributes().get("uid");if (userSocketSessionMap.get(uid) == null) {userSocketSessionMap.put(uid, session);}}/** * 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理 */public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {if(message.getPayloadLength()==0)return;Message msg=new Gson().fromJson(message.getPayload().toString(),Message.class);msg.setDate(new Date());sendMessageToUser(msg.getTo(), new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));}/** * 消息传输错误处理 */public void handleTransportError(WebSocketSession session,Throwable exception) throws Exception {if (session.isOpen()) {session.close();}Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();// 移除Socket会话while (it.hasNext()) {Entry<Long, WebSocketSession> entry = it.next();if (entry.getValue().getId().equals(session.getId())) {userSocketSessionMap.remove(entry.getKey());System.out.println("Socket会话已经移除:用户ID" + entry.getKey());break;}}}/** * 关闭连接后 */public void afterConnectionClosed(WebSocketSession session,CloseStatus closeStatus) throws Exception {System.out.println("Websocket:" + session.getId() + "已经关闭");Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();// 移除Socket会话while (it.hasNext()) {Entry<Long, WebSocketSession> entry = it.next();if (entry.getValue().getId().equals(session.getId())) {userSocketSessionMap.remove(entry.getKey());System.out.println("Socket会话已经移除:用户ID" + entry.getKey());break;}}}public boolean supportsPartialMessages() {return false;}/** * 给所有在线用户发送消息 * * @param message * @throws IOException */public void broadcast(final TextMessage message) throws IOException {Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();// 多线程群发while (it.hasNext()) {final Entry<Long, WebSocketSession> entry = it.next();if (entry.getValue().isOpen()) {// entry.getValue().sendMessage(message);new Thread(new Runnable() {public void run() {try {if (entry.getValue().isOpen()) {entry.getValue().sendMessage(message);}} catch (IOException e) {e.printStackTrace();}}}).start();}}}/** * 给某个用户发送消息 * * @param userName * @param message * @throws IOException */public void sendMessageToUser(Long uid, TextMessage message)throws IOException {WebSocketSession session = userSocketSessionMap.get(uid);if (session != null && session.isOpen()) {session.sendMessage(message);}}} |
HandShake(每次建立连接都会进行握手)
package org.xdemo.example.websocket.websocket;import java.util.Map;import javax.servlet.http.HttpSession;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.http.server.ServletServerHttpRequest;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.HandshakeInterceptor;/** * Socket建立连接(握手)和断开 * * @author Goofy * @Date 2015年6月11日 下午2:23:09 */public class HandShake implements HandshakeInterceptor {public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {System.out.println("Websocket:用户[ID:" + ((ServletServerHttpRequest) request).getServletRequest().getSession(false).getAttribute("uid") + "]已经建立连接");if (request instanceof ServletServerHttpRequest) {ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;HttpSession session = servletRequest.getServletRequest().getSession(false);// 标记用户Long uid = (Long) session.getAttribute("uid");if(uid!=null){attributes.put("uid", uid);}else{return false;}}return true;}public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {}} |
一个Controller
package org.xdemo.example.websocket.controller;import java.io.IOException;import java.util.Date;import java.util.HashMap;import java.util.Map;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.servlet.ModelAndView;import org.springframework.web.socket.TextMessage;import org.xdemo.example.websocket.entity.Message;import org.xdemo.example.websocket.entity.User;import org.xdemo.example.websocket.websocket.MyWebSocketHandler;import com.google.gson.GsonBuilder;@Controller@RequestMapping("/msg")public class MsgController {@ResourceMyWebSocketHandler handler;Map<Long, User> users = new HashMap<Long, User>(); //模拟一些数据@ModelAttributepublic void setReqAndRes() {User u1 = new User();u1.setId(1L);u1.setName("张三");users.put(u1.getId(), u1);User u2 = new User();u2.setId(2L);u2.setName("李四");users.put(u2.getId(), u2);}//用户登录@RequestMapping(value="login",method=RequestMethod.POST)public ModelAndView doLogin(User user,HttpServletRequest request){request.getSession().setAttribute("uid", user.getId());request.getSession().setAttribute("name", users.get(user.getId()).getName());return new ModelAndView("redirect:talk");}//跳转到交谈聊天页面@RequestMapping(value="talk",method=RequestMethod.GET)public ModelAndView talk(){return new ModelAndView("talk");}//跳转到发布广播页面@RequestMapping(value="broadcast",method=RequestMethod.GET)public ModelAndView broadcast(){return new ModelAndView("broadcast");}//发布系统广播(群发)@ResponseBody@RequestMapping(value="broadcast",method=RequestMethod.POST)public void broadcast(String text) throws IOException{Message msg=new Message();msg.setDate(new Date());msg.setFrom(-1L);msg.setFromName("系统广播");msg.setTo(0L);msg.setText(text);handler.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));}} |
一个消息的封装的类
package org.xdemo.example.websocket.entity;import java.util.Date;/** * 消息类 * @author Goofy * @Date 2015年6月12日 下午7:32:39 */public class Message {//发送者public Long from;//发送者名称public String fromName;//接收者public Long to;//发送的文本public String text;//发送日期public Date date;public Long getFrom() {return from;}public void setFrom(Long from) {this.from = from;}public Long getTo() {return to;}public void setTo(Long to) {this.to = to;}public String getText() {return text;}public void setText(String text) {this.text = text;}public String getFromName() {return fromName;}public void setFromName(String fromName) {this.fromName = fromName;}public Date getDate() {return date;}public void setDate(Date date) {this.date = date;}} |
聊天页面
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%><%String path = request.getContextPath();String basePath = request.getServerName() + ":"+ request.getServerPort() + path + "/";String basePath2 = request.getScheme() + "://"+ request.getServerName() + ":" + request.getServerPort()+ path + "/";%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title></title><script type="text/javascript" src="<%=basePath2%>resources/jquery.js"></script><style>textarea {height: 300px;width: 100%;resize: none;outline: none;}input[type=button] {float: right;margin: 5px;width: 50px;height: 35px;border: none;color: white;font-weight: bold;outline: none;}.clear {background: red;}.send {background: green;}.clear:active {background: yellow;}.send:active {background: yellow;}.msg {width: 100%;height: 25px;outline: none;}#content {border: 1px solid gray;width: 100%;height: 400px;overflow-y: scroll;}.from {background-color: green;width: 80%;border-radius: 10px;height: 30px;line-height: 30px;margin: 5px;float: left;color: white;padding: 5px;font-size: 22px;}.to {background-color: gray;width: 80%;border-radius: 10px;height: 30px;line-height: 30px;margin: 5px;float: right;color: white;padding: 5px;font-size: 22px;}.name {color: gray;font-size: 12px;}.tmsg_text {color: white;background-color: rgb(47, 47, 47);font-size: 18px;border-radius: 5px;padding: 2px;}.fmsg_text {color: white;background-color: rgb(66, 138, 140);font-size: 18px;border-radius: 5px;padding: 2px;}.sfmsg_text {color: white;background-color: rgb(148, 16, 16);font-size: 18px;border-radius: 5px;padding: 2px;}.tmsg {clear: both;float: right;width: 80%;text-align: right;}.fmsg {clear: both;float: left;width: 80%;}</style><script>var path = '<%=basePath%>';var uid=${uid eq null?-1:uid};if(uid==-1){location.href="<%=basePath2%>";}var from=uid;var fromName='${name}';var to=uid==1?2:1;var websocket;if ('WebSocket' in window) {websocket = new WebSocket("ws://" + path + "/ws?uid="+uid);} else if ('MozWebSocket' in window) {websocket = new MozWebSocket("ws://" + path + "/ws"+uid);} else {websocket = new SockJS("http://" + path + "/ws/sockjs"+uid);}websocket.onopen = function(event) {console.log("WebSocket:已连接");console.log(event);};websocket.onmessage = function(event) {var data=JSON.parse(event.data);console.log("WebSocket:收到一条消息",data);var textCss=data.from==-1?"sfmsg_text":"fmsg_text";$("#content").append("<div><label>"+data.fromName+" "+data.date+"</label><div class='"+textCss+"'>"+data.text+"</div></div>");scrollToBottom();};websocket.onerror = function(event) {console.log("WebSocket:发生错误 ");console.log(event);};websocket.onclose = function(event) {console.log("WebSocket:已关闭");console.log(event);}function sendMsg(){var v=$("#msg").val();if(v==""){return;}else{var data={};data["from"]=from;data["fromName"]=fromName;data["to"]=to;data["text"]=v;websocket.send(JSON.stringify(data));$("#content").append("<div><label>我 "+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</label><div>"+data.text+"</div></div>");scrollToBottom();$("#msg").val("");}}function scrollToBottom(){var div = document.getElementById('content');div.scrollTop = div.scrollHeight;}Date.prototype.Format = function (fmt) { //author: meizz var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小时 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt;}function send(event){var code;if(window.event){code = window.event.keyCode; // IE}else{code = e.which; // Firefox}if(code==13){ sendMsg(); }}function clearAll(){$("#content").empty();}</script></head><body>欢迎:${sessionScope.name }<div id="content"></div><input type="text" placeholder="请输入要发送的信息" id="msg" onkeydown="send(event)"><input type="button" value="发送" onclick="sendMsg()" ><input type="button" value="清空" onclick="clearAll()"></body></html> |
发布广播的页面
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%><%String path = request.getContextPath();String basePath= request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title></title><script type="text/javascript" src="<%=basePath%>resources/jquery.js"></script><script type="text/javascript">var path='<%=basePath%>';function broadcast(){$.ajax({url:path+'msg/broadcast',type:"post",data:{text:$("#msg").val()},dataType:"json",success:function(data){alert("发送成功");}});}</script></head><body>发送广播<textarea style="width:100%;height:300px;" id="msg" ></textarea><input type="button" value="发送" onclick="broadcast()"></body></html> |
Chrome的控制台网络信息
Type:websocket
Time:Pending
表示这是一个websocket请求,请求一直没有结束,可以通过此通道进行双向通信,即双工,实现了服务器推送的效果,也减少了网络流量。

Chrome控制台信息

Demo下载
Spring之WebSocket网页聊天以及服务器推送的更多相关文章
- 基于comet服务器推送技术(web实时聊天)
http://www.cnblogs.com/zengqinglei/archive/2013/03/31/2991189.html Comet 也称反向 Ajax 或服务器端推技术.其思想很简单:将 ...
- SSE技术详解:一种全新的HTML5服务器推送事件技术
前言 一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Ser ...
- HTML5 服务器推送事件(Server-sent Events)实战开发
转自:http://www.ibm.com/developerworks/cn/web/1307_chengfu_serversentevent/ http://www.ibm.com/develop ...
- HTML5中的SSE(服务器推送技术)
本文原链接:https://cloud.tencent.com/developer/article/1194063 SSE技术详解:一种全新的HTML5服务器推送事件技术 前言 概述 基本介绍 与We ...
- 基于Tomcat7、Java、WebSocket的服务器推送聊天室
http://blog.csdn.net/leecho571/article/details/9707497 http://blog.fens.me/java-websocket-intro/ jav ...
- Tomcat学习总结(4)——基于Tomcat7、Java、WebSocket的服务器推送聊天室
前言 HTML5 WebSocket实现了服务器与浏览器的双向通讯,双向通讯使服务器消息推送开发更加简单,最常见的就是即时通讯和对信息实时性要求比较高的应用.以前的服务器消息推送大 ...
- WebSocket 网页聊天室
先给大家开一个原始的websocket的连接使用范例 <?php /* * recv是从套接口接收数据,也就是拿过来,但是不知道是什么 * read是读取拿过来的数据,就是要知道recv过来的是 ...
- HTTP/2 服务器推送(Server Push)教程(HTTP/2 协议的主要目的是提高网页性能,配置Nginx和Apache)
HTTP/2 协议的主要目的是提高网页性能. 头信息(header)原来是直接传输文本,现在是压缩后传输.原来是同一个 TCP 连接里面,上一个回应(response)发送完了,服务器才能发送下一个, ...
- 浅入浅出“服务器推送”之一:Comet简介
最近有个项目,其中有项需求要从服务器端主动向客户端推送数据,本以为很简单,但在实际做的过程中发现很棘手,并没有想象中的简单.从网上搜索学习,发现主流讲的还是Ajax的长轮询技术或者流技术,websoc ...
随机推荐
- php 变量的8类类型
整形,布尔,浮点形,字符串,数组,资源,对象和null php数据类型之查看和判断数据类型 php数据类型之自动转换和强制转换
- pytorch中词向量生成的原理
pytorch中的词向量的使用 在pytorch我们使用nn.embedding进行词嵌入的工作. 具体用法就是: import torch word_to_ix={'hello':0,'world' ...
- Spring使用mutipartFile上传文件报错【Failed to instantiate [org.springframework.web.multipart.MultipartFile]】
报错场景: 使用SSM框架实现文件上传时报“Failed to instantiate [org.springframework.web.multipart.MultipartFile]”错,控制器源 ...
- 5,Linux之文档与目录结构
Linux文件系统结构 Linux目录结构的组织形式和Windows有很大的不同.首先Linux没有“盘(C盘.D盘.E盘)”的概念.已经建立文件系统的硬盘分区被挂载到某一个目录下,用户通过操作目录来 ...
- UOJ #2321. 「清华集训 2017」无限之环
首先裂点表示四个方向 一条边上都有插头或者都不有插头,相当于满足流量平衡 最大流 = 插头个数*2时有解 然后求最小费用最大流 黑白染色分别连原点汇点
- android apk瘦身之 图片压缩 tinypng
参考地址: http://blog.csdn.net/jy692405180/article/details/52409369 http://www.tuicool.com/articles/BraI ...
- Android toolbar menu 字体点击样式
今天在做toolbar的时候,右边的菜单的点击事件,就是文字,然后文字的样式,文字的大小,文字的颜色,高了半天.最后发现,文字点下去之后是有样式的,也就是按下去有阴影. 哥哥的耐心好,就知道这不是问题 ...
- wireshark 获取RTP payload
wireshark 抓包获取RTP TS流数据,保存为TS文件 首先解析RTP流 2.点击菜单栏[Statistics]-[RTP]-[Show All Streams] 3.在Wireshark:R ...
- 网易考拉Android客户端网络模块设计
本文来自网易云社区 作者:王鲁才 客户端开发中不可避免的需要接触到访问网络的需求,如何把访问网络模块设计的更具有扩展性是每一个移动开发者不得不面对的事情.现在有很多主流的网络请求处理框架,如Squar ...
- 《Cracking the Coding Interview》——第5章:位操作——题目8
2014-03-19 06:33 题目:用一个byte数组来模拟WxH的屏幕,每个二进制位表示一个像素.请设计一个画水平线的函数. 解法:一个点一个点地画就可以了.如果要优化的话,其实可以把中间整字节 ...