Spring之WebSocket網頁聊天以及服務器推送

Spring之WebSocket網頁聊天以及服務器推送

轉自:http://www.xdemo.org/spring-websocket-comet/javascript

1. WebSocket protocol 是HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通訊(full-duplex)。html

2. 輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務器發出HTTP request,而後由服務器返回最新的數據給客服端的瀏覽器。這種傳統的HTTP request 的模式帶來很明顯的缺點 – 瀏覽器須要不斷的向服務器發出請求,然而HTTP request 的header是很是長的,裏面包含的有用數據可能只是一個很小的值,這樣會佔用不少的帶寬。java

3. 比較新的技術去作輪詢的效果是Comet – 用了AJAX。但這種技術雖然可達到全雙工通訊,但依然須要發出請求jquery

4. 在 WebSocket API,瀏覽器和服務器只須要要作一個握手的動做,而後,瀏覽器和服務器之間就造成了一條快速通道。二者之間就直接能夠數據互相傳送web

5. 在此WebSocket 協議中,爲咱們實現即時服務帶來了兩大好處:ajax

 5.1. Headerspring

  互相溝通的Header是很小的-大概只有 2 Bytesjson

 5.2. Server Pushapi

瀏覽器支持狀況瀏覽器

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;
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+"&nbsp;"+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 >我&nbsp;"+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下載

百度網盤:http://pan.baidu.com/s/1dD0b15Z

相關文章
相關標籤/搜索