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 @EnableWebSocket public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer { @Resource MyWebSocketHandler 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 */ @Component public 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 { @Resource MyWebSocketHandler handler; Map<Long, User> users = new HashMap<Long, User>(); //模拟一些数据 @ModelAttribute public 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; 100%; resize: none; outline: none; } input[type=button] { float: right; margin: 5px; 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 { 100%; height: 25px; outline: none; } #content { border: 1px solid gray; 100%; height: 400px; overflow-y: scroll; } .from { background-color: green; 80%; border-radius: 10px; height: 30px; line-height: 30px; margin: 5px; float: left; color: white; padding: 5px; font-size: 22px; } .to { background-color: gray; 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; 80%; text-align: right; } .fmsg { clear: both; float: left; 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 = "100%;height:300px;" id = "msg" ></ textarea > < input type = "button" value = "发送" onclick = "broadcast()" > </ body > </ html > |
Chrome的控制台网络信息
Type:websocket
Time:Pending
表示这是一个websocket请求,请求一直没有结束,可以通过此通道进行双向通信,即双工,实现了服务器推送的效果,也减少了网络流量。
Chrome控制台信息
Demo下载